export const dynamic = 'force-dynamic';
export const revalidate = 0;

import { NextResponse } from 'next/server';
import { getOrgId } from '../../lib/org';
import { validateGuardInput } from '../../lib/validate';
import { evaluateGuard } from '../../lib/guard';
import { getSql } from '../../lib/db.js';
import { apiErrorResponse } from '../../lib/apiErrors.js';
import { scanForPromptInjection } from '../../lib/promptInjection.js';
import { listGuardDecisions } from '../../lib/repositories/guard.repository.js';
import { isSelfHostModeEnabled } from '../../lib/selfHost.js';
import { verifyJwt, extractBearerToken } from '../../lib/jwks-verifier.js';
import { checkAndRecord as checkAndRecordJti } from '../../lib/repositories/jti-replay.repository.js';
import { resolveActStatus } from '../../lib/act-binding.js';

/**
 * POST /api/guard — Evaluate guard policies for a proposed action.
 * Returns allow/warn/block/require_approval.
 *
 * Body: { action_type, risk_score?, agent_id?, agent_name?, systems_touched?, reversible?, declared_goal? }
 * Query: ?include_signals=true (optional, adds live signal warnings)
 *
 * Agent identity — two tiers:
 *
 *   Phase 1 (trust-on-assertion): Pass agent_id / agent_name in the request
 *   body. The API-key boundary provides authentication. verification_status
 *   will be 'unverified'.
 *
 *   Phase 2 (JWKS verification): Attach `Authorization: Bearer <JWT>` to the
 *   request. DashClaw verifies the token against the issuer's JWKS endpoint
 *   (fetched from {iss}/.well-known/jwks.json, cached 1 h). On success:
 *     - verification_status → 'verified'
 *     - agent_id is set to the JWT `sub` claim (body value overridden for
 *       integrity: cryptographic proof beats self-assertion)
 *     - agent_name is set to the JWT `agent_name` claim (if present)
 *   On issuer outage the verifier fails-soft to 'unverified' so a downed
 *   identity provider cannot block agent decisions.
 *
 *   verification_status enum: verified | unverified | expired | failed | unknown_issuer | exp_too_far
 *
 * Config (env vars, no YAML needed):
 *   DASHCLAW_ALLOWED_ISSUER  — restrict which JWT issuers are trusted
 *   DASHCLAW_JWT_AUDIENCE    — require this value in the `aud` claim
 *
 * Provider-agnostic: works with any OIDC issuer (Keycloak, Auth0, AgentLair,
 * etc.) — see docs/agent-identity.md for setup examples.
 */
