/**
 * reflection-api.ts — REST router for reflection history + metrics
 *
 * Mounted at /api/local/reflection
 *
 * Routes:
 *   GET  /history                      — paged index listing (limit, before cursor)
 *   GET  /history/:snapshotId          — full detail (meta + before/after files + diff)
 *   POST /history/:snapshotId/revert   — idempotent revert
 *   GET  /metrics                      — outcome counts + token sums (?days=30)
 *   GET  /latest-for-task/:taskId      — feeds the ReflectionBadge on OverviewTab
 *
 * Auth: all routes require an authenticated user (req.user).
 * Owner: every operation is scoped to req.user.id — 404 on mismatch (no existence leak).
 */

import { Router, type Request, type Response } from 'express';
import { logger } from '../logger.js';
import {
  listSnapshots,
  readSnapshot,
  revertSnapshotForUser,
  type SnapshotDeps,
  type SnapshotIndexEntry,
  type SnapshotDetail,
} from '../engine/reflection/snapshot.js';
import type { Repository } from '../db/repository.js';

// ── Types ──────────────────────────────────────────────────────────────────────

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

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

// ── Deps ───────────────────────────────────────────────────────────────────────

export interface ReflectionApiDeps {
  /** Root data dir (same as userFolderRoot in the rest of the app). */
  dataDir: string;
  /** Repository for job lookups (latest-for-task, metrics). */
  repo: Repository;
  /** When false (local-dev mode), inject a synthetic 'local' user if req.user absent. */
  authActive?: boolean;
}

// ── Helpers ────────────────────────────────────────────────────────────────────

function makeDeps(dataDir: string): SnapshotDeps {
  return { dataDir };
}

// ── Factory ────────────────────────────────────────────────────────────────────

