/**
 * types.ts — shared type definitions for the defineApi route adapter.
 *
 * Spec 01 — defineApi route adapter.
 *
 * Design notes:
 * - ApiSpec is a plain interface with optional fields so that Spec 03 can
 *   add `services?: ServiceKey[]` without a breaking change.
 * - `auth` carries an optional `message` override so that pilot handlers
 *   with custom error phrasing preserve byte-identical responses.
 * - `db` defaults to 'required' (most routes need DATABASE_URL); pass
 *   `db: 'none'` for routes that don't touch the DB.
 * - `body` is only meaningful for non-GET methods; adapter skips parsing for GET.
 */

import type { z } from 'zod';
import type { APIRoute } from 'astro';
import { type RateLimitScope } from '@/server/security/rate-limit-policies.js';
import type { Services, ServiceKey } from '@/server/services/types.js';

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export type ApiQueryValue = string | string[];
export type ApiQuery = Record<string, ApiQueryValue | undefined>;
export interface ApiSchemas<TBody, TParams, TQuery> {
  body?: z.ZodType<TBody>;
  params?: z.ZodType<TParams>;
  query?: z.ZodType<TQuery>;
}

/**
 * Declarative spec passed to defineApi.
 *
 * TBody     — validated body type (inferred from `body` schema).
 * TData     — success payload type (returned by handler as `ok: true, data: TData`).
 * TErrCode  — union of handler-specific error code strings.
 * TServices — tuple of ServiceKey literals; drives ctx.services typed projection.
 */
export interface ApiSpec<
  TBody,
  _TData,
  _TErrCode extends string,
  TServices extends readonly ServiceKey[] = readonly ServiceKey[],
  TParams = Record<string, string | undefined>,
  TQuery = ApiQuery,
> {
  /** HTTP method this route accepts. */
  method: HttpMethod;

  /**
   * Authentication requirement.
   * - 'required': 401 AUTH_REQUIRED when no session. `ctx.user` is narrowed to non-null.
   * - 'optional': session loaded if present; `ctx.user` may be null.
   * - 'public': no session needed; `ctx.user` is always null.
   *
   * `message` allows per-handler override of the 401 error message to preserve
   * byte-identical response bodies during incremental migration. Once all handlers
   * are canonicalized, `message` can be dropped.
   */
  auth:
    | 'required'
    | 'optional'
    | 'public'
    | { mode: 'required'; message?: string }
    | { mode: 'optional' }
    | { mode: 'public' };

  /** Planned schema contract for request validation. */
  schemas?: ApiSchemas<TBody, TParams, TQuery>;

  /** @deprecated Prefer `schemas.body`. */
  body?: z.ZodType<TBody>;

  /** @deprecated Prefer `schemas.params`. */
  params?: z.ZodType<TParams>;

  /** @deprecated Prefer `schemas.query`. */
  query?: z.ZodType<TQuery>;

  /** Rate-limit policy to apply before handler invocation. */
  rateLimit?: { scope: RateLimitScope };

  /** Fail-closed route guard evaluated before rate limiting and body consumption. */
  preflight?: (request: Request) => Response | null | Promise<Response | null>;

  /**
   * Sentry observability scope. Defaults to 'pages.api.unknown'.
   * Handlers should pass an explicit value: 'pages.api.<route-name>'.
   */
  scope?: string;

  /**
   * Database availability requirement.
   * - 'required' (default): 503 CONFIG_ERROR when DATABASE_URL is absent.
   * - 'none': skip the DATABASE_URL check (health routes, vapid-key, etc.).
   */
  db?: 'required' | 'none';

  /**
   * Maximum request body size in bytes. Defaults to 1MB (1_048_576).
   * Requests with Content-Length exceeding this return 413 BODY_TOO_LARGE.
   */
  maxBodyBytes?: number;

  /**
   * HTTP status code for VALIDATION_ERROR responses.
   * Defaults to 422 (standard). Use 400 to match legacy handler behavior
   * during incremental migration.
   */
  validationErrorStatus?: 400 | 422;

  /**
   * Service keys this route needs. Spec 03 — request-scoped service bundle.
   *
   * When declared, `ctx.services` is typed as `Pick<Services, TServices[number]>`.
   * When omitted, `ctx.services` is the full Services bundle (unnarrowed).
   *
   * Handler receives no-op stubs for capability-disabled services (push/email/stripe)
   * rather than a 503 — the handler decides whether to degrade gracefully.
   */
  services?: TServices;

  /**
   * Optional domain-error mapper. Called on any unhandled throw from the handler
   * before the adapter falls through to the generic 500 INTERNAL_ERROR.
   *
   * Return a non-null ApiResult to claim the error and have it serialized via the
   * same translate path as a handler-returned result (statusFromCode still applies
   * when result.status is omitted). Return null to leave the error unclaimed —
   * the adapter then captures it with Sentry and returns 500 INTERNAL_ERROR.
   *
   * Spec 01 — Wave 1. Spec 02 will provide a shared `errorToApiResult` helper.
   */
  errorMap?: (err: unknown) => ApiResult<_TData, _TErrCode> | null;

  /**
   * CSRF enforcement mode.
   *
   * - undefined (default): global csrfMiddleware handles CSRF before the route runs.
   * - 'after-body': route's prefix is opted out of csrfMiddleware; defineApi enforces
   *   CSRF here, after body parse + Zod validation (step 6.5). This ensures malformed
   *   bodies return 400 INVALID_BODY / VALIDATION_ERROR rather than 403 CSRF_INVALID.
   *
   * When using 'after-body', the route prefix MUST also be added to
   * CSRF_OPT_OUT_PREFIXES in src/server/middleware/csrf.ts.
   */
  csrf?: 'after-body';
}

