/**
 * Site Finder API hooks — TanStack Query wrappers.
 *
 * Mock fallback strategy (from mock-toggle.ts):
 *  MOCK_PARCELS_BBOX   — uses parcels-bbox.json fixture (B1 not yet in prod)
 *  MOCK_RECENT_PARCELS — uses localStorage only (B2 stub not yet in prod)
 *  MOCK_ANALYZE        — uses parcel-analyze.json fixture (B5)
 *  MOCK_POI_SCORE      — uses poi-score.json fixture (B6)
 */

import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { HTTPError, apiFetch, apiFetchWithStatus } from "@/lib/api";
import { abortableSleep } from "@/lib/abortableSleep";
import type {
  AnalyzeAcceptedResponse,
  FetchStatusResponse,
} from "@/hooks/useSiteAnalysis";
import {
  MOCK_PARCELS_BBOX,
  MOCK_RECENT_PARCELS,
  MOCK_ANALYZE,
  MOCK_POI_SCORE,
} from "@/lib/mock-toggle";
import fixtureParcels from "@/lib/mocks/parcels-bbox.json";
import fixtureAnalyze from "@/lib/mocks/parcel-analyze.json";
import fixturePoiScore from "@/lib/mocks/poi-score.json";
import fixtureForecast from "@/lib/mocks/parcel-forecast.json";
import type { ForecastEnvelope } from "@/types/forecast";
import type {
  ParcelAnalysisCompetitor,
  Pipeline24mo,
} from "@/types/site-finder";
import type {
  NspdZoning,
  NspdZouitOverlap,
  OpportunityParcel,
  RedLine,
  RiskZone,
} from "@/types/nspd";

// ── Types ─────────────────────────────────────────────────────────────────────

export type ParcelStatus = "free" | "in_progress" | "favorite";

export type ParcelVri =
  "multistory" | "mixed" | "individual" | "office" | "other";

export interface ParcelBboxItem {
  cad_num: string;
  address: string;
  // #1421: nullable — the B1 by-bbox payload may omit area_m2; null означает
  // «н/д», в отличие от реального 0 га. Потребители обязаны гейтить null.
  area_ha: number | null;
  status: ParcelStatus;
  district: string;
  vri: ParcelVri;
  lat: number;
  lon: number;
}

export interface ParcelBboxFilters {
  min_area?: number;
  max_area?: number;
  status?: ParcelStatus;
  district?: string;
  vri?: ParcelVri;
}

export interface BboxCoords {
  minLat: number;
  minLon: number;
  maxLat: number;
  maxLon: number;
}

export interface RecentParcel {
  cad_num: string;
  address: string;
  // #1421: nullable — площадь может быть неизвестна; null → «н/д», не «0 га».
  area_ha: number | null;
  district: string;
  visited_at: string; // ISO string
}

// ── Constants ─────────────────────────────────────────────────────────────────

const LOCAL_STORAGE_KEY = "gd_recent_parcels";
const MAX_RECENT = 10;

// ── localStorage helpers ──────────────────────────────────────────────────────

export function getLocalRecentParcels(): RecentParcel[] {
  if (typeof window === "undefined") return [];
  try {
    const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
    if (!raw) return [];
    const parsed: unknown = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed as RecentParcel[];
  } catch {
    return [];
  }
}

export function addLocalRecentParcel(parcel: RecentParcel): void {
  if (typeof window === "undefined") return;
  try {
    const existing = getLocalRecentParcels();
    const filtered = existing.filter((p) => p.cad_num !== parcel.cad_num);
    const updated = [parcel, ...filtered].slice(0, MAX_RECENT);
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updated));
  } catch {
    // ignore localStorage errors
  }
}

// ── Hook: useParcelsBboxQuery ─────────────────────────────────────────────────

