import { randomUUID } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import { BadRequestError, ForbiddenError } from '../../lib/errors.js';
import {
  BudgetAlertDoc,
  BudgetPolicyDoc,
  BudgetRolloverDoc,
  BudgetSpendEntryDoc,
  CostDashboardQuerySchema,
  CreateBudgetPolicySchema,
  ListBudgetAlertsQuerySchema,
  ListBudgetPoliciesQuerySchema,
  RecordBudgetSpendSchema,
  UpdateBudgetPolicySchema,
} 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 periodStart(now: Date, period: 'daily' | 'monthly'): string {
  if (period === 'daily') {
    return now.toISOString().slice(0, 10);
  }
  return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-01`;
}

function toIsoDayBoundary(day: string): string {
  return `${day}T00:00:00.000Z`;
}

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

export async function aiBudgetRoutes(app: FastifyInstance) {
  app.get('/ai-budgets/policies', async req => {
    const access = requireAdmin(req);
    const parsed = ListBudgetPoliciesQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }
    return repo.listPolicies(access.productId, parsed.data);
  });

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

    const now = new Date().toISOString();
    const doc: BudgetPolicyDoc = {
      id: `aibudget_${randomUUID()}`,
      productId: access.productId,
      scopeType: parsed.data.scopeType,
      scopeId: parsed.data.scopeId,
      name: parsed.data.name,
      status: 'active',
      period: parsed.data.period,
      budgetUsd: parsed.data.budgetUsd,
      softThreshold: parsed.data.softThreshold,
      hardThreshold: parsed.data.hardThreshold,
      modelAllowlist: parsed.data.modelAllowlist,
      metadata: parsed.data.metadata,
      createdBy: access.userId,
      createdAt: now,
      updatedAt: now,
    };
    return repo.createPolicy(doc);
  });

  app.get('/ai-budgets/policies/:id', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.getPolicy(id, access.productId);
  });

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

  app.get('/ai-budgets/policies/:id/status', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const policy = await repo.getPolicy(id, access.productId);
    const since = toIsoDayBoundary(periodStart(new Date(), policy.period));
    const entries = await repo.listSpendEntries(access.productId, {
      policyId: id,
      since,
    });
    const spendUsd = entries.reduce((sum, entry) => sum + entry.costUsd, 0);
    const tokensUsed = entries.reduce((sum, entry) => sum + entry.tokensUsed, 0);
    const percentUsed = policy.budgetUsd > 0 ? spendUsd / policy.budgetUsd : 0;

    return {
      policy,
      spendUsd,
      tokensUsed,
      percentUsed,
      verdict:
        percentUsed >= policy.hardThreshold
          ? 'block'
          : percentUsed >= policy.softThreshold
            ? 'warn'
            : 'allow',
      entries: entries.length,
    };
  });

  app.get('/ai-budgets/alerts', async req => {
    const access = requireAdmin(req);
    const parsed = ListBudgetAlertsQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }
    return repo.listAlerts(access.productId, parsed.data);
  });

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

    const candidatePolicies = parsed.data.policyId
      ? [await repo.getPolicy(parsed.data.policyId, access.productId)]
      : await repo.listPolicies(access.productId, {
          scopeType: parsed.data.scopeType,
          scopeId: parsed.data.scopeId,
          status: 'active',
          limit: 100,
        });

    const now = new Date();
    const alerts: BudgetAlertDoc[] = [];
    let overallVerdict: 'allow' | 'warn' | 'block' = 'allow';

    for (const policy of candidatePolicies) {
      if (policy.status !== 'active') continue;

      if (parsed.data.model && policy.modelAllowlist.length > 0) {
        const allowed = policy.modelAllowlist.includes(parsed.data.model);
        if (!allowed) {
          overallVerdict = 'block';
          alerts.push({
            id: `aialert_${randomUUID()}`,
            productId: access.productId,
            policyId: policy.id,
            scopeType: policy.scopeType,
            scopeId: policy.scopeId,
            severity: 'block',
            message: `Model '${parsed.data.model}' is not allowed by policy '${policy.name}'`,
            spendUsd: parsed.data.costUsd,
            budgetUsd: policy.budgetUsd,
            percentUsed: 0,
            createdAt: now.toISOString(),
          });
          continue;
        }
      }

      const since = toIsoDayBoundary(periodStart(now, policy.period));
      const entries = await repo.listSpendEntries(access.productId, {
        policyId: policy.id,
        since,
      });
      const spendUsd = entries.reduce((sum, entry) => sum + entry.costUsd, 0) + parsed.data.costUsd;
      const percentUsed = policy.budgetUsd > 0 ? spendUsd / policy.budgetUsd : 0;

      if (percentUsed >= policy.hardThreshold) {
        overallVerdict = 'block';
        alerts.push({
          id: `aialert_${randomUUID()}`,
          productId: access.productId,
          policyId: policy.id,
          scopeType: policy.scopeType,
          scopeId: policy.scopeId,
          severity: 'block',
          message: `AI budget hard limit exceeded for policy '${policy.name}'`,
          spendUsd,
          budgetUsd: policy.budgetUsd,
          percentUsed,
          createdAt: now.toISOString(),
        });
      } else if (percentUsed >= policy.softThreshold && overallVerdict !== 'block') {
        overallVerdict = 'warn';
        alerts.push({
          id: `aialert_${randomUUID()}`,
          productId: access.productId,
          policyId: policy.id,
          scopeType: policy.scopeType,
          scopeId: policy.scopeId,
          severity: 'warn',
          message: `AI budget nearing threshold for policy '${policy.name}'`,
          spendUsd,
          budgetUsd: policy.budgetUsd,
          percentUsed,
          createdAt: now.toISOString(),
        });
      }
    }

    const entry: BudgetSpendEntryDoc = {
      id: `aispend_${randomUUID()}`,
      productId: access.productId,
      policyId: candidatePolicies.length === 1 ? candidatePolicies[0].id : parsed.data.policyId,
      scopeType: parsed.data.scopeType,
      scopeId: parsed.data.scopeId,
      agentId: parsed.data.agentId,
      agentVersionId: parsed.data.agentVersionId,
      runId: parsed.data.runId,
      evaluationRunId: parsed.data.evaluationRunId,
      model: parsed.data.model,
      tokensUsed: parsed.data.tokensUsed,
      costUsd: parsed.data.costUsd,
      source: parsed.data.source,
      verdict: overallVerdict,
      recordedAt: now.toISOString(),
    };

    const saved = await repo.createSpendEntry(entry);
    if (alerts.length > 0) {
      await repo.createAlerts(alerts);
    }

    return {
      verdict: overallVerdict,
      entry: saved,
      alerts,
    };
  });

  // ── Cost Dashboard ──────────────────────────────────────

  app.get('/ai-budgets/costs', async req => {
    const access = requireAdmin(req);
    const parsed = CostDashboardQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      validationError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const entries = await repo.listAllSpendEntries(access.productId, {
      scopeType: parsed.data.scopeType,
      scopeId: parsed.data.scopeId,
      since: parsed.data.since,
      until: parsed.data.until,
      limit: parsed.data.limit,
    });

    const totalCostUsd = entries.reduce((sum, e) => sum + e.costUsd, 0);
    const totalTokens = entries.reduce((sum, e) => sum + e.tokensUsed, 0);

    // Group by requested dimension
    const groups = new Map<string, { costUsd: number; tokensUsed: number; count: number }>();
    for (const entry of entries) {
      let key: string;
      switch (parsed.data.groupBy) {
        case 'model':
          key = entry.model ?? 'unknown';
          break;
        case 'agent':
          key = entry.agentId ?? 'unknown';
          break;
        case 'day':
          key = entry.recordedAt.slice(0, 10);
          break;
        case 'scope':
          key = `${entry.scopeType}:${entry.scopeId}`;
          break;
        default:
          key = 'all';
      }
      const existing = groups.get(key) ?? { costUsd: 0, tokensUsed: 0, count: 0 };
      existing.costUsd += entry.costUsd;
      existing.tokensUsed += entry.tokensUsed;
      existing.count++;
      groups.set(key, existing);
    }

    const breakdown = Array.from(groups.entries())
      .map(([key, data]) => ({ key, ...data }))
      .sort((a, b) => b.costUsd - a.costUsd);

    return {
      totalCostUsd: Math.round(totalCostUsd * 10000) / 10000,
      totalTokens,
      entryCount: entries.length,
      groupBy: parsed.data.groupBy,
      breakdown,
    };
  });

  // ── Budget Rollover ─────────────────────────────────────

  app.post('/ai-budgets/policies/:id/rollover', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const policy = await repo.getPolicy(id, access.productId);

    const now = new Date();
    const currentPeriodStart = toIsoDayBoundary(periodStart(now, policy.period));

    // Calculate previous period
    const prev = new Date(now);
    if (policy.period === 'monthly') {
      prev.setUTCMonth(prev.getUTCMonth() - 1);
    } else {
      prev.setUTCDate(prev.getUTCDate() - 1);
    }
    const prevPeriodStart = toIsoDayBoundary(periodStart(prev, policy.period));

    // Get spend from previous period only (bounded to prev→current)
    const prevEntries = await repo.listSpendEntries(access.productId, {
      policyId: id,
      since: prevPeriodStart,
    });
    const prevPeriodEntries = prevEntries.filter(
      e => e.recordedAt >= prevPeriodStart && e.recordedAt < currentPeriodStart
    );
    const spentUsd = prevPeriodEntries.reduce((sum, e) => sum + e.costUsd, 0);
    const remainingUsd = Math.max(0, policy.budgetUsd - spentUsd);
    const rolledOverUsd = remainingUsd; // Full rollover — could add a cap later

    const rollover: BudgetRolloverDoc = {
      id: `airollover_${randomUUID()}`,
      productId: access.productId,
      policyId: id,
      fromPeriod: prevPeriodStart,
      toPeriod: currentPeriodStart,
      spentUsd,
      budgetUsd: policy.budgetUsd,
      remainingUsd,
      rolledOverUsd,
      createdAt: now.toISOString(),
    };

    const saved = await repo.createRollover(rollover);

    // Temporarily increase budget for current period
    await repo.updatePolicy(id, access.productId, {
      budgetUsd: policy.budgetUsd + rolledOverUsd,
      metadata: {
        ...(policy.metadata ?? {}),
        lastRollover: saved.id,
        originalBudgetUsd: policy.budgetUsd,
      },
    });

    return {
      rollover: saved,
      newBudgetUsd: policy.budgetUsd + rolledOverUsd,
    };
  });

  app.get('/ai-budgets/policies/:id/rollovers', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    return repo.listRollovers(access.productId, id);
  });

  // ── Budget Enforcement Check ────────────────────────────
  // Lightweight endpoint for pre-flight budget checks (no spend recorded)

  app.post('/ai-budgets/check', async req => {
    const access = requireAdmin(req);
    const body = req.body as {
      scopeType: string;
      scopeId: string;
      model?: string;
      estimatedCostUsd: number;
    };

    if (!body.scopeType || !body.scopeId || body.estimatedCostUsd === undefined) {
      validationError('scopeType, scopeId, and estimatedCostUsd are required');
    }

    const policies = await repo.listPolicies(access.productId, {
      scopeType: body.scopeType as 'product' | 'agent' | 'org' | 'workspace',
      scopeId: body.scopeId,
      status: 'active',
      limit: 100,
    });

    const now = new Date();
    let verdict: 'allow' | 'warn' | 'block' = 'allow';
    const reasons: string[] = [];

    for (const policy of policies) {
      // Model allowlist check
      if (body.model && policy.modelAllowlist.length > 0) {
        if (!policy.modelAllowlist.includes(body.model)) {
          verdict = 'block';
          reasons.push(`Model '${body.model}' not in allowlist for '${policy.name}'`);
          continue;
        }
      }

      const since = toIsoDayBoundary(periodStart(now, policy.period));
      const entries = await repo.listSpendEntries(access.productId, {
        policyId: policy.id,
        since,
      });
      const currentSpend = entries.reduce((sum, e) => sum + e.costUsd, 0);
      const projectedSpend = currentSpend + body.estimatedCostUsd;
      const percentUsed = policy.budgetUsd > 0 ? projectedSpend / policy.budgetUsd : 0;

      if (percentUsed >= policy.hardThreshold) {
        verdict = 'block';
        reasons.push(
          `Would exceed hard threshold (${Math.round(percentUsed * 100)}%) for '${policy.name}'`
        );
      } else if (percentUsed >= policy.softThreshold && verdict !== 'block') {
        verdict = 'warn';
        reasons.push(
          `Would exceed soft threshold (${Math.round(percentUsed * 100)}%) for '${policy.name}'`
        );
      }
    }

    return {
      verdict,
      reasons,
      policiesEvaluated: policies.length,
    };
  });
}
