/**
 * Concept (Generative Design) API — TanStack Query wrappers.
 *
 * Contract mirrors `backend/app/schemas/concept.py` exactly (Stage 1a frozen
 * interface). 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 — same approach as the
 * parcel-chat contract in `site-finder-api.ts`.
 *
 * Endpoint: POST /api/v1/concepts → ConceptOutput.
 * Export endpoints (DXF/PDF) are not in the Stage-1a contract yet; the export
 * buttons fall back to client-side GeoJSON/CSV generation and probe the
 * documented `/concepts/{...}/export` path opportunistically (see
 * ConceptExportButtons).
 */

import { useMutation, useQuery } from "@tanstack/react-query";
import type {
  Feature,
  FeatureCollection,
  MultiPolygon,
  Polygon,
} from "geojson";

import { apiFetch, apiFetchWithStatus } from "@/lib/api";

// ── Input contract (ConceptInput) ───────────────────────────────────────────

export type HousingClass = "econom" | "comfort" | "business";
export type DevelopmentType = "spot" | "mid_rise" | "high_rise";
export type ConceptStrategy = "max_area" | "max_insolation" | "balanced";

/**
 * Один пункт программы застройки (Stage 3a/3b, #1965): «поставить `count`
 * секций каталожного типа `section_type` этажностью `floors`». Габариты пятна
 * берёт бэк из каталога по ключу. None-программа → жадная max-FAR раскладка.
 * Зеркалит `BuildingProgramItem` в backend/app/schemas/concept.py
 * (`components["schemas"]["BuildingProgramItem"]` в api-types.ts).
 */
export interface BuildingProgramItem {
  /** Ключ типа дома из каталога house-types (см. useHouseTypes). */
  section_type: string;
  /** Этажность группы секций, 1–40. */
  floors: number;
  /** Сколько секций этого типа разместить, 1–50. */
  count: number;
  /**
   * Опциональное ручное пятно секции (ширина, м), 4–120. None / не задано →
   * габариты берёт бэк из каталога по `section_type`. Должно задаваться вместе с
   * `footprint_d_m` (частичное задание бэк игнорирует). ADDITIVE (Stage 3c).
   */
  footprint_w_m?: number | null;
  /** Опциональное ручное пятно секции (глубина, м), 4–120. См. `footprint_w_m`. */
  footprint_d_m?: number | null;
}

export interface ConceptInput {
  /** GeoJSON Polygon of the parcel (WGS84 / EPSG:4326). */
  parcel_geojson: Polygon;
  housing_class: HousingClass;
  /** Целевая этажность 1–30. */
  target_floors: number;
  development_type: DevelopmentType;
  /** Стоимость участка (₽) — опционально, для финмодели. */
  land_cost_rub?: number | null;
  /**
   * Stage 3b (#1965): опциональная программа типовых домов. Не задана / пусто →
   * жадная max-FAR раскладка (3 стратегии, без изменений). Задана → бэк кладёт
   * РОВНО эту программу (один вариант) и возвращает partial-fit сигнал.
   */
  building_program?: BuildingProgramItem[] | null;
}

// ── Output contract (ConceptOutput) ─────────────────────────────────────────

/** Технико-экономические показатели (ТЭП). */
export interface Teap {
  built_area_sqm: number;
  total_floor_area_sqm: number;
  residential_area_sqm: number;
  /** Нежилое (коммерция/офисы 1-го этажа), кв.м. Additive к жилой площади. */
  office_area_sqm: number;
  apartments_count: number;
  density: number;
  parking_spaces: number;
}