function applyFilters(
  parcels: ParcelBboxItem[],
  filters: ParcelBboxFilters,
): ParcelBboxItem[] {
  return parcels.filter((p) => {
    // #1421: area_ha теперь nullable («н/д»). Площадной фильтр применяем только
    // к участкам с известной площадью; null проходит мимо min/max гейтов.
    if (
      filters.min_area != null &&
      p.area_ha != null &&
      p.area_ha < filters.min_area
    )
      return false;
    if (
      filters.max_area != null &&
      p.area_ha != null &&
      p.area_ha > filters.max_area
    )
      return false;
    if (filters.status != null && p.status !== filters.status) return false;
    if (
      filters.district != null &&
      filters.district !== "" &&
      p.district !== filters.district
    )
      return false;
    if (filters.vri != null && filters.vri !== "other" && p.vri !== filters.vri)
      return false;
    return true;
  });
}

function filterByBbox(
  parcels: ParcelBboxItem[],
  bbox: BboxCoords,
): ParcelBboxItem[] {
  return parcels.filter(
    (p) =>
      p.lat >= bbox.minLat &&
      p.lat <= bbox.maxLat &&
      p.lon >= bbox.minLon &&
      p.lon <= bbox.maxLon,
  );
}

export function useParcelsBboxQuery(
  bbox: BboxCoords | null,
  filters: ParcelBboxFilters = {},
) {
  return useQuery({
    queryKey: [
      "parcels-bbox",
      bbox?.minLat,
      bbox?.minLon,
      bbox?.maxLat,
      bbox?.maxLon,
      filters.min_area,
      filters.max_area,
      filters.status,
      filters.district,
      filters.vri,
    ],
    queryFn: async (): Promise<ParcelBboxItem[]> => {
      if (MOCK_PARCELS_BBOX) {
        // Fixture: filter by bbox + filters client-side
        const typed = fixtureParcels as ParcelBboxItem[];
        const inBbox = bbox ? filterByBbox(typed, bbox) : typed;
        return applyFilters(inBbox, filters);
      }

      if (!bbox) return [];

      const params = new URLSearchParams({
        min_lat: String(bbox.minLat),
        min_lon: String(bbox.minLon),
        max_lat: String(bbox.maxLat),
        max_lon: String(bbox.maxLon),
      });
      if (filters.min_area != null)
        params.set("min_area", String(filters.min_area));
      if (filters.max_area != null)
        params.set("max_area", String(filters.max_area));
      if (filters.status) params.set("status", filters.status);
      if (filters.district) params.set("district", filters.district);
      if (filters.vri) params.set("vri", filters.vri);

      // Backend returns { parcels, count, limit, bbox_area_km2 } with field
      // names centroid_lat/centroid_lon/area_m2 and nullable status. Adapt to
      // the flat ParcelBboxItem[] shape the map components consume. district /
      // vri / address are not in B1 yet — defaulted until enrichment lands.
      const raw = await apiFetch<{
        parcels: Array<{
          cad_num: string;
          centroid_lat: number;
          centroid_lon: number;
          area_m2: number | null;
          land_category: string | null;
          status: ParcelStatus | null;
        }>;
      }>(`/api/v1/parcels/by-bbox?${params.toString()}`);

      const adapted: ParcelBboxItem[] = raw.parcels.map((p) => ({
        cad_num: p.cad_num,
        lat: p.centroid_lat,
        lon: p.centroid_lon,
        // #1421: keep null when area_m2 is missing — не приземляем в 0, иначе
        // карточка показывает «0 га» вместо «н/д».
        area_ha: p.area_m2 != null ? p.area_m2 / 10000 : null,
        status: p.status ?? "free",
        district: "—",
        vri: "other",
        address: "",
      }));
      // Apply filters client-side until backend supports them (B1 follow-up).
      // district/vri are NOT in the B1 by-bbox payload — every adapted parcel
      // carries the placeholders district:"—" / vri:"other". Filtering on those
      // client-side would wipe the whole map the moment the user picks any real
      // district or VRI (none of the UI values equal "—"/"other"), so omit those
      // two filter keys here. Площадь/статус — реальные данные, фильтруем их.
      const areaStatusFilters: ParcelBboxFilters = {
        min_area: filters.min_area,
        max_area: filters.max_area,
        status: filters.status,
      };
      return applyFilters(adapted, areaStatusFilters);
    },
    enabled: MOCK_PARCELS_BBOX || bbox != null,
    staleTime: 30_000,
    // #1419: bbox is part of the queryKey, so every pan/zoom is a fresh cache
    // entry and `data` would blank to `undefined` mid-refetch → KPI flickers to
    // «0 / 0». Hold the previous parcels on screen while the new bbox refetches.
    placeholderData: keepPreviousData,
  });
}

