// OpenAPI / Swagger wiring for the geocoder API.
//
// `registerDocs(app)` registers @fastify/swagger (which builds the spec from
// each route's `schema`) and @fastify/swagger-ui (which serves the interactive
// docs). The per-route schemas live in `routeSchemas` below so server.js can
// attach them when it declares the routes — that's what makes the generated
// spec accurate rather than empty.

import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(
  readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8"),
);

const AI_PROVIDERS = ["anthropic", "openai", "gemini", "ollama"];

// A gazetteer place row (the `match` and each `suggestions[]` entry).
const placeSchema = {
  type: "object",
  additionalProperties: true,
  properties: {
    name: { type: "string", description: "Place name with the LSAD suffix stripped (e.g. \"Austin\").", example: "Austin" },
    fullName: { type: "string", description: "Full Census name including suffix.", example: "Austin city" },
    stateCode: { type: "string", description: "USPS state code.", example: "TX" },
    cityKey: { type: "string", description: "Normalized lookup key for the bare name." },
    fullCityKey: { type: "string", description: "Normalized lookup key including suffix." },
    geoid: { type: "string", description: "Census GEOID.", example: "4805000" },
    lsad: { type: "string", description: "Census Legal/Statistical Area Description code.", example: "25" },
    lsadPriority: { type: "integer", description: "Tie-break priority (lower wins): city < town < CDP." },
    latitude: { type: "number", example: 30.307182 },
    longitude: { type: "number", example: -97.755996 },
  },
};

// A fuzzy/AI suggestion: a place plus match metadata.
const suggestionSchema = {
  type: "object",
  additionalProperties: true,
  properties: {
    name: { type: "string" },
    fullName: { type: "string" },
    stateCode: { type: "string" },
    geoid: { type: "string" },
    lsad: { type: "string" },
    latitude: { type: "number" },
    longitude: { type: "number" },
    distance: { type: ["integer", "null"], description: "Levenshtein edit distance from the input (null for AI-sourced suggestions)." },
    displayName: { type: "string", example: "Austin, TX" },
    source: { type: "string", enum: ["ai"], description: "Present only when the suggestion came from the AI fallback." },
    confidence: { type: "string", enum: ["high", "medium", "low"], description: "AI confidence (AI-sourced suggestions only)." },
  },
};

// The aiCorrection object. It is either an error stub or a provider result;
// modeled here as the union of all fields it may carry.
const aiCorrectionSchema = {
  type: ["object", "null"],
  additionalProperties: true,
  description: "Populated only when `useAi=true` and exact+fuzzy both miss. Either an error stub `{ error }` or the provider result below.",
  properties: {
    error: { type: "string", description: "Present when the AI call could not run or failed.", example: "No AI provider configured" },
    provider: { type: "string", enum: AI_PROVIDERS, example: "anthropic" },
    model: { type: "string", example: "claude-haiku-4-5-20251001" },
    correctedCityName: { type: ["string", "null"], description: "A candidate name copied verbatim, or null if nothing matched." },
    confidence: { type: "string", enum: ["high", "medium", "low"] },
    reasoning: { type: "string", description: "Short model explanation." },
    candidateCount: { type: "integer", description: "Number of candidate place names sent to the model." },
    elapsedMs: { type: "integer", description: "AI round-trip time in milliseconds." },
    usage: {
      type: "object",
      properties: {
        input_tokens: { type: "integer" },
        output_tokens: { type: "integer" },
        cache_creation_input_tokens: { type: "integer" },
        cache_read_input_tokens: { type: "integer" },
      },
    },
    costUsd: { type: "number", description: "Estimated cost of this call in USD." },
    pricingPerMTok: {
      type: "object",
      description: "Per-million-token rates `costUsd` was computed against.",
      properties: {
        input: { type: "number" },
        output: { type: "number" },
        cacheWrite: { type: "number" },
        cacheRead: { type: "number" },
      },
    },
  },
};