export interface FinancialModel {
  // Legacy summary (backward-compat)
  revenue_rub: number;
  cost_rub: number;
  gross_margin_rub: number;
  irr: number;
  // Revenue breakdown
  revenue_residential_rub: number;
  revenue_parking_rub: number;
  /** Нежилое (коммерция/офисы 1-го этажа) — additive выручка к GDV. */
  revenue_office_rub: number;
  // Cost cascade
  construction_rub: number;
  pir_rub: number;
  networks_rub: number;
  developer_services_rub: number;
  contingency_rub: number;
  marketing_rub: number;
  land_rub: number;
  // БДР / taxes → net profit
  vat_rub: number;
  profit_before_tax_rub: number;
  profit_tax_rub: number;
  net_profit_rub: number;
  // Metrics
  roi: number;
  margin_pct: number;
  /** false → IRR настоящий (DCF); true → IRR — proxy (вырожденный поток). */
  irr_is_proxy: boolean;
  // DCF / investment metrics (PR-3, эпик #1881)
  /** NPV дисконтированного помесячного cashflow, ₽. */
  npv_rub: number;
  /** Окупаемость, мес (линейная интерполяция); null — не окупается. */
  payback_months: number | null;
  /** Годовая ставка дисконтирования, применённая в NPV. */
  discount_rate_used: number;
  /** true — график фаз/продаж типовой (допущение), не график проекта. */
  schedule_is_default: boolean;
  // Price calibration (PR-2, эпик #1881)
  /** Цена продажи жилья, ₽/м², фактически использованная в выручке. */
  price_per_sqm_used: number;
  /** true — калибровано по рынку; false — норматив класса. */
  price_is_calibrated: boolean;
  /** "objective_district_median" | "district_reference" | "class_norm". */
  price_source: PriceSource;
  // Financing / кредит (PR-5, эпик #1881)
  /** false → финансирование не моделировалось (строки кредита не показываем). */
  financing_enabled: boolean;
  /** Годовая ставка кредита, доля (0.15 = 15% годовых). */
  annual_rate_used: number;
  /** Пиковый долг по телу кредита, ₽. */
  peak_debt_rub: number;
  /** Суммарные проценты по кредиту за проект, ₽. */
  total_interest_rub: number;
  /** Чистая прибыль за вычетом процентов по кредиту, ₽. */
  net_profit_after_financing_rub: number;
  /**
   * true — упрощённая модель: весь кассовый разрыв покрыт кредитом по
   * ставке-нормативу, проценты капитализируются, эскроу не моделируется точно.
   */
  financing_is_simplified: boolean;
  /**
   * IRR на собственные средства (equity) при кредитном плече LTC 70 %. При
   * положительном леверидже > проектной (unlevered) `irr`; при отрицательном —
   * ниже неё или отрицательная (проект не покрывает стоимость долга).
   */
  levered_irr: number;
  /** true → levered_irr — оценка (roi-proxy / вырожденный equity-поток), не DCF. */
  levered_irr_is_proxy: boolean;
}

export type PriceSource =
  "objective_district_median" | "district_reference" | "class_norm";

export interface ConceptVariant {
  strategy: ConceptStrategy;
  /** FeatureCollection полигонов зданий (WGS84). */
  buildings_geojson: FeatureCollection;
  teap: Teap;
  financial: FinancialModel;
  /**
   * Stage 3b (#1965) — partial-fit сигнал program-режима: `requested_count` —
   * сколько секций просили (Σ count), `placed_count` — сколько реально влезло.
   * placed < requested → участок вмещает меньше (честная заметка, не ошибка).
   * Оба null в greedy-режиме (программа не задана) — backward-compat.
   */
  placed_count?: number | null;
  requested_count?: number | null;
}

export interface ConceptOutput {
  variants: ConceptVariant[];
}

// ── Strategy display metadata ────────────────────────────────────────────────

export const STRATEGY_LABELS: Record<ConceptStrategy, string> = {
  max_area: "Макс. площадь",
  max_insolation: "Макс. инсоляция",
  balanced: "Баланс",
};

export const STRATEGY_HINTS: Record<ConceptStrategy, string> = {
  max_area: "Максимальная плотность застройки в пределах нормативов.",
  max_insolation: "Приоритет инсоляции и разрывов между корпусами.",
  balanced: "Компромисс между ТЭП и качеством среды.",
};