// ── Hook: useRecentParcels ────────────────────────────────────────────────────

interface RecentParcelsResponse {
  parcels: RecentParcel[];
}

export function useRecentParcels() {
  return useQuery({
    queryKey: ["recent-parcels"],
    queryFn: async (): Promise<RecentParcel[]> => {
      const localItems = getLocalRecentParcels();

      if (MOCK_RECENT_PARCELS) {
        return localItems;
      }

      try {
        const serverData = await apiFetch<RecentParcelsResponse>(
          "/api/v1/users/me/recent-parcels",
        );
        // Merge: server list + local items not already in server list
        const serverCads = new Set(serverData.parcels.map((p) => p.cad_num));
        const localOnly = localItems.filter((p) => !serverCads.has(p.cad_num));
        return [...serverData.parcels, ...localOnly].slice(0, MAX_RECENT);
      } catch {
        // B2 stub may not be deployed yet — fall back to localStorage
        return localItems;
      }
    },
    staleTime: 60_000,
  });
}

// ── Types: B5 extended analyze response ──────────────────────────────────────

export interface ParcelEgrn {
  cad_num: string;
  address: string;
  area_m2: number;
  vri: string;
  category: string;
  registration_date: string | null;
  owner_type: string;
  encumbrance: string;
  status: string;
  last_updated: string | null;
}

// ── Encumbrance (#1954) ──────────────────────────────────────────────────────

/**
 * Обременения участка — ЗОУИТ-сводка из `encumbrance_block` бэкенда
 * (`parcels.py`, сериализуется top-level как `encumbrance`). Пересечение геома
 * участка с `cad_zouit`.
 */
export interface ParcelEncumbrance {
  has_zouit: boolean;
  zouit_count: number;
  zouit_types: string[];
}

/**
 * Человекочитаемая строка обременений для строки «Обременения» ЕГРН-таблицы
 * (#1954). Раньше ячейка была захардкожена «—», хотя бэкенд уже отдаёт ЗОУИТ-
 * сводку. has_zouit=true → «ЗОУИТ: N (типы через запятую)»; иначе — честный
 * «не выявлено (НСПД)». null/отсутствие блока (старый кэш) → «—» (неизвестно).
 */
export function formatEncumbrance(
  enc: ParcelEncumbrance | null | undefined,
): string {
  if (enc == null) return "—";
  if (!enc.has_zouit) return "не выявлено (НСПД)";
  const types = enc.zouit_types.filter((t) => t && t.length > 0);
  const count = enc.zouit_count;
  return types.length > 0
    ? `ЗОУИТ: ${count} (${types.join(", ")})`
    : `ЗОУИТ: ${count}`;
}

// ── Adapter: backend egrn_block → frontend ParcelEgrn (issue #1217) ──────────

/**
 * Adapt the backend `egrn` block (see `backend/app/api/v1/parcels.py:2004-2024`)
 * into the frontend's `ParcelEgrn` shape. Backend column names trace to ЕГРН's
 * cad_parcels table (`permitted_use_established_by_document`, `ownership_type`,
 * `status`, `cost_registration_date`, `land_record_category_type`), so the
 * frontend type — designed around the spec'd table labels — drifted. Without
 * this mapping 6/10 rows in the ЕГРН table and the matching CSV cells render
 * empty in prod (#1217, P2).
 *
 * Encumbrance and cad_num are NOT in `egrn_block`:
 *   - encumbrance is its own `encumbrance_block` (ЗОУИТ derived) → defaulted "—".
 *   - cad_num comes from the route/caller, threaded in via the `cad` argument.
 *
 * Returns `null` for an empty object `{}` or non-object input — the call-site
 * substitutes `buildFallbackEgrn(cad)`.
 */