// AI info block reused by /api/health.
const aiInfoSchema = {
  type: "object",
  additionalProperties: true,
  properties: {
    available: { type: "boolean", description: "Whether an AI provider is configured and usable." },
    provider: { type: "string", enum: AI_PROVIDERS, description: "Active provider (present when available)." },
    model: { type: "string", description: "Active model id (present when available)." },
    requested: { type: ["string", "null"], description: "Value of AI_PROVIDER, if set." },
    reason: { type: "string", description: "Why AI is unavailable (present when not available)." },
    configured: {
      type: "array",
      description: "Every provider whose key/config is present.",
      items: {
        type: "object",
        properties: {
          name: { type: "string", enum: AI_PROVIDERS },
          model: { type: "string" },
        },
      },
    },
  },
};

const errorSchema = {
  type: "object",
  properties: { error: { type: "string" } },
  required: ["error"],
};

export const routeSchemas = {
  health: {
    summary: "Service health and AI provider status",
    description: "Liveness probe plus the active/configured AI provider info.",
    tags: ["meta"],
    response: {
      200: {
        type: "object",
        additionalProperties: true,
        properties: {
          ok: { type: "boolean", example: true },
          service: { type: "string", example: "united-states-geocoder" },
          dataset: { type: "string", example: "US Census 2024 Gazetteer Places" },
          ai: aiInfoSchema,
        },
        required: ["ok", "service", "dataset", "ai"],
      },
    },
  },

  geocode: {
    summary: "Geocode a US city + state to lat/lng",
    description:
      "Resolves a free-form city/town name within a state to coordinates. " +
      "Tries an exact normalized lookup, then Levenshtein fuzzy suggestions, " +
      "then (optionally) an AI fallback for typos and aliases. Only " +
      "high-confidence AI matches auto-resolve; others come back in `suggestions`.",
    tags: ["geocode"],
    querystring: {
      type: "object",
      required: ["city", "state"],
      properties: {
        city: { type: "string", description: "Free-form city/town name.", example: "Austin" },
        state: { type: "string", description: "USPS code (`TX`) or full name (`Texas`).", example: "TX" },
        useAi: {
          type: "string",
          description: "Enable the AI fallback on a miss. Truthy values: 1, true, yes, on.",
          example: "false",
        },
        provider: {
          type: "string",
          enum: AI_PROVIDERS,
          description: "Force a specific AI provider for this call (must be configured).",
        },
      },
    },
    response: {
      200: {
        type: "object",
        additionalProperties: true,
        properties: {
          provider: { type: "string", example: "us-census-gazetteer" },
          found: { type: "boolean" },
          matchMethod: { type: ["string", "null"], enum: ["exact", "ai", null], description: "How the match was resolved, or null on a miss." },
          elapsedMs: { type: "integer" },
          city: { type: "string", description: "Echo of the input city.", example: "Austin" },
          state: { type: "string", description: "Normalized state code.", example: "TX" },
          latitude: { type: ["number", "null"] },
          longitude: { type: ["number", "null"] },
          displayName: { type: "string", example: "Austin, TX" },
          match: { ...placeSchema, type: ["object", "null"], description: "The resolved place, or null on a miss." },
          suggestions: { type: "array", items: suggestionSchema },
          aiCorrection: aiCorrectionSchema,
        },
        required: ["provider", "found", "elapsedMs", "city", "state", "suggestions"],
      },
      400: errorSchema,
      500: errorSchema,
    },
  },
};

export async function registerDocs(app) {
  await app.register(swagger, {
    openapi: {
      info: {
        title: "United States Geocoder API",
        description: pkg.description,
        version: pkg.version,
        license: { name: "MIT" },
      },
      tags: [
        { name: "geocode", description: "City/state → lat/lng lookups" },
        { name: "meta", description: "Health and service metadata" },
      ],
    },
  });

  await app.register(swaggerUi, {
    routePrefix: "/docs",
    uiConfig: { docExpansion: "list", deepLinking: true },
  });
}