export const HOUSING_CLASS_LABELS: Record<HousingClass, string> = {
  econom: "Эконом",
  comfort: "Комфорт",
  business: "Бизнес",
};

export const DEVELOPMENT_TYPE_LABELS: Record<DevelopmentType, string> = {
  spot: "Точечная",
  mid_rise: "Среднеэтажная",
  high_rise: "Высотная",
};

/**
 * Honest caption for the sale price source (PR-2). Never passes a class norm off
 * as a market figure — when the price is not calibrated the caption says so.
 */
export function priceSourceCaption(financial: FinancialModel): string {
  switch (financial.price_source) {
    case "objective_district_median":
      return "рынок: медиана объявлений Objective по району";
    case "district_reference":
      return "рынок: справочная медиана района (нет свежей выборки Objective)";
    case "class_norm":
    default:
      return "норматив класса (нет рыночных данных по участку)";
  }
}

// ── Hook: useCreateConcept ────────────────────────────────────────────────────

/**
 * POST /api/v1/concepts — generate building variants for a parcel polygon.
 * Action (not a query) → drive from `useMutation` per the data-layer rule.
 * Uses the shared `apiFetch` (base URL + session header + Content-Type) rather
 * than re-adding fetch config.
 */
export function useCreateConcept() {
  return useMutation<ConceptOutput, Error, ConceptInput>({
    mutationFn: (payload) =>
      apiFetch<ConceptOutput>("/api/v1/concepts", {
        method: "POST",
        body: JSON.stringify(payload),
      }),
  });
}

// ── House-type catalog (Stage 3b, #1965) ─────────────────────────────────────

/**
 * Один тип дома каталога, отдаваемый `GET /concepts/house-types`. Зеркалит
 * `HouseTypeCatalogItem` в backend/app/schemas/concept.py
 * (`components["schemas"]["HouseTypeCatalogItem"]` в api-types.ts). Каталог —
 * единый источник истины: фронт НЕ хардкодит габариты/этажности.
 */
export interface HouseTypeCatalogItem {
  /** Ключ типа — значение для BuildingProgramItem.section_type. */
  section_type: string;
  /** Человекочитаемый русский лейбл. */
  label_ru: string;
  /** Ширина пятна секции, м. */
  footprint_w_m: number;
  /** Глубина пятна секции, м. */
  footprint_d_m: number;
  /** Площадь пятна секции, м² (ширина × глубина). */
  footprint_sqm: number;
  /** Дефолтная этажность типа (UI подставляет; пользователь меняет в 1–40). */
  default_floors: number;
  /** Подходящий класс жилья. */
  housing_class: HousingClass;
}

interface HouseTypeCatalog {
  house_types: HouseTypeCatalogItem[];
}

/**
 * GET /api/v1/concepts/house-types — каталог типовых домов для пикера Stage 3b.
 * Read-only справочник (статичен на бэке) → `useQuery` per data-layer rule;
 * `staleTime: Infinity`, т.к. каталог не меняется в рамках сессии. Возвращает
 * массив пунктов каталога (разворачиваем `{house_types}` в `select`).
 */
export function useHouseTypes() {
  return useQuery<HouseTypeCatalogItem[]>({
    queryKey: ["concept", "house-types"],
    queryFn: async () => {
      const data = await apiFetch<HouseTypeCatalog>(
        "/api/v1/concepts/house-types",
      );
      return data.house_types;
    },
    staleTime: Infinity,
  });
}

// ── Live recompute contract (Stage 2b, #1965) ───────────────────────────────

/**
 * MassingProgram — the aggregated building program POSTed to
 * `/api/v1/concepts/recompute`. Mirrors `backend/app/schemas/concept.py`
 * (and `components["schemas"]["MassingProgram"]` in api-types.ts): an already
 * FOLDED program from the interactive 3D massing's `computeModel`
 * (Σ footprint × floors), без покомпонентной геометрии секций. The endpoint
 * synthesises ТЭП from it and runs the same `compute_financial` → live economics.
 */
