/**
 * dashboard-api.ts — REST router for Side Info Panel.
 * Mounted at /api/local/dashboard.
 *
 * Routes:
 *   GET    /widgets         — list current user's widgets
 *   POST   /widgets         — create
 *   PATCH  /widgets/:id     — update title/content
 *   DELETE /widgets/:id     — delete
 *   PUT    /widgets/reorder — reorder by id list
 *   GET    /workers         — worker idle/running (no job details)
 *
 * Auth: all routes require req.user (or fall back to 'local' when authActive=false).
 * Owner: every operation scopes to req.user.id; cross-user access returns 404.
 */

import { Router, type Request, type Response } from 'express';
import { createHash } from 'crypto';
import { isDashboardWidgetKind, type DashboardWidgetKind, type Repository } from '../db/repository.js';
import type { WorkerDef } from '../config.js';
import { collectWorkerStatuses } from './dashboard-workers.js';
import type { BackendStatusRegistry } from '../engine/backend-status-registry.js';
import { logger } from '../logger.js';

const SLUG_PATTERN = /^[a-z0-9-]+$/;
const MAX_SLUG_LEN = 32;
const MAX_TITLE_LEN = 64;
const MAX_CONTENT_BYTES = 64 * 1024;

interface AuthedUser { id: string; role: string; }

function getUser(req: Request): AuthedUser | null {
  return (req.user as AuthedUser | undefined) ?? null;
}

export interface DashboardApiDeps {
  repo: Repository;
  getWorkers: () => WorkerDef[];
  authActive?: boolean;
  /**
   * Optional BackendStatusRegistry. When supplied, the API exposes
   * GET /node-status; when omitted (e.g. in unit tests that don't care
   * about node status), the route 503s.
   */
  backendStatusRegistry?: BackendStatusRegistry | null;
}

