/**
 * dashboard-api.ts — REST router for the Side Info Panel.
 * Mounted at /api/local/dashboard.
 *
 * Routes:
 *   GET /workers      — worker idle/running (no job details)
 *   GET /node-status  — latest BackendStatusRegistry snapshot (GPU/node feed)
 *
 * Auth: all routes require req.user (or fall back to 'local' when authActive=false).
 */

import { Router, type Request, type Response } from 'express';
import { createHash } from 'crypto';
import 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';

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('/workers', async (req, res) => {
    try {
      // Admin-only: surface which users' jobs occupy each worker/GPU. The
      // panel is shown to all tenants, so non-admins never get owner names.
      const includeOwners = getUser(req)?.role === 'admin';
      const workers = await collectWorkerStatuses(
        repo,
        getWorkers(),
        deps.backendStatusRegistry ?? null,
        { includeOwners },
      );
      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;
}