export function adaptEgrn(raw: unknown, cad: string): ParcelEgrn | null {
  if (raw == null || typeof raw !== "object") return null;
  const src = raw as Record<string, unknown>;
  // Empty object → "no data" (backend writes `{}` when cad_parcels row is missing).
  if (Object.keys(src).length === 0) return null;

  const str = (v: unknown): string | null =>
    typeof v === "string" && v.length > 0 ? v : null;
  const num = (v: unknown): number | null =>
    typeof v === "number" && Number.isFinite(v) ? v : null;

  return {
    cad_num: cad,
    address: str(src.address) ?? "—",
    area_m2: num(src.area_m2) ?? NaN,
    vri: str(src.permitted_use_text) ?? "—",
    category: str(src.land_category) ?? "—",
    registration_date: str(src.registration_date),
    owner_type: str(src.ownership_type) ?? "—",
    // encumbrance lives in a sibling `encumbrance_block`, not in `egrn_block`.
    // Section 1 renders ЗОУИТ-derived encumbrance separately; keep "—" here so
    // the ЕГРН table doesn't claim "Нет" when we simply don't know.
    encumbrance: "—",
    status: str(src.parcel_status) ?? "—",
    last_updated: str(src.last_egrn_update_date),
  };
}

// ── Types: utilities (engineering nearby) ────────────────────────────────────

export type UtilitySubtype =
  | "substation"
  | "pipeline"
  | "power_line"
  | "water_intake"
  | "pumping_station"
  | string; // open for future subtypes

export interface UtilitySummaryItem {
  subtype: UtilitySubtype;
  nearest_m: number;
  name: string | null;
  count_within_2km: number;
}

export interface UtilitiesData {
  summary: UtilitySummaryItem[];
  power_line_охранная_зона_flag: boolean;
  note: string | null;
}

export interface ParcelAnalyzeResponse {
  cad_num: string;
  source: string;
  geom_geojson: unknown;
  district: {
    district_name: string;
    median_price_per_m2: number;
    dist_to_center: number;
  } | null;
  score: number;
  score_label?: string;
  score_explanation?: string;
  score_breakdown: Record<
    string,
    Array<{
      name: string | null;
      distance_m: number;
      last_edit: string | null;
      lat: number;
      lon: number;
    }>
  >;
  poi_count: number;
  /**
   * EGRN data — raw backend `egrn_block` (snake_case ЕГРН-cad_parcels columns,
   * see `backend/app/api/v1/parcels.py:2004-2024`). Use `adaptEgrn()` to narrow
   * into the frontend `ParcelEgrn` shape before rendering. Typed as `unknown`
   * because the wire shape (`permitted_use_text` / `land_category` / …) doesn't
   * match `ParcelEgrn` (`vri` / `category` / …) — the un-narrowed cast was the
   * root cause of #1217. May be `null`/missing if B5-extended hasn't shipped.
   */
  egrn?: unknown;
  /**
   * #1954 — обременения участка. Бэкенд (`parcels.py` `encumbrance_block`,
   * сериализуется как top-level `encumbrance`) пересекает геом участка с
   * `cad_zouit` и отдаёт ЗОУИТ-сводку. `has_zouit=false` → обременений по НСПД
   * не выявлено; `has_zouit=true` → `zouit_count` зон, `zouit_types` —
   * уникальные категории («Санитарно-защитная зона» и т.п.). Optional +
   * nullable: старый кэш / тонкий 202-стаб поля не несут.
   */
  encumbrance?: ParcelEncumbrance | null;
  /** Engineering / utilities nearby — present when NSPD data available */
  utilities?: UtilitiesData | null;
  // #999 (958-B4) — рыночные слои для карты Site Finder. Все optional:
  // отражают graceful 202-стаб (ещё нет данных) и тонкие ответы (нет
  // конкурентов / пустой pipeline / нет risk-зон).
  /** Конкуренты в радиусе 3 км (DOM.РФ + objective_lots), несут lat/lon. */
  competitors?: ParcelAnalysisCompetitor[];
  /** Будущие проекты 24 мес (D4 #36); top_objects несут lat/lon. */
  pipeline_24mo?: Pipeline24mo | null;
  /** Риск-зоны НСПД (TIER 3 #94) — geom_wkt; часто пустой массив. */
  nspd_risk_zones?: RiskZone[] | null;
  // §12.1-13 (#958) — карта-слои граддок/перспективного предложения. Все
  // optional: backend сериализует их в /analyze (parcels.py), но дамп квартала
  // может быть не загружен → пустой массив (graceful), либо тонкий 202-стаб.
  /** TIER 4 opportunity-ЗУ (#94): auction/scheme/free/future/oopt — geom_wkt. */
  nspd_opportunity_parcels?: OpportunityParcel[] | null;
  /** TIER 4 красные линии застройки (#94, граддок) — geom_wkt (LINESTRING). */
  nspd_red_lines?: RedLine[] | null;
  /**
   * #255 — ЗОУИТ overlaps (зоны с особыми условиями использования). Бэкенд уже
   * отдаёт их в /analyze (Section4Estimate читает через cast в ParcelAnalysis);
   * объявляем здесь, чтобы MiniMap-адаптер пробрасывал их в ЗОУИТ-слой карты без
   * cast. geom_geojson на каждом overlap — для отрисовки полигонов (graceful,
   * если backend ещё не добавил поле).
   */
  nspd_zouit_overlaps?: NspdZouitOverlap[] | null;
  /**
   * #1962 — резолвленный ПЗЗ-градрегламент зоны (synth #1891). Бэкенд
   * (`parcels.py`, top-level `nspd_zoning`) отдаёт тер.зону (`regulation_zone_index`
   * напр. "ПК-1"), КСИТ (`max_far`), предельную высоту/этажность, коэф. застройки
   * (`max_building_pct`), источник регламента (`regulation_source`) и список
   * разрешённых ВРИ зоны (`main_vri`). Light-отчёт (Section1ParcelInfo) выводит его
   * в «Градрегламент (НСПД)»-блоке через `adaptNspdRegulation`. Optional + nullable:
   * для не-ЕКБ участков / при miss резолвера поле может быть null (gap-зоны #1891).
   */
  nspd_zoning?: NspdZoning | null;
}

