/**
 * nspd-regulation — PURE typed mapper: NspdZoning → градрегламент view-model.
 *
 * Single source of truth for turning the resolved ПЗЗ-регламент (synth #1891,
 * `nspd_zoning`) into the rows the light report (Section1ParcelInfo, #1962) and
 * the /ptica cockpit drawer render. Reused by `ptica-adapt` so the two stay
 * consistent (same field paths, same КСИТ-capacity maths, same coercion).
 *
 * No React, no side effects — narrowing + formatting only.
 */

import type { NspdZoning } from "@/types/nspd";

/**
 * Coerce a wire value to a finite float. The ekb-geoportal-urbancard regulation
 * columns are NUMERIC, so they arrive as STRINGS ("2.4", "12", "80") in
 * `nspd_zoning.max_far` / `max_height_m` / `max_building_pct` — this accepts
 * both string and number and rejects null / "" / non-numeric so callers never
 * do `NaN` arithmetic. The single coercion point for #1962 + the cockpit.
 */
export function coerceFloat(
  v: string | number | null | undefined,
): number | null {
  if (v == null) return null;
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
  const n = Number.parseFloat(v);
  return Number.isFinite(n) ? n : null;
}

/** Human regulation zone — prefer the readable index ("ПК-1") over reg-number. */
export function regulationZoneLabel(
  z: NspdZoning | null | undefined,
): string | null {
  return z?.regulation_zone_index ?? z?.zone_code ?? null;
}

/** КСИТ (max_far) as a plain RU number — e.g. "2,4". */
function formatFar(far: number): string {
  return far.toLocaleString("ru-RU", {
    minimumFractionDigits: 1,
    maximumFractionDigits: 1,
  });
}

function formatInt(n: number): string {
  return Math.round(n).toLocaleString("ru-RU");
}

/** A single value row of the градрегламент block. */
export interface NspdRegulationRow {
  label: string;
  value: string;
}

/** View-model consumed by the Раздел 1 «Градрегламент (НСПД)» card. */
export interface NspdRegulationVM {
  /** True when the resolver produced at least a zone — otherwise hide / empty. */
  present: boolean;
  /** тер.зона ("ПК-1") or null. */
  zone: string | null;
  /** источник регламента, e.g. "ekb-geoportal-urbancard". */
  source: string | null;
  /** Param rows (зона / КСИТ / высота / этажность / коэф. застройки / ёмкость). */
  rows: NspdRegulationRow[];
  /** Разрешённые ВРИ зоны (#1891 main_vri), trimmed; empty when absent. */
  permittedUses: string[];
}

/**
 * Map `nspd_zoning` (+ optional parcel area in m² for the КСИТ-capacity row) into
 * the градрегламент view-model. `areaM2` lets us derive КСИТ-ёмкость
 * (площадь × max_far) and пятно застройки (площадь × коэф. застройки) — the same
 * computation `adaptUrbanDrawer` uses; pass null when geometry is unknown and
 * those derived rows are simply omitted.
 */
export function adaptNspdRegulation(
  z: NspdZoning | null | undefined,
  areaM2: number | null,
): NspdRegulationVM {
  const zone = regulationZoneLabel(z);
  const maxFar = coerceFloat(z?.max_far);
  const maxHeight = coerceFloat(z?.max_height_m);
  const maxFloors = coerceFloat(z?.max_floors);
  const buildingPct = coerceFloat(z?.max_building_pct);
  const source = z?.regulation_source ?? null;

  const permittedUses = (z?.main_vri ?? [])
    .map((v) => v.replace(/\s+/g, " ").trim())
    .filter((v) => v.length > 0);

  const rows: NspdRegulationRow[] = [];

  if (zone != null) rows.push({ label: "Тер. зона", value: zone });

  if (maxFar != null) {
    rows.push({ label: "КСИТ (max FAR)", value: formatFar(maxFar) });
  }

  if (maxHeight != null) {
    rows.push({ label: "Пред. высота", value: `${formatInt(maxHeight)} м` });
  }

  if (maxFloors != null) {
    rows.push({ label: "Этажность", value: `${formatInt(maxFloors)} эт.` });
  }

  if (buildingPct != null) {
    rows.push({
      label: "Коэф. застройки",
      value: `${formatInt(buildingPct)} %`,
    });
  }

  // КСИТ-ёмкость = площадь × max_far (надземная GFA) — real only with both inputs.
  if (maxFar != null && areaM2 != null) {
    rows.push({
      label: "КСИТ-ёмкость",
      value: `≈ ${formatInt(areaM2 * maxFar)} м²`,
    });
  }

  // Пятно застройки = площадь × коэф. застройки.
  if (buildingPct != null && areaM2 != null) {
    rows.push({
      label: "Пятно застройки",
      value: `≈ ${formatInt(areaM2 * (buildingPct / 100))} м²`,
    });
  }

  if (source != null) rows.push({ label: "Источник", value: source });

  // "present" = the resolver produced a zone (synth #1891 always sets one when it
  // hits). A null nspd_zoning or an empty resolve → not present → caller hides /
  // shows «регламент не определён».
  const present = zone != null || rows.length > 0;

  return { present, zone, source, rows, permittedUses };
}