export async function POST(request) {
  try {
    const orgId = getOrgId(request);
    const body = await request.json();

    const { valid, data, errors } = validateGuardInput(body);

    if (!valid) {
      return NextResponse.json({ error: 'Validation failed', details: errors }, { status: 400 });
    }

    // SECURITY: Block prompt injection patterns in declared_goal (per D-04)
    const goalText = data.declared_goal || '';
    if (goalText) {
      const injectionScan = scanForPromptInjection(goalText);
      if (injectionScan.recommendation === 'block') {
        return NextResponse.json({
          error: 'Input rejected: prompt injection pattern detected',
          risk_level: injectionScan.risk_level,
          categories: injectionScan.categories,
        }, { status: 400 });
      }
    }

    // Hoist sql once — used by the replay check and evaluateGuard. getSql()
    // returns a cached singleton so calling it twice is harmless, but a
    // single binding makes the dependency clear and avoids subtle risk if
    // getSql() ever stops being idempotent (e.g., during hot reload).
    const sql = getSql();

    // Phase 2: JWKS verification — resolve agent identity from JWT bearer token.
    // Fail-soft: infrastructure errors fall back to 'unverified', never 'failed'.
    const authHeader = request.headers.get('authorization');
    const bearerToken = extractBearerToken(authHeader);

    if (bearerToken) {
      const verificationResult = await verifyJwt(bearerToken);

      if (verificationResult.verification_status === 'verified') {
        // Cryptographic proof beats self-assertion: JWT sub overrides body agent_id.
        if (verificationResult.agent_id) {
          if (data.agent_id && data.agent_id !== verificationResult.agent_id) {
            console.warn(
              `[Guard] JWT sub (${verificationResult.agent_id}) overrides body agent_id (${data.agent_id})`
            );
          }
          data.agent_id = verificationResult.agent_id;
        }
        if (verificationResult.agent_name && !data.agent_name) {
          data.agent_name = verificationResult.agent_name;
        }
      }

      data.verification_status = verificationResult.verification_status;
      data.jti = verificationResult.jti || null;

      // Phase 2b: replay-protection check (issue #120, design by @piiiico).
      // Only verified tokens hit the store — there's no signature trust to
      // replay without that. The exp_too_far signal flows through verification
      // status directly (the verifier sets it before any network call).
      const replayProtection = (process.env.DASHCLAW_JTI_REPLAY_PROTECTION || 'best_effort').toLowerCase();
      if (verificationResult.verification_status === 'exp_too_far') {
        data.replay_status = 'exp_too_far';
      } else if (verificationResult.verification_status === 'verified' && replayProtection === 'off') {
        // Distinct from `not_applicable` so the audit trail can tell apart
        // "Phase 1 path / no JWT" from "verified JWT but operator opted out
        // of replay protection." Same allow-everything outcome, different
        // forensic story during incident review.
        data.replay_status = 'disabled';
      } else if (verificationResult.verification_status === 'verified') {
        // Length cap matches the repository's MAX_JTI_LENGTH (1024). Catching
        // it here too means a hostile-IdP-issued multi-MB jti never reaches
        // the store at all and never throws OVERSIZED_JTI. Boundary
        // validation > deep validation.
        const oversizedJti = typeof verificationResult.jti === 'string' && verificationResult.jti.length > 1024;
        if (!verificationResult.jti) {
          data.replay_status = 'not_present';
        } else if (oversizedJti) {
          console.warn('[Guard] Oversized jti rejected from replay store', {
            jti_length: verificationResult.jti.length,
            issuer: verificationResult.issuer,
          });
          data.replay_status = 'not_present';
        } else if (typeof verificationResult.exp !== 'number') {
          // jti without exp can't be safely TTL'd → treat as not_present so
          // the store never accumulates rows with no purge horizon.
          data.replay_status = 'not_present';
        } else if (!verificationResult.issuer) {
          // Defense in depth: the verifier currently sets verification_status
          // to 'failed' when issuer is null, so we should never reach here
          // with a 'verified' status and null issuer. If a future code path
          // ever does, treat as not_present rather than throwing INVALID_INPUT
          // out of the repository (which would surface as an unhandled 500).
          data.replay_status = 'not_present';
        } else {
          data.replay_status = await checkAndRecordJti(sql, {
            jti: verificationResult.jti,
            issuer: verificationResult.issuer,
            expiresAt: verificationResult.exp,
            agentId: verificationResult.agent_id,
          });
        }
      } else {
        data.replay_status = 'not_applicable';
      }

      // Phase 2c: action-binding status (issue #121). Its own axis, like
      // replay_status — never overloads verification_status. Computed for
      // verified tokens in EVERY mode (off included): even an operator running
      // DASHCLAW_ACT_BINDING=off gets the `match` signal that tells them their
      // issuer started minting bindings and it's safe to flip to required.
      // resolveActStatus returns 'not_applicable' for any non-verified token,
      // and hashes the raw request context (pre-redaction) so legitimate
      // matches whose goal contains a redactable pattern still compare.
      data.act_status = resolveActStatus(verificationResult, data);
      data.act_hash = verificationResult.act?.hash || null;
    } else {
      data.verification_status = 'unverified';
      data.replay_status = 'not_applicable';
      data.jti = null;
      data.act_status = 'not_applicable';
      data.act_hash = null;
    }

    const includeSignals = request.nextUrl.searchParams.get('include_signals') === 'true';

    let computeSignalsFn = null;
    if (includeSignals) {
      const { computeSignals } = await import('../../lib/signals');
      computeSignalsFn = computeSignals;
    }

    const result = await evaluateGuard(orgId, data, sql, {
      includeSignals,
      computeSignals: computeSignalsFn,
    });

    return NextResponse.json(result, { status: 200 });
  } catch (err) {
    return apiErrorResponse(err, 'GUARD POST');
  }
}

/**
 * GET /api/guard — List recent guard decisions.
 *
 * Query: ?agent_id=X&decision=block&limit=20&offset=0
 */
export async function GET(request) {
  try {
    const orgId = getOrgId(request);

    // Self-host bypass: if no org is configured yet, return empty results gracefully.
    if (isSelfHostModeEnabled() && orgId === 'org_default') {
      const sql = getSql();
      const { searchParams } = request.nextUrl;
      const agentId = searchParams.get('agent_id') || undefined;
      const decision = searchParams.get('decision') || undefined;
      const limit = Math.min(parseInt(searchParams.get('limit') || '20', 10), 1000);
      const offset = parseInt(searchParams.get('offset') || '0', 10);

      const result = await listGuardDecisions(sql, orgId, { agentId, decision, limit, offset });
      return NextResponse.json({ ...result, limit, offset });
    }

    const sql = getSql();
    const { searchParams } = request.nextUrl;
    const agentId = searchParams.get('agent_id') || undefined;
    const decision = searchParams.get('decision') || undefined;
    const limit = Math.min(parseInt(searchParams.get('limit') || '20', 10), 1000);
    const offset = parseInt(searchParams.get('offset') || '0', 10);

    const result = await listGuardDecisions(sql, orgId, { agentId, decision, limit, offset });
    return NextResponse.json({ ...result, limit, offset });
  } catch (err) {
    return apiErrorResponse(err, 'GUARD GET');
  }
}