// ── Types: B6 poi-score response ─────────────────────────────────────────────

export interface PoiScoreItem {
  category: string;
  name: string;
  distance_m: number;
  weight: number;
  score_contribution: number;
}

export interface PoiScoreResponse {
  cad_num: string;
  poi_weighted_score: number;
  items: PoiScoreItem[];
}

// ── Hook: useParcelAnalyzeQuery (B5) ─────────────────────────────────────────

// #93 on-demand NSPD fetch: POST /analyze returns 202 for any parcel whose
// geometry isn't cached yet. We must poll /fetch-status and re-POST once ready,
// mirroring useSiteAnalysis — otherwise the 202 stub (no score/competitors)
// reaches the render tree and crashes Section 3/4 (#1001, 958-B5).
const ANALYZE_POLL_INTERVAL_MS = 2000;
const ANALYZE_POLL_MAX_ITERATIONS = 60; // 60 × 2s = 2 min hard cap

export function useParcelAnalyzeQuery(cad: string, horizon: number = 12) {
  return useQuery({
    queryKey: ["parcel-analyze", cad, horizon],
    // TanStack Query v5 passes an AbortSignal in the queryFn context; it aborts
    // on unmount and whenever the queryKey changes (смена cad/horizon). Thread
    // it through the POST/GET fetches and check it before each poll iteration so
    // the loop dies immediately instead of zombie-polling /fetch-status + re-
    // POSTing /analyze up to the 2-min hard cap (#1248).
    queryFn: async ({ signal }): Promise<ParcelAnalyzeResponse> => {
      if (MOCK_ANALYZE) {
        // Return fixture data regardless of cad/horizon — for dev only
        return fixtureAnalyze as ParcelAnalyzeResponse;
      }

      // Backend accepts ?horizon= ∈ {6,12,18} (default 12; 422 otherwise, #995).
      const analyzeUrl = `/api/v1/parcels/${encodeURIComponent(
        cad,
      )}/analyze?horizon=${horizon}`;

      // First request — POST /analyze. apiFetchWithStatus surfaces the 202
      // Accepted code instead of treating it as a successful payload.
      const first = await apiFetchWithStatus<
        ParcelAnalyzeResponse | AnalyzeAcceptedResponse
      >(analyzeUrl, { method: "POST", signal });

      // 200 → geometry was cached, full analysis is ready.
      if (first.status === 200) {
        return first.body as ParcelAnalyzeResponse;
      }

      // 202 Accepted → geometry is being fetched from НСПД. Poll /fetch-status
      // every 2s; once ready re-POST /analyze (now 200). The query stays
      // pending throughout, so the page's "Анализируем участок…" screen shows.
      if (first.status === 202) {
        for (let i = 0; i < ANALYZE_POLL_MAX_ITERATIONS; i++) {
          // abortableSleep wakes immediately on abort; then bail before issuing
          // any further requests (signal also aborts the in-flight fetches).
          await abortableSleep(ANALYZE_POLL_INTERVAL_MS, signal);
          if (signal.aborted) {
            throw new DOMException("Анализ отменён", "AbortError");
          }
          const status = await apiFetch<FetchStatusResponse>(
            `/api/v1/parcels/${encodeURIComponent(cad)}/fetch-status`,
            { signal },
          );
          if (status.status === "ready") {
            // Re-POST should now be 200. If it races back to 202, keep polling
            // rather than returning the stub (symmetry with the first request).
            const second = await apiFetchWithStatus<
              ParcelAnalyzeResponse | AnalyzeAcceptedResponse
            >(analyzeUrl, { method: "POST", signal });
            if (second.status === 200) {
              return second.body as ParcelAnalyzeResponse;
            }
            // 202 race → fall through and poll /fetch-status again.
          }
          if (status.status === "not_in_nspd") {
            throw new HTTPError(
              404,
              status,
              status.error_msg ?? "Кадастровый номер не найден в НСПД",
            );
          }
          if (status.status === "failed") {
            throw new HTTPError(
              503,
              status,
              status.error_msg ?? "НСПД временно недоступен",
            );
          }
          if (status.status === "invalid_format") {
            throw new HTTPError(
              400,
              status,
              status.error_msg ?? "Неверный формат кадастрового номера",
            );
          }
          // status === "fetching" → continue polling
        }
        throw new Error(
          "Загрузка длится слишком долго (>2 мин). Попробуйте позже.",
        );
      }

      throw new Error(`Неожиданный статус ответа: ${first.status}`);
    },
    staleTime: 5 * 60_000, // 5 min — analyze is expensive
    // The queryFn polls internally; a thrown terminal error (404/503/400/
    // timeout) is final and must NOT restart the whole poll loop via retry.
    retry: false,
    // Horizon is in the queryKey, so switching it is a fresh cache entry. Keep
    // the previous analysis on screen while the new horizon re-fetches instead
    // of blanking the whole page to the loading state on every selector change.
    placeholderData: keepPreviousData,
  });
}

