/**
 * Fastify preHandler that authenticates `/projects/*` and `/mcp` requests
 * against mcpd's McpToken introspection endpoint.
 *
 * Flow:
 *   1. Reject non-Bearer and non-`mcpctl_pat_` auth up front.
 *   2. Call `GET <mcpd>/api/v1/mcptokens/introspect` with the raw bearer.
 *   3. Cache the result (positive + negative TTLs) to avoid a round-trip per MCP call.
 *   4. Enforce `request.params.projectName === response.projectName`.
 *   5. Stash the principal on `request.mcpToken` for the audit collector.
 */
import type { FastifyRequest, FastifyReply } from 'fastify';
import { isMcpToken, hashToken } from '@mcpctl/shared';

export interface TokenAuthOptions {
  mcpdUrl: string;
  /** TTL for a successful introspection, ms. Default 30_000. */
  positiveTtlMs?: number;
  /** TTL for a failed introspection, ms. Default 5_000. */
  negativeTtlMs?: number;
  /** Injectable HTTP fetcher for tests. Defaults to `fetch`. */
  fetch?: (url: string, init?: RequestInit) => Promise<Response>;
}

export interface McpTokenPrincipal {
  tokenName: string;
  tokenSha: string;
  projectName: string;
}

declare module 'fastify' {
  interface FastifyRequest {
    /** Populated by the token-auth preHandler when the bearer was a McpToken. */
    mcpToken?: McpTokenPrincipal;
  }
}

interface IntrospectResponse {
  ok: boolean;
  tokenName?: string;
  tokenSha?: string;
  projectName?: string;
  revoked?: boolean;
  expired?: boolean;
  error?: string;
}

interface CacheEntry {
  result: IntrospectResponse;
  expiresAt: number;
}

export function createTokenAuthMiddleware(opts: TokenAuthOptions) {
  // Positive TTL must be tight enough that token revocation propagates
  // quickly. mcpd's introspection endpoint is a single DB lookup — the cache
  // only protects against burst restart storms, not steady-state load. A 30s
  // positive cache let revoked tokens keep working for the full window
  // (caught by mcptoken.smoke negative-cache-window assertion); 5s matches
  // negativeTtl and aligns with the test's `wait 7s after revoke` expectation.
  const positiveTtl = opts.positiveTtlMs ?? 5_000;
  const negativeTtl = opts.negativeTtlMs ?? 5_000;
  const fetchImpl = opts.fetch ?? (globalThis.fetch as typeof fetch);
  const cache = new Map<string, CacheEntry>();

  async function introspect(raw: string): Promise<IntrospectResponse> {
    const key = hashToken(raw);
    const now = Date.now();
    const hit = cache.get(key);
    if (hit && hit.expiresAt > now) return hit.result;

    try {
      const res = await fetchImpl(`${opts.mcpdUrl.replace(/\/$/, '')}/api/v1/mcptokens/introspect`, {
        method: 'GET',
        headers: { Authorization: `Bearer ${raw}` },
      });
      const body = (await res.json().catch(() => ({ ok: false, error: 'unreadable body' }))) as IntrospectResponse;
      const result: IntrospectResponse = res.ok ? body : { ...body, ok: false };
      cache.set(key, { result, expiresAt: now + (result.ok ? positiveTtl : negativeTtl) });
      return result;
    } catch (err) {
      const result: IntrospectResponse = { ok: false, error: err instanceof Error ? err.message : String(err) };
      cache.set(key, { result, expiresAt: now + negativeTtl });
      return result;
    }
  }

  return async function tokenAuth(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const header = request.headers.authorization;
    if (header === undefined || !header.startsWith('Bearer ')) {
      reply.code(401).send({ error: 'Missing Authorization bearer' });
      return;
    }
    const raw = header.slice(7);
    if (!isMcpToken(raw)) {
      reply.code(401).send({ error: 'Only mcpctl_pat_ bearers are accepted on this endpoint' });
      return;
    }

    const introspection = await introspect(raw);
    if (!introspection.ok) {
      reply.code(401).send({
        error: introspection.revoked ? 'Token revoked' : introspection.expired ? 'Token expired' : 'Invalid token',
      });
      return;
    }

    // Project-scope check: token.projectName must match the path param.
    const params = request.params as { projectName?: string } | undefined;
    if (params?.projectName !== undefined && params.projectName !== introspection.projectName) {
      reply.code(403).send({ error: `Token is not valid for project '${params.projectName}'` });
      return;
    }

    request.mcpToken = {
      tokenName: introspection.tokenName!,
      tokenSha: introspection.tokenSha!,
      projectName: introspection.projectName!,
    };
  };
}