export function createReflectionApi(deps: ReflectionApiDeps): Router {
  const { dataDir, repo } = deps;
  const authActive = deps.authActive ?? true;

  const r = Router();

  // ── Auth gate ──────────────────────────────────────────────────────────────
  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();
  });

  // ── GET /history ───────────────────────────────────────────────────────────
  // Returns paged list of snapshot index entries (most recent first).
  // Query params:
  //   limit  — max items to return (default 50, max 200)
  //   before — ISO timestamp cursor (exclusive, for pagination)
  r.get('/history', (req: Request, res: Response) => {
    const u = getUser(req)!;

    const rawLimit = parseInt(String(req.query.limit ?? '50'), 10);
    const limit = isNaN(rawLimit) || rawLimit < 1 ? 50 : Math.min(rawLimit, 200);
    const before = typeof req.query.before === 'string' ? req.query.before : undefined;

    try {
      const items = listSnapshots(makeDeps(dataDir), u.id, { limit, before });

      // Compute nextCursor from the last item's ts (if we got a full page)
      const nextCursor: string | null =
        items.length === limit ? (items[items.length - 1]?.ts ?? null) : null;

      res.json({ items, nextCursor });
    } catch (err) {
      logger.error(`[reflection-api] GET /history failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to list reflection history' });
    }
  });

  // ── GET /history/:snapshotId ───────────────────────────────────────────────
  // Returns full snapshot detail for the owner.  404 for non-owner or missing.
  r.get('/history/:snapshotId', (req: Request, res: Response) => {
    const u = getUser(req)!;
    const { snapshotId } = req.params;

    try {
      const detail: SnapshotDetail | null = readSnapshot(makeDeps(dataDir), u.id, snapshotId);

      if (!detail) {
        // Either doesn't exist or belongs to another user — always 404
        res.status(404).json({ error: 'not_found' });
        return;
      }

      // Owner check: the meta.json embeds userId
      if (detail.userId !== u.id) {
        res.status(404).json({ error: 'not_found' });
        return;
      }

      res.json(detail);
    } catch (err) {
      logger.error(`[reflection-api] GET /history/${snapshotId} failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to read snapshot' });
    }
  });

  // ── POST /history/:snapshotId/revert ──────────────────────────────────────
  // Idempotent revert.  Returns { reverted: true } on first call, { reverted: false } thereafter.
  r.post('/history/:snapshotId/revert', async (req: Request, res: Response) => {
    const u = getUser(req)!;
    const { snapshotId } = req.params;

    // Owner check: read meta first (cheaper than a full revert attempt that fails)
    try {
      const detail = readSnapshot(makeDeps(dataDir), u.id, snapshotId);
      if (!detail || detail.userId !== u.id) {
        res.status(404).json({ error: 'not_found' });
        return;
      }
    } catch (err) {
      logger.error(`[reflection-api] POST /revert owner-check failed user=${u.id} snapshotId=${snapshotId} err=${err}`);
      res.status(500).json({ error: 'Failed to verify snapshot ownership' });
      return;
    }

    try {
      const result = await revertSnapshotForUser(makeDeps(dataDir), u.id, snapshotId);
      logger.info(`[reflection-api] POST /revert snapshotId=${snapshotId} user=${u.id} reverted=${result.reverted}`);
      res.json(result);
    } catch (err) {
      logger.error(`[reflection-api] POST /revert failed user=${u.id} snapshotId=${snapshotId} err=${err}`);
      res.status(500).json({ error: 'Failed to revert snapshot' });
    }
  });

  // ── GET /metrics ───────────────────────────────────────────────────────────
  // Returns aggregated outcome counts + token sums for the caller.
  // Query params:
  //   days — look-back window in days (default 30)
  //
  // Gracefully returns zero counts when the reflection_metrics table doesn't
  // exist yet (Phase 8.1 creates it and starts inserting rows).
  r.get('/metrics', (req: Request, res: Response) => {
    const u = getUser(req)!;

    const rawDays = parseInt(String(req.query.days ?? '30'), 10);
    const days = isNaN(rawDays) || rawDays < 1 ? 30 : Math.min(rawDays, 365);

    const zeroMetrics = {
      applied: 0,
      partial: 0,
      abstained: 0,
      rejected: 0,
      failed: 0,
      tokensIn: 0,
      tokensOut: 0,
      pieceEdits: 0,
    };

    try {
      const db = repo.getDb();

      // Check if the table exists before querying (Phase 8.1 creates it)
      const tableExists = (db.prepare(`PRAGMA table_info('reflection_metrics')`).all() as Array<{ name: string }>).length > 0;
      if (!tableExists) {
        res.json(zeroMetrics);
        return;
      }

      // Phase 8.1's reflection_metrics.created_at is INTEGER ms-since-epoch.
      const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;

      interface MetricsRow {
        outcome: string;
        piece_edited: number;
        tokens_in: number | null;
        tokens_out: number | null;
      }

      const rows = db
        .prepare(
          `SELECT outcome, piece_edited, tokens_in, tokens_out
           FROM reflection_metrics
           WHERE user_id = ? AND created_at >= ?`,
        )
        .all(u.id, cutoff) as MetricsRow[];

      const metrics = { ...zeroMetrics };
      for (const row of rows) {
        switch (row.outcome) {
          case 'applied':    metrics.applied++;    break;
          case 'partial':    metrics.partial++;    break;
          case 'abstained':  metrics.abstained++;  break;
          case 'rejected':   metrics.rejected++;   break;
          case 'failed':     metrics.failed++;     break;
        }
        metrics.tokensIn  += row.tokens_in  ?? 0;
        metrics.tokensOut += row.tokens_out ?? 0;
        if (row.piece_edited) metrics.pieceEdits++;
      }

      res.json(metrics);
    } catch (err) {
      // If the table doesn't exist yet (race window between check and query), return zeros
      const msg = String(err);
      if (msg.includes('no such table')) {
        res.json(zeroMetrics);
        return;
      }
      logger.error(`[reflection-api] GET /metrics failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to load reflection metrics' });
    }
  });

  // ── GET /latest-for-task/:taskId ──────────────────────────────────────────
  // Returns the most recent snapshot triggered by the given local task's job,
  // or null if none found.  Used by the ReflectionBadge on OverviewTab (Phase 7.5).
  //
  // Owner check: the job must belong to the caller (owner_id match), or the
  // user must be an admin.  Returns null (not 404) when there's no snapshot —
  // the badge simply stays hidden.
  r.get('/latest-for-task/:taskId', (req: Request, res: Response) => {
    const u = getUser(req)!;
    const rawTaskId = parseInt(req.params.taskId, 10);

    if (isNaN(rawTaskId)) {
      res.status(400).json({ error: 'invalid_task_id' });
      return;
    }

    try {
      const db = repo.getDb();

      // Find all jobs for this local task, owned by the caller (or any if admin)
      const repoName = `local/task-${rawTaskId}`;

      interface JobRow {
        id: string;
        owner_id: string | null;
      }

      let rows: JobRow[];
      if (u.role === 'admin') {
        rows = db
          .prepare(`SELECT id, owner_id FROM jobs WHERE repo = ? ORDER BY created_at DESC`)
          .all(repoName) as JobRow[];
      } else {
        rows = db
          .prepare(
            `SELECT id, owner_id FROM jobs WHERE repo = ? AND owner_id = ? ORDER BY created_at DESC`,
          )
          .all(repoName, u.id) as JobRow[];
      }

      if (rows.length === 0) {
        res.json(null);
        return;
      }

      // Collect all job IDs for this task
      const jobIds = new Set(rows.map((r) => r.id));

      // List all snapshots for this user (no limit — we need to scan for a match)
      const allSnapshots = listSnapshots(makeDeps(dataDir), u.id, { limit: 200 });

      // Find the most recent snapshot whose originalJobId is in our job set
      const match = allSnapshots.find((s: SnapshotIndexEntry) => jobIds.has(s.jobId));

      if (!match) {
        res.json(null);
        return;
      }

      // Load the full detail so the badge can show outcome + counts
      const detail = readSnapshot(makeDeps(dataDir), u.id, match.snapshotId);
      if (!detail) {
        res.json(null);
        return;
      }

      res.json({
        snapshotId: detail.snapshotId,
        outcome: detail.outcome,
        memoryChanges: detail.memoryChanges,
        pieceEdited: detail.pieceEdited,
      });
    } catch (err) {
      logger.error(`[reflection-api] GET /latest-for-task/${rawTaskId} failed user=${u.id} err=${err}`);
      res.status(500).json({ error: 'Failed to load latest reflection for task' });
    }
  });

  return r;
}