// ── Hook: useParcelPoiScoreQuery (B6) ────────────────────────────────────────

/**
 * Raw shape from backend /api/v1/parcels/{cad}/poi-score (#1486).
 * Backend now returns poi_weighted_score (0..100) and per-POI score_contribution (0..100)
 * normalized on the server side — no client-side reconstruction needed.
 */
interface PoiScoreRaw {
  cad_num: string;
  radius_m: number;
  poi_weighted_score: number;
  top_poi: Array<{
    name: string;
    category: string;
    distance_m: number;
    weight: number;
    score_contribution: number;
    address?: string | null;
  }>;
}

export function useParcelPoiScoreQuery(cad: string) {
  return useQuery({
    queryKey: ["parcel-poi-score", cad],
    queryFn: async (): Promise<PoiScoreResponse> => {
      if (MOCK_POI_SCORE) {
        return fixturePoiScore as PoiScoreResponse;
      }
      // Backend returns normalized poi_weighted_score (0..100) and score_contribution per POI.
      // Pass through directly — no client-side weight × 100 reconstruction (#1486).
      const raw = await apiFetch<PoiScoreRaw>(
        `/api/v1/parcels/${encodeURIComponent(cad)}/poi-score`,
      );
      const items: PoiScoreItem[] = (raw.top_poi ?? []).map((p) => ({
        category: p.category,
        name: p.name,
        distance_m: p.distance_m,
        weight: p.weight,
        score_contribution: p.score_contribution,
      }));
      return {
        cad_num: raw.cad_num,
        poi_weighted_score: raw.poi_weighted_score,
        items,
      };
    },
    staleTime: 5 * 60_000,
    retry: 1,
  });
}

