import { randomUUID } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import { BadRequestError, ForbiddenError } from '../../lib/errors.js';
import * as agentRepo from '../agents/repository.js';
import {
  CreateEvaluationCaseSchema,
  CreateEvaluationRunSchema,
  CreateEvaluationSuiteSchema,
  EvaluationCaseDoc,
  EvaluationResultDoc,
  EvaluationRunDoc,
  EvaluationSuiteDoc,
  ListEvaluationSuitesQuerySchema,
  RecordEvaluationResultsSchema,
  UpdateEvaluationSuiteSchema,
} from './types.js';
import * as repo from './repository.js';

function requireAdmin(req: { jwtPayload?: { sub?: string; role?: string; productId?: string } }): {
  userId: string;
  productId: string;
} {
  const payload = req.jwtPayload;
  if (!payload?.sub) throw new ForbiddenError('Authentication required');
  if (!payload.role || !['super_admin', 'admin'].includes(payload.role)) {
    throw new ForbiddenError('Admin access required');
  }
  return {
    userId: payload.sub,
    productId: payload.productId ?? process.env.DEFAULT_PRODUCT_ID ?? 'lysnrai',
  };
}

function validationError(message: string): never {
  throw new BadRequestError(message);
}

export async function agentEvalRoutes(app: FastifyInstance) {
  app.get('/agent-evals/suites', async req => {
    const access = requireAdmin(req);
    const parsed = ListEvaluationSuitesQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }
    return repo.listSuites(access.productId, parsed.data);
  });

  app.post('/agent-evals/suites', async req => {
    const access = requireAdmin(req);
    const parsed = CreateEvaluationSuiteSchema.safeParse(req.body);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    await agentRepo.getAgent(parsed.data.agentId, access.productId);
    const now = new Date().toISOString();
    const suite: EvaluationSuiteDoc = {
      id: `evals_${randomUUID()}`,
      productId: access.productId,
      agentId: parsed.data.agentId,
      name: parsed.data.name,
      description: parsed.data.description,
      status: 'draft',
      passThreshold: parsed.data.passThreshold,
      targetVersion: parsed.data.targetVersion,
      tags: parsed.data.tags,
      metadata: parsed.data.metadata,
      createdBy: access.userId,
      createdAt: now,
      updatedAt: now,
    };
    return repo.createSuite(suite);
  });

  app.get('/agent-evals/suites/:id', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.getSuite(id, access.productId);
  });

  app.patch('/agent-evals/suites/:id', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const parsed = UpdateEvaluationSuiteSchema.safeParse(req.body);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }
    return repo.updateSuite(id, access.productId, parsed.data);
  });

  app.delete('/agent-evals/suites/:id', async (req, reply) => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    await repo.getSuite(id, access.productId); // throws NotFoundError if missing
    await repo.deleteSuite(id, access.productId);
    return reply.code(204).send();
  });

  app.get('/agent-evals/suites/:id/cases', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.listCases(id);
  });

  app.post('/agent-evals/suites/:id/cases', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    await repo.getSuite(id, access.productId);
    const parsed = CreateEvaluationCaseSchema.safeParse(req.body);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const doc: EvaluationCaseDoc = {
      id: `${id}:case:${randomUUID()}`,
      suiteId: id,
      productId: access.productId,
      name: parsed.data.name,
      input: parsed.data.input,
      expectedOutput: parsed.data.expectedOutput,
      rubric: parsed.data.rubric,
      tags: parsed.data.tags,
      critical: parsed.data.critical,
      createdAt: new Date().toISOString(),
    };
    return repo.createCase(doc);
  });

  app.post('/agent-evals/suites/:id/runs', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const suite = await repo.getSuite(id, access.productId);
    const parsed = CreateEvaluationRunSchema.safeParse(req.body ?? {});
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const agent = await agentRepo.getAgent(suite.agentId, access.productId);
    const versionId =
      parsed.data.agentVersionId ??
      `${suite.agentId}:v${suite.targetVersion ?? agent.currentVersion}`;
    const version = await agentRepo.getAgentVersion(versionId, suite.agentId);
    const cases = await repo.listCases(id);

    const run: EvaluationRunDoc = {
      id: `evalrun_${randomUUID()}`,
      suiteId: id,
      agentId: suite.agentId,
      productId: access.productId,
      agentVersionId: version.id,
      agentVersion: version.version,
      status: 'queued',
      passThreshold: suite.passThreshold,
      releaseGate: parsed.data.releaseGate,
      totalCases: cases.length,
      passedCases: 0,
      failedCases: 0,
      triggeredBy: access.userId,
      startedAt: new Date().toISOString(),
      reviewRequired: false,
    };

    return repo.createRun(run);
  });

  app.get('/agent-evals/runs/:id', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.getRun(id, access.productId);
  });

  app.get('/agent-evals/runs/:id/results', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.listResults(id);
  });

  app.post('/agent-evals/runs/:id/results', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const run = await repo.getRun(id, access.productId);
    const parsed = RecordEvaluationResultsSchema.safeParse(req.body);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const cases = await repo.listCases(run.suiteId);
    const knownCaseIds = new Set(cases.map(item => item.id));
    for (const result of parsed.data.results) {
      if (!knownCaseIds.has(result.caseId)) {
        validationError(`Unknown evaluation case '${result.caseId}' for suite '${run.suiteId}'`);
      }
    }

    const now = new Date().toISOString();
    const docs: EvaluationResultDoc[] = parsed.data.results.map(result => ({
      id: `${id}:${result.caseId}`,
      runId: id,
      caseId: result.caseId,
      productId: access.productId,
      passed: result.passed,
      score: result.score,
      actualOutput: result.actualOutput,
      notes: result.notes,
      createdAt: now,
    }));
    await repo.createResults(docs);

    const passedCases = parsed.data.results.filter(result => result.passed).length;
    const failedCases = parsed.data.results.length - passedCases;
    const averageScore =
      parsed.data.results.reduce(
        (sum, result) => sum + (result.score ?? (result.passed ? 1 : 0)),
        0
      ) / parsed.data.results.length;
    const criticalFailures = cases
      .filter(item => item.critical)
      .some(item =>
        parsed.data.results.some(result => result.caseId === item.id && !result.passed)
      );
    const reviewRequired = criticalFailures || averageScore < run.passThreshold;

    const verdict = criticalFailures
      ? 'needs_review'
      : averageScore >= run.passThreshold
        ? 'pass'
        : 'fail';

    return repo.updateRun(id, access.productId, {
      status: 'completed',
      passedCases,
      failedCases,
      score: averageScore,
      verdict,
      completedAt: now,
      summary: parsed.data.summary,
      reviewRequired,
    });
  });

  // ── Run History ─────────────────────────────────────────

  app.get('/agent-evals/suites/:id/runs', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const query = req.query as { limit?: string };
    return repo.listRunsBySuite(access.productId, id, parseInt(query.limit ?? '20', 10));
  });

  // ── Regression Comparison ───────────────────────────────

  app.get('/agent-evals/suites/:id/regression', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };

    const runs = await repo.listRunsBySuite(access.productId, id, 10);
    const completedRuns = runs.filter(r => r.status === 'completed' && r.score !== undefined);

    if (completedRuns.length < 2) {
      return {
        regression: false,
        message: 'Need at least 2 completed runs to compare',
        runs: completedRuns,
      };
    }

    const latest = completedRuns[0];
    const previous = completedRuns[1];
    const scoreDelta = (latest.score ?? 0) - (previous.score ?? 0);
    const regression = scoreDelta < -0.05; // 5% drop = regression

    return {
      regression,
      latest: {
        runId: latest.id,
        version: latest.agentVersion,
        score: latest.score,
        verdict: latest.verdict,
        passedCases: latest.passedCases,
        failedCases: latest.failedCases,
      },
      previous: {
        runId: previous.id,
        version: previous.agentVersion,
        score: previous.score,
        verdict: previous.verdict,
        passedCases: previous.passedCases,
        failedCases: previous.failedCases,
      },
      scoreDelta: Math.round(scoreDelta * 1000) / 1000,
      trend: completedRuns.slice(0, 5).map(r => ({
        runId: r.id,
        version: r.agentVersion,
        score: r.score,
        date: r.startedAt,
      })),
    };
  });

  // ── Release Gate Check ──────────────────────────────────

  app.get('/agent-evals/suites/:id/gate', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const suite = await repo.getSuite(id, access.productId);

    const runs = await repo.listRunsBySuite(access.productId, id, 5);
    const latestGateRun = runs.find(r => r.releaseGate && r.status === 'completed');

    if (!latestGateRun) {
      return { allowed: false, reason: 'No completed release gate run found' };
    }

    const passed = latestGateRun.verdict === 'pass';
    return {
      allowed: passed,
      runId: latestGateRun.id,
      version: latestGateRun.agentVersion,
      score: latestGateRun.score,
      verdict: latestGateRun.verdict,
      passThreshold: suite.passThreshold,
      reason: passed
        ? `Score ${latestGateRun.score} >= threshold ${suite.passThreshold}`
        : `Score ${latestGateRun.score} below threshold ${suite.passThreshold}`,
    };
  });

  // ── Agent Compliance Report ─────────────────────────────

  app.get('/agent-evals/agents/:agentId/report', async req => {
    const access = requireAdmin(req);
    const { agentId } = req.params as { agentId: string };

    const suites = await repo.listSuites(access.productId, { agentId, limit: 100 });
    const allRuns = await repo.listRunsByAgent(access.productId, agentId, 100);

    const completedRuns = allRuns.filter(r => r.status === 'completed');
    const passedRuns = completedRuns.filter(r => r.verdict === 'pass');
    const failedRuns = completedRuns.filter(r => r.verdict === 'fail');
    const reviewRequired = completedRuns.filter(r => r.verdict === 'needs_review');

    const scores = completedRuns.map(r => r.score).filter((s): s is number => s !== undefined);
    const avgScore =
      scores.length > 0
        ? Math.round((scores.reduce((a, b) => a + b, 0) / scores.length) * 1000) / 1000
        : 0;

    return {
      agentId,
      totalSuites: suites.length,
      activeSuites: suites.filter(s => s.status === 'active').length,
      totalRuns: allRuns.length,
      completedRuns: completedRuns.length,
      passed: passedRuns.length,
      failed: failedRuns.length,
      needsReview: reviewRequired.length,
      averageScore: avgScore,
      passRate:
        completedRuns.length > 0 ? Math.round((passedRuns.length / completedRuns.length) * 100) : 0,
      recentRuns: completedRuns.slice(0, 5).map(r => ({
        runId: r.id,
        suiteId: r.suiteId,
        version: r.agentVersion,
        score: r.score,
        verdict: r.verdict,
        date: r.completedAt ?? r.startedAt,
      })),
    };
  });

  // ── Schedule Eval Suite ─────────────────────────────────

  app.post('/agent-evals/suites/:id/schedule', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const body = req.body as { cronExpression?: string };

    if (!body.cronExpression) {
      validationError('cronExpression is required');
    }

    const suite = await repo.getSuite(id, access.productId);
    const { ensureJobDefinitions } = await import('../jobs/runner.js');
    const { registerJob } = await import('../jobs/registry.js');

    const jobName = `eval:${id}`;
    registerJob(jobName, async () => {
      // Create a new eval run (queued status, to be picked up by eval executor)
      const agent = await agentRepo.getAgent(suite.agentId, access.productId);
      const versionId = `${suite.agentId}:v${suite.targetVersion ?? agent.currentVersion}`;
      const version = await agentRepo.getAgentVersion(versionId, suite.agentId);
      const cases = await repo.listCases(id);

      await repo.createRun({
        id: `evalrun_${randomUUID()}`,
        suiteId: id,
        agentId: suite.agentId,
        productId: access.productId,
        agentVersionId: version.id,
        agentVersion: version.version,
        status: 'queued',
        passThreshold: suite.passThreshold,
        releaseGate: false,
        totalCases: cases.length,
        passedCases: 0,
        failedCases: 0,
        triggeredBy: 'scheduler',
        startedAt: new Date().toISOString(),
        reviewRequired: false,
      });

      return { success: true, message: `Eval run queued for suite ${id}` };
    });

    await ensureJobDefinitions([
      {
        name: jobName,
        cron: body.cronExpression,
        description: `Scheduled eval for suite ${suite.name}`,
      },
    ]);

    return { scheduled: true, jobName, cronExpression: body.cronExpression };
  });
}