export interface MassingProgram {
  /** Суммарное пятно застройки всех секций, кв.м (скаляр). */
  total_footprint_sqm: number;
  /** Этажность программы. */
  floors: number;
  /** Число секций (метаданные программы). */
  sections: number;
  /** Площадь участка для плотности (FAR). */
  site_area_sqm: number;
  housing_class: HousingClass;
  development_type: DevelopmentType;
  /** Стоимость участка для финмодели (опционально). */
  land_cost_rub?: number | null;
  /** Предрезолвленная рыночная цена жилья, ₽/м² (skip backend DB lookup). */
  market_price_per_sqm?: number | null;
  /** Подлинный источник предрезолвленной цены (forward из financial_estimate). */
  price_source?: string | null;
  /** WKT-точка центроида участка (WGS84) для DB-резолва цены при fallback. */
  parcel_centroid_wkt?: string | null;
}

/** Output of `/api/v1/concepts/recompute`: synthesised ТЭП + finmodel. */
export interface MassingRecomputeOutput {
  teap: Teap;
  financial: FinancialModel;
}

/**
 * POST /api/v1/concepts/recompute — live financial recompute for an interactive
 * massing program. Action (slider-driven) → `useMutation` per the data-layer
 * rule; uses the shared `apiFetch` (base URL + session header + Content-Type).
 *
 * Stage 2b drives this off the 3D MassingScene's `onModelChange` (debounced),
 * with latest-wins sequencing handled by the caller (see MassingEconomics).
 */
export function useRecomputeMassing() {
  return useMutation<MassingRecomputeOutput, Error, MassingProgram>({
    mutationFn: (payload) =>
      apiFetch<MassingRecomputeOutput>("/api/v1/concepts/recompute", {
        method: "POST",
        body: JSON.stringify(payload),
      }),
  });
}

// ── Hook: useCadastreGeom ─────────────────────────────────────────────────────

/**
 * Resolves a cadastre number to its parcel polygon by POSTing the Site Finder
 * analyze endpoint (`/api/v1/parcels/{cad}/analyze`) and reading `geom_geojson`.
 * Re-uses the existing endpoint rather than adding a new backend route.
 *
 * The analyze endpoint returns 202 while НСПД geometry is still being fetched
 * (see useParcelAnalyzeQuery). For the concept input we keep the UX simple: a
 * 202 surfaces a "try again" error instead of a 2-minute poll loop — the user
 * can re-submit once geometry is cached, or draw the polygon manually.
 */
export function useCadastreGeom() {
  return useMutation<Polygon, Error, string>({
    mutationFn: async (cad) => {
      const url = `/api/v1/parcels/${encodeURIComponent(cad)}/analyze?horizon=12`;
      const { status, body } = await apiFetchWithStatus<{
        geom_geojson?: unknown;
      }>(url, { method: "POST" });

      if (status === 202) {
        throw new Error(
          "Геометрия участка ещё загружается из НСПД. Повторите через несколько секунд.",
        );
      }
      const polygon = extractPolygon(body?.geom_geojson);
      if (!polygon) {
        throw new Error(
          "Не удалось получить геометрию участка по этому номеру.",
        );
      }
      return polygon;
    },
  });
}

// ── Polygon helpers ───────────────────────────────────────────────────────────

/**
 * Narrows an unknown GeoJSON value (e.g. the analyze endpoint's `geom_geojson`)
 * into a single Polygon — the geometry POSTed as `parcel_geojson` to /concepts.
 * Accepts a bare Polygon, a Feature wrapping one, or a MultiPolygon (collapsed to
 * its LARGEST-area sub-polygon, NOT `coordinates[0]`: for a multi-part parcel the
 * first part can be a sliver, so the generative corpus must sit on the dominant
 * part). Returns null on any other shape.
 *
 * NB: the massing SCENE (outline / projection origin / OSM tile) must instead use
 * the WHOLE geometry via `extractParcelGeometry`, so it shares the frame the
 * backend neighbour endpoint anchors context to.
 */