/**
 * Handler context passed to the business logic function.
 *
 * When auth.mode is 'required', `user` is guaranteed non-null at the type level.
 * When 'optional' or 'public', user may be null.
 *
 * `TServices` drives the typed projection on `ctx.services`:
 *   - When the spec declares `services: ['db', 'push'] as const`, TServices is
 *     readonly ['db', 'push'] and ctx.services is Pick<Services, 'db' | 'push'>.
 *   - When omitted, ctx.services is the full Services bundle.
 */
export interface ApiHandlerCtx<
  TBody,
  TServices extends readonly ServiceKey[] = readonly ServiceKey[],
  TParams = Record<string, string | undefined>,
  TQuery = ApiQuery,
> {
  /** Authenticated user — non-null only when auth is 'required'. */
  user: App.UserRow | null;
  /** Parsed and validated request body (undefined when no body schema). */
  body: TBody;
  /** Raw Astro request. */
  request: Request;
  /** Astro locals. */
  locals: App.Locals;
  /** Route params (e.g. { id: '...' } for [id] routes). */
  params: TParams;
  /** Parsed query params. Repeated keys are exposed as string arrays. */
  query: TQuery;
  /**
   * Typed service projection. Only keys declared in `spec.services` are present
   * at the type level; other keys exist at runtime on the full bundle.
   * Spec 03 — request-scoped service bundle.
   */
  services: Pick<Services, TServices[number]>;
}

/**
 * Result type returned by handler functions.
 *
 * ok: true  — `data` is serialized into the response. `status` defaults to 200.
 *             `headers` accepts a `Headers` instance for multi-value headers
 *             (e.g. several `Set-Cookie` entries) that a plain object can't hold.
 * ok: false — `error` + `code` form the error envelope. `status` defaults to
 *             the built-in statusFromCode map, falling back to 400.
 * raw       — escape hatch for responses that can't fit the JSON envelope at all
 *             (WebSocket upgrades, redirects, HTML/CSV bodies, proxied upstream
 *             responses). The adapter returns `raw` unchanged, skipping translation.
 */
export type ApiResult<TData, TErrCode extends string> =
  | { ok: true; data?: TData; status?: number; headers?: Record<string, string> | Headers }
  | { ok: false; code: TErrCode | BuiltInErrorCode; error: string; status?: number }
  | { raw: Response };

/**
 * Built-in error codes emitted by the adapter (not the handler).
 * Handlers may also return these codes if they need to signal the same errors.
 */
export type BuiltInErrorCode =
  | 'AUTH_REQUIRED'
  | 'VALIDATION_ERROR'
  | 'INVALID_BODY'
  | 'BODY_TOO_LARGE'
  | 'CSRF_INVALID'
  | 'CONFIG_ERROR'
  | 'INTERNAL_ERROR';

/** Handler function type. */
export type ApiHandler<
  TBody,
  TData,
  TErrCode extends string,
  TServices extends readonly ServiceKey[] = readonly ServiceKey[],
  TParams = Record<string, string | undefined>,
  TQuery = ApiQuery,
> = (ctx: ApiHandlerCtx<TBody, TServices, TParams, TQuery>) => Promise<ApiResult<TData, TErrCode>>;

/** The defineApi function signature. */
export type DefineApiFn = <
  TBody = unknown,
  TData = unknown,
  TErrCode extends string = never,
  TServices extends readonly ServiceKey[] = readonly ServiceKey[],
  TParams = Record<string, string | undefined>,
  TQuery = ApiQuery,
>(
  spec: ApiSpec<TBody, TData, TErrCode, TServices, TParams, TQuery>,
  handler: ApiHandler<TBody, TData, TErrCode, TServices, TParams, TQuery>,
) => APIRoute;