// ── Chat: POST /api/v1/chat/ask (#958) ───────────────────────────────────────

/**
 * Parcel-chat contract (backend shipped, deployed). The endpoint is behind
 * Caddy auth + RBAC (any authenticated user). Types are defined inline rather
 * than via `npm run codegen` because codegen targets a live OpenAPI on
 * localhost:8000 (`src/lib/api-types.ts`) which is unreachable in this env;
 * these mirror the documented response exactly.
 */
export type ChatIntent =
  | "summary"
  | "what_to_build"
  | "why_forecast"
  | "risks"
  | "scenarios"
  | "unknown";

export interface ChatMessage {
  role: "user" | "assistant";
  content: string;
}

export interface ChatAskRequest {
  cad_num: string;
  message: string;
  intent?: ChatIntent;
  /** Recent turns for context — caller caps the history length. */
  history?: ChatMessage[];
}

export interface ChatGroundedIn {
  run_id: number;
  schema_version: string;
  sections: string[];
}

/** "ready" → forecast exists; "pending" → answer is a "run analysis first" msg. */
export type ChatReportStatus = "ready" | "pending";

export interface ChatAskResponse {
  answer: string;
  grounded_in: ChatGroundedIn | null;
  llm_used: boolean;
  fallback_reason: string | null;
  advisory: boolean;
  report_status: ChatReportStatus;
}

/**
 * POST /api/v1/chat/ask — ask a grounded question about a parcel's analysis.
 * Action (not a query) → drive from a TanStack `useMutation` in the component.
 * Uses the shared apiFetch (base URL + session header + Content-Type) per the
 * "don't duplicate fetch config" rule.
 */
export function postChatAsk(req: ChatAskRequest): Promise<ChatAskResponse> {
  return apiFetch<ChatAskResponse>("/api/v1/chat/ask", {
    method: "POST",
    body: JSON.stringify(req),
  });
}

// ── Hook: useParcelForecastQuery (958-B3 / §22) ──────────────────────────────

/**
 * Polls GET /api/v1/parcels/{cad}/forecast until the async §22 forecast is
 * ready. The forecast is enqueued fire-and-forget by useParcelAnalyzeQuery on
 * page mount (POST /analyze) — this hook does NOT trigger it, only reads.
 *
 * Endpoint contract:
 *   200 → { status: "ready", run_id, created_at, report }
 *   202 → { status: "pending" } (also returned gracefully on any error — never 404/500)
 *
 * Polls every 4s while pending; stops once status === "ready". Mock mode
 * (MOCK_ANALYZE) returns the committed prod fixture immediately.
 */
export function useParcelForecastQuery(cad: string) {
  return useQuery({
    queryKey: ["parcel-forecast", cad],
    queryFn: async (): Promise<ForecastEnvelope> => {
      if (MOCK_ANALYZE) {
        // Fixture is raw JSON (loose shape: factors carries a stray
        // `advisory_capped` boolean) — cast via unknown, runtime guards in the
        // confidence block narrow factor entries.
        return fixtureForecast as unknown as ForecastEnvelope;
      }
      // apiFetchWithStatus tolerates the 202 path (Accepted) without throwing.
      const { body } = await apiFetchWithStatus<ForecastEnvelope>(
        `/api/v1/parcels/${encodeURIComponent(cad)}/forecast`,
      );
      return body;
    },
    enabled: !!cad,
    // Stop polling once ready; otherwise re-poll every 4s while pending.
    refetchInterval: (query) =>
      query.state.data?.status === "ready" ? false : 4000,
    retry: 1,
  });
}