export function createDashboardApi(deps: DashboardApiDeps): Router {
  const { repo, getWorkers } = deps;
  const authActive = deps.authActive ?? true;

  const r = Router();

  r.use((req: Request, res: Response, next) => {
    if (!authActive && !getUser(req)) {
      (req as any).user = { id: 'local', role: 'user' };
    }
    if (!getUser(req)) {
      res.status(401).json({ error: 'Unauthenticated' });
      return;
    }
    next();
  });

  r.get('/widgets', async (req, res) => {
    const u = getUser(req)!;
    try {
      const widgets = await repo.listDashboardWidgets(u.id);
      res.json({ widgets });
    } catch (err) {
      logger.error(`[dashboard-api] GET /widgets failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to list widgets' });
    }
  });

  r.post('/widgets', async (req, res) => {
    const u = getUser(req)!;
    const { slug, title, content, kind } = (req.body ?? {}) as {
      slug?: string;
      title?: string;
      content?: string;
      kind?: string;
    };
    if (!slug || !SLUG_PATTERN.test(slug) || slug.length > MAX_SLUG_LEN) {
      res.status(400).json({ error: 'invalid slug (lowercase a-z, 0-9, hyphen; max 32 chars)' });
      return;
    }
    if (!title || title.length > MAX_TITLE_LEN) {
      res.status(400).json({ error: `title required and <= ${MAX_TITLE_LEN} chars` });
      return;
    }
    if (content !== undefined && Buffer.byteLength(content, 'utf8') > MAX_CONTENT_BYTES) {
      res.status(400).json({ error: `content exceeds ${MAX_CONTENT_BYTES} bytes` });
      return;
    }
    // kind is optional; defaults to 'markdown' for backward compat.
    let resolvedKind: DashboardWidgetKind = 'markdown';
    if (kind !== undefined) {
      if (!isDashboardWidgetKind(kind)) {
        res.status(400).json({ error: 'invalid kind (allowed: markdown, node-status)' });
        return;
      }
      resolvedKind = kind;
    }
    try {
      const widget = await repo.createDashboardWidget({
        userId: u.id,
        slug,
        title,
        content: content ?? '',
        kind: resolvedKind,
      });
      res.status(201).json({ widget });
    } catch (err: any) {
      if (String(err?.message ?? err).includes('UNIQUE')) {
        res.status(409).json({ error: 'slug already exists' });
        return;
      }
      logger.error(`[dashboard-api] POST /widgets failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to create widget' });
    }
  });

  r.patch('/widgets/:id', async (req, res) => {
    const u = getUser(req)!;
    const id = Number(req.params.id);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const existing = await repo.getDashboardWidget(id, u.id);
    if (!existing) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    const { title, content } = (req.body ?? {}) as { title?: string; content?: string };
    // Non-markdown widget kinds (currently just 'node-status') render
    // data live from a backing source instead of stored markdown — any
    // content the caller sends would be dead state at best and a
    // confusing surprise on the next render at worst. Title remains
    // editable so the user can rename the panel.
    if (existing.kind !== 'markdown' && content !== undefined) {
      res.status(400).json({
        error: `cannot edit content of ${existing.kind} widget (title-only updates allowed)`,
      });
      return;
    }
    if (title !== undefined && (title.length === 0 || title.length > MAX_TITLE_LEN)) {
      res.status(400).json({ error: `title must be 1..${MAX_TITLE_LEN} chars` });
      return;
    }
    if (content !== undefined && Buffer.byteLength(content, 'utf8') > MAX_CONTENT_BYTES) {
      res.status(400).json({ error: `content exceeds ${MAX_CONTENT_BYTES} bytes` });
      return;
    }
    try {
      const widget = await repo.updateDashboardWidget(id, u.id, { title, content });
      res.json({ widget });
    } catch (err) {
      logger.error(`[dashboard-api] PATCH /widgets/${id} failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to update widget' });
    }
  });

  r.delete('/widgets/:id', async (req, res) => {
    const u = getUser(req)!;
    const id = Number(req.params.id);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const existing = await repo.getDashboardWidget(id, u.id);
    if (!existing) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    try {
      await repo.deleteDashboardWidget(id, u.id);
      res.status(204).end();
    } catch (err) {
      logger.error(`[dashboard-api] DELETE /widgets/${id} failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to delete widget' });
    }
  });

  r.put('/widgets/reorder', async (req, res) => {
    const u = getUser(req)!;
    const { ids } = (req.body ?? {}) as { ids?: number[] };
    if (!Array.isArray(ids) || !ids.every(n => Number.isFinite(n))) {
      res.status(400).json({ error: 'ids must be array of numbers' });
      return;
    }
    try {
      await repo.reorderDashboardWidgets(u.id, ids);
      res.json({ ok: true });
    } catch (err) {
      logger.error(`[dashboard-api] reorder failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to reorder' });
    }
  });

  r.get('/workers', async (_req, res) => {
    try {
      const workers = await collectWorkerStatuses(repo, getWorkers(), deps.backendStatusRegistry ?? null);
      res.json({ workers });
    } catch (err) {
      logger.error(`[dashboard-api] GET /workers failed err=${err}`);
      res.status(500).json({ error: 'Failed to list worker status' });
    }
  });

  // GET /node-status
  //
  // Returns the latest BackendStatusRegistry snapshot. The registry is
  // already polling in the background at a fixed cadence, so this
  // handler is a cheap cache read.
  //
  // Caching headers (Phase C):
  //  - `Cache-Control: no-store` — multiple AAO instances might sit
  //    behind a shared proxy/CDN with body-level caching defaults; the
  //    snapshot is per-process state and must never be cached
  //    intermediately.
  //  - Weak ETag of the JSON payload + 304 short-circuit — when 5s polls
  //    land on an unchanged registry (idle pool, no probes flipped) the
  //    response avoids re-serialising the body and the browser refetch
  //    skips the JSON parse, halving the per-tick CPU under N-tab loads.
  //
  // The registry tick also notifies the registry that a subscriber is
  // active so the polling cadence can fall back to the idle interval
  // when no UI is open (see BackendStatusRegistry.noteSubscriberActivity).
  r.get('/node-status', async (req, res) => {
    const reg = deps.backendStatusRegistry ?? null;
    if (!reg) {
      // The registry is started by server.ts; when running under tests
      // that don't bother to construct one, we'd rather signal "feature
      // disabled" than crash.
      res.status(503).json({ nodes: [], error: 'node-status registry not configured' });
      return;
    }
    try {
      const nodes = reg.getAll();
      // Signal to the registry that a UI is actively watching so the
      // polling cadence stays in the active (5s) band; without any
      // recent GET the registry falls back to the idle (30s) cadence.
      if (typeof reg.noteSubscriberActivity === 'function') {
        try { reg.noteSubscriberActivity(); } catch { /* never fail the GET on metrics */ }
      }
      const body = JSON.stringify({ nodes });
      // Weak ETag: payload identity is the only thing that matters for
      // 304 short-circuiting; we don't care about byte-for-byte
      // equivalence (no Content-Encoding negotiation here).
      const etag = `W/"${createHash('sha1').update(body).digest('hex').slice(0, 16)}"`;
      res.setHeader('Cache-Control', 'no-store');
      res.setHeader('ETag', etag);
      const inm = req.headers['if-none-match'];
      // RFC 9110 §13.1.2: If-None-Match may carry a comma-separated
      // list of entity tags (browsers' BFCache restore and HTTP
      // intermediaries can both produce multi-tag headers). Strict
      // equality on the whole header would silently miss matches and
      // re-send the body unnecessarily — splitting + per-tag compare
      // is the spec-compliant behaviour.
      if (typeof inm === 'string') {
        const tags = inm.split(',').map(s => s.trim());
        if (tags.includes(etag)) {
          res.status(304).end();
          return;
        }
      }
      res.type('application/json').send(body);
    } catch (err) {
      logger.error(`[dashboard-api] GET /node-status failed err=${err}`);
      res.status(500).json({ error: 'Failed to read node status' });
    }
  });

  return r;
}