export function extractPolygon(geom: unknown): Polygon | null {
  if (geom == null || typeof geom !== "object") return null;
  const g = geom as {
    type?: unknown;
    coordinates?: unknown;
    geometry?: unknown;
  };

  if (g.type === "Feature") {
    return extractPolygon((g as Feature).geometry);
  }
  if (g.type === "Polygon" && Array.isArray(g.coordinates)) {
    return {
      type: "Polygon",
      coordinates: g.coordinates as Polygon["coordinates"],
    };
  }
  if (g.type === "MultiPolygon" && Array.isArray(g.coordinates)) {
    const parts = g.coordinates as Polygon["coordinates"][];
    let best: Polygon["coordinates"] | null = null;
    let bestArea = -Infinity;
    for (const part of parts) {
      if (!Array.isArray(part) || part.length === 0) continue;
      const area = polygonAreaSqm({ type: "Polygon", coordinates: part });
      if (area > bestArea) {
        bestArea = area;
        best = part;
      }
    }
    if (best) return { type: "Polygon", coordinates: best };
  }
  return null;
}

/**
 * Narrows an unknown GeoJSON value into the WHOLE parcel geometry — a Polygon or a
 * MultiPolygon preserving ALL sub-polygons (unlike `extractPolygon`, which drops
 * to one part for the /concepts request). The §7 massing scene feeds this to
 * derive its projection origin, OSM ground-tile bounds and drawn outline over
 * every part, so neighbour context (anchored by the backend to the entire parcel
 * geometry) lands geo-aligned instead of offset from part 0. Returns null on any
 * non-polygonal shape.
 */
export function extractParcelGeometry(
  geom: unknown,
): Polygon | MultiPolygon | null {
  if (geom == null || typeof geom !== "object") return null;
  const g = geom as {
    type?: unknown;
    coordinates?: unknown;
    geometry?: unknown;
  };

  if (g.type === "Feature") {
    return extractParcelGeometry((g as Feature).geometry);
  }
  if (g.type === "Polygon" && Array.isArray(g.coordinates)) {
    return {
      type: "Polygon",
      coordinates: g.coordinates as Polygon["coordinates"],
    };
  }
  if (g.type === "MultiPolygon" && Array.isArray(g.coordinates)) {
    return {
      type: "MultiPolygon",
      coordinates: g.coordinates as MultiPolygon["coordinates"],
    };
  }
  return null;
}

/** Approximate parcel area in m² from a WGS84 polygon (spherical-excess shoelace). */
export function polygonAreaSqm(polygon: Polygon): number {
  const ring = polygon.coordinates[0];
  if (!ring || ring.length < 4) return 0;
  const R = 6_378_137; // WGS84 equatorial radius (m)
  const toRad = (deg: number) => (deg * Math.PI) / 180;
  let area = 0;
  for (let i = 0; i < ring.length - 1; i++) {
    const [lon1, lat1] = ring[i];
    const [lon2, lat2] = ring[i + 1];
    area +=
      toRad(lon2 - lon1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
  }
  return Math.abs((area * R * R) / 2);
}

/**
 * Centroid of a WGS84 polygon's outer ring as a WKT `POINT(lon lat)` string —
 * used as `parcel_centroid_wkt` so the recompute endpoint can DB-resolve a
 * market price when no pre-resolved one is forwarded. Simple ring-average
 * (good enough as a point hint; not a true area centroid). Returns null on a
 * degenerate ring.
 */
export function polygonCentroidWkt(polygon: Polygon): string | null {
  const ring = polygon.coordinates[0];
  if (!ring || ring.length < 4) return null;
  // Skip the closing vertex (== first) when averaging.
  const pts = ring.slice(0, ring.length - 1);
  if (pts.length === 0) return null;
  let sumLon = 0;
  let sumLat = 0;
  for (const [lon, lat] of pts) {
    sumLon += lon;
    sumLat += lat;
  }
  const lon = sumLon / pts.length;
  const lat = sumLat / pts.length;
  if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
  return `POINT(${lon} ${lat})`;
}
