/**
 * §22 forecast / scenarios / confidence — TypeScript contract.
 *
 * Ground truth: the committed prod report at
 * `src/lib/mocks/parcel-forecast.json` (run #68, parcel 66:41:0702048:27).
 * Types describe ONLY the sub-shapes Section 6 renders; the §13 report is
 * larger (scoring, market_now, product_tz, …) — fields not consumed here are
 * intentionally left as optional/loose to avoid over-coupling to the backend.
 *
 * The endpoint envelope (GET /api/v1/parcels/{cad}/forecast):
 *   200 → { status: "ready", run_id, created_at, report }
 *   202 → { status: "pending" }
 */

export type ConfidenceLevel = "high" | "medium" | "low";

/** Scenario keys produced by the §22 envelope generator. */
export type ScenarioKey = "base" | "aggressive" | "conservative";

// ── Segment ─────────────────────────────────────────────────────────────────

export interface ForecastSegment {
  district: string | null;
  obj_class: string | null;
  room_bucket: string | null;
  price_bucket: string | null;
}

// ── Future competitor (forward-looking, distinct from market_now competitors) ─

/**
 * §9.7 forward-looking competitor (top-N by `relevance_weight` at the horizon).
 * Shape mirrors the backend `Competitor` schema (`schemas/parcel.py`) as flattened
 * by `_competitor_to_dict` (demand_supply_forecast.py): `distance_m` /
 * `velocity_per_month` are required floats there, while `comm_name` / `obj_class` /
 * `flats_total` / `relevance_weight` are nullable — null is rendered as «нет данных»,
 * never a fabricated 0.
 */
export interface FutureCompetitor {
  obj_id: number;
  comm_name: string | null;
  obj_class: string | null;
  distance_m: number;
  flats_total: number | null;
  relevance_weight: number | null;
  velocity_per_month: number;
}

// ── Demand / supply forecast (one per horizon) ───────────────────────────────

export interface DemandSupplyForecast {
  segment: ForecastSegment;
  advisory: boolean;
  confidence: ConfidenceLevel;
  open_units: number;
  rate_future: number;
  balance_ratio: number;
  balance_units: number;
  /** −1..1 — see deficit semantics: >0 недонасыщенность, <0 затоварка. */
  deficit_index: number;
  horizon_months: number;
  macro_coefficient: number;
  future_competitors: FutureCompetitor[];
  future_online_units: number;
  months_of_inventory: number;
  hidden_release_units: number;
  base_pace_units_per_mo: number;
  projected_demand_units: number;
  projected_supply_units: number;
  demand_norm_coefficient: number;
  rate_sensitivity_phrase: string;
}

// ── Exec summary ─────────────────────────────────────────────────────────────

export interface ReportExecSummaryKeyNumbers {
  confidence: ConfidenceLevel | null;
  deficit_index: number | null;
  overall_score: number | null;
  months_of_inventory: number | null;
}

export interface ReportExecSummary {
  headline: string | null;
  verdict: string | null;
  key_numbers: ReportExecSummaryKeyNumbers;
  overall_confidence: ConfidenceLevel | null;
}

// ── Future market ────────────────────────────────────────────────────────────

/**
 * §9.3 future-supply-pressure breakdown (FutureSupplyPressure.breakdown()).
 * `open_units` / `hidden_units` are integer unit stocks (never null at the
 * source); `future_units_by_horizon` / `monthly_absorption_units` /
 * `months_of_pressure` / `index` go through `_round_or_none` and are null when the
 * market signal is missing (no absorption → давление неизмеримо) — rendered as
 * «нет данных», never a fabricated 0.
 */
export interface FutureSupplyBreakdown {
  index: number | null;
  open_units: number;
  hidden_units: number;
  months_of_pressure: number | null;
  future_units_by_horizon: number | null;
  monthly_absorption_units: number | null;
}

/**
 * §9.3 future supply pressure (FutureSupplyPressure.as_dict()). `confidence` and
 * `horizon_months` are always present at the source; `index` is null on thin data;
 * `district` / `premise_kind` may be null (EKB-wide / unspecified).
 */
export interface FutureSupply {
  index: number | null;
  district: string | null;
  breakdown: FutureSupplyBreakdown;
  confidence: ConfidenceLevel;
  premise_kind: string | null;
  horizon_months: number;
}

/** deficit_index per scenario at the target horizon. */
export interface ScenariosSummary {
  base: number;
  aggressive: number;
  conservative: number;
}

export interface ReportFutureMarket {
  summary: string | null;
  forecasts_by_horizon: DemandSupplyForecast[];
  future_supply: FutureSupply | null;
  scenarios_summary: ScenariosSummary | null;
  future_competitors: FutureCompetitor[];
}

// ── Scenarios ────────────────────────────────────────────────────────────────

/** Key path = horizon months (as string), value = key rate (% годовых). */
export type RatePath = Record<string, number>;

export interface ScenarioForecast {
  advisory: boolean;
  scenario: string;
  forecasts: DemandSupplyForecast[];
  rate_path: RatePath;
}

export interface ReportScenarios {
  summary: string | null;
  by_scenario: Partial<Record<ScenarioKey, ScenarioForecast>>;
  /** #1871 P1 — true когда demand_normalization gate отбил β rate-sensitivity и
   * conservative/base/aggressive дают идентичный projected_demand_units на всех
   * горизонтах. UI рендерит ОДНУ карточку «Базовый» + caveat вместо трёх копий. */
  scenarios_collapsed?: boolean;
  /** Полное русское предложение-причина (готовое к рендеру). null когда не схлопнуто. */
  scenarios_collapse_reason?: string | null;
}

// ── Confidence ───────────────────────────────────────────────────────────────

export interface ConfidenceFactor {
  note: string;
  level: ConfidenceLevel;
  value: number | null;
  /**
   * Human-readable RU label (#1737). For per-service «component» factors this is
   * the service name («Рынок» / «Будущее предложение» / …) so the UI never shows a
   * bare «Компонент N». Optional — older envelopes omit it; the UI falls back to
   * the factor key.
   */
  label?: string;
}

export interface ReportConfidence {
  level: ConfidenceLevel | null;
  rationale: string | null;
  /** The explicit drivers map (DoD: «явный список drivers»). */
  factors: Record<string, ConfidenceFactor>;
}

// ── Product TZ (§13.4 — рекомендация продукта, #953/#983) ────────────────────

/**
 * §16 structured reason — shared by `reasons[]` and the `reason` nested on each
 * USP / mix entry. All fields optional: the assembler lifts class_reco.reason or
 * per-segment reasons, and a thin overlay may carry only `why`.
 */
export interface ProductReasonDriver {
  factor: string;
  value: number | null;
  /** "+" сигнал вверх / "−" давление вниз (типографский минус). */
  direction: string;
}

export interface ProductReasonRejected {
  alternative: string;
  deficit_index: number | null;
  /** «затоварка» | «слабее сигнал». */
  reason: string;
}

export interface ProductReason {
  why?: string | null;
  drivers?: ProductReasonDriver[];
  rejected?: ProductReasonRejected[];
  what_would_change?: string[];
  confidence?: ConfidenceLevel | null;
  advisory?: boolean;
}

/**
 * Квартирография entry. The assembler emits {bucket, obj_class, deficit_index}
 * from ranked_segments; `pct` (доля формата) is consumed by the export renderers
 * but absent from current runs — kept optional so a future explicit `mix` with
 * shares renders too. `deficit_index` semantics mirror the rest of §22:
 * >0 недонасыщенность, <0 затоварка.
 */
export interface ProductMixEntry {
  bucket: string | null;
  obj_class: string | null;
  deficit_index: number | null;
  /** Доля формата, % (0..100) — присутствует только в явном mix. */
  pct?: number | null;
  /**
   * #1745 — прогноз спроса по формату (квартир) на целевом горизонте. Проброшен из
   * ranked_segments overlay (`projected_demand_units`). null при тонких данных или в
   * demand_only-режиме (без геометрии участка спрос-проекция неизмерима) — рендерится
   * «тонкие данные», НЕ 0.
   */
  projected_demand_units?: number | null;
  /**
   * #1745 — деривированный сигнал из deficit_index: «строить» (недонасыщенность) /
   * «баланс» / «избегать» (затоварка). null при deficit_index=null (тонкие данные).
   * Дублирует frontend-семантику deficitWord — фронт может пересчитать сам как fallback.
   */
  signal?: string | null;
  confidence?: ConfidenceLevel | null;
}

/** §10.4 коммерческий сигнал — обычно «нет данных» (available=false + caveat). */
export interface ProductCommercial {
  available?: boolean;
  caveat?: string | null;
  advisory?: boolean;
}

/** §10.5 USP-из-дефицитов — top-K недообеспеченных форматов. */
export interface ProductUsp {
  segment: string | null;
  obj_class: string | null;
  deficit_index: number | null;
  usp_text: string | null;
  reason?: ProductReason;
}

export interface ReportProductTz {
  /** Рекомендованный класс (§10.2). */
  obj_class: string | null;
  /** Квартирография — доли/дефицит форматов. */
  mix: ProductMixEntry[];
  /** Коммерческий сигнал (§10.4). */
  commercial: ProductCommercial | null;
  /** USP-ниши из дефицитов (§10.5). */
  usp: ProductUsp[];
  /** §16-причины (отвергнутые альтернативы / what-would-change). */
  reasons: ProductReason[];
  /** Короткий RU-текст про рекомендованный продукт. */
  summary: string | null;
  /**
   * Оговорка по gate-вердикту: рекомендация условна, если МКД-строительство под
   * вопросом (gate: Нельзя). null когда gate не ограничивает.
   */
  gate_caveat?: string | null;
}

// ── Scoring (§13.6 — прозрачность скоринга, #985/#986) ───────────────────────

/**
 * One of the 10 §14.2 product scores (#985). `value` ∈ [0,1] (выше = лучше для
 * девелопера — риск-скоры supply_risk / future_competition / mortgage_sensitivity
 * уже инвертированы у источника, так что бóльшее значение всегда «лучше»). `value`
 * is null when backing data is thin (`source: "unavailable"`) — never 0-as-stub.
 */
export interface ProductScore {
  /** Canonical key (demand / market_fit / supply_risk / …). */
  key: string;
  /** ∈ [0,1] (выше = лучше) или null при тонких данных. */
  value: number | null;
  /** Короткая RU §16-причина значения скора. */
  reason: string;
  /** Backing-сервис (market_metrics / poi_score / unavailable / …). */
  source: string;
  confidence: ConfidenceLevel;
}

/**
 * §14.2 product-score card (#985). `scores` is keyed by the canonical score key
 * (10 entries по `_SCORE_KEYS`). `overall` дублирует `ReportScoring.overall`.
 */
export interface ProductScoreCard {
  scores: Record<string, ProductScore>;
  overall: number | null;
  segment: ForecastSegment;
  advisory: boolean;
  confidence: ConfidenceLevel;
  horizon_months: number;
}

/**
 * One of the 6 §25 special indices (#986). `value` ∈ [0,1] = СИЛА сигнала (НЕ
 * однозначно «хорошо/плохо» — часть индексов риск-направленные: cost_of_error /
 * cannibalization / competitor_strength / artificial_demand растут к худшему,
 * launch_window / product_void — к лучшему). null при тонких данных/сбое
 * (`method: "unavailable"`). `label` — человекочитаемый дескриптор значения
 * (например «6 мес» для launch_window, «Комфорт» для cannibalization), НЕ имя
 * метрики. `detail` — опорные числа для explainability.
 */
export interface SpecialIndex {
  /** Canonical key (launch_window / product_void / cost_of_error / …). */
  key: string;
  /** ∈ [0,1] = сила сигнала (направление зависит от индекса) или null. */
  value: number | null;
  /** Человекочитаемый дескриптор значения (горизонт / класс / метка), не имя. */
  label: string | null;
  confidence: ConfidenceLevel;
  /** Опорные числа (explainability) — форма зависит от индекса. */
  detail: Record<string, unknown>;
  /** Источник/способ; "unavailable" если индекс не посчитан. */
  method: string;
  advisory: boolean;
}

/**
 * §25 special-indices card (#986). `indices` is keyed by the canonical index key
 * (6 entries по `_INDEX_KEYS`); недоступные присутствуют с value=null.
 */
export interface SpecialIndicesCard {
  indices: Record<string, SpecialIndex>;
  segment: ForecastSegment;
  district: string | null;
  advisory: boolean;
  confidence: ConfidenceLevel;
}

/**
 * §13.6 scoring — прозрачность итогового скора (#985/#986). `overall` ∈ [0,1]
 * дублируется в exec_summary KPI (карточка «Итоговый скор»), поэтому in-app блок
 * 6.5 показывает РАЗБИВКУ (почему скор такой), а не повторяет overall как KPI.
 */
export interface ReportScoring {
  /** §14.2 карта 10 продуктовых скоров (#985). */
  product_scores: ProductScoreCard | null;
  /** §25 карта 6 специальных индексов (#986). */
  special_indices: SpecialIndicesCard | null;
  /** Итоговый продуктовый скор ∈ [0,1] (дубль из карты для удобства). */
  overall: number | null;
}

// ── Meta ─────────────────────────────────────────────────────────────────────

export interface ReportMeta {
  cad_num: string;
  segment: ForecastSegment;
  district: string | null;
  horizons: number[];
  generated_at: string | null;
  schema_version: string;
  generated_advisory: boolean;
}

// ── Report (only the sections Section 6 renders are typed precisely) ──────────

export interface ForecastReport {
  meta: ReportMeta;
  /** Always true for §22 — drives the advisory disclaimer. */
  advisory: boolean;
  exec_summary: ReportExecSummary;
  future_market: ReportFutureMarket;
  scenarios: ReportScenarios;
  confidence: ReportConfidence;
  /**
   * §13.4 рекомендация продукта (#953). Optional: the report may be 202-pending
   * or a thin assembly may omit the section entirely — guarded at render time.
   */
  product_tz?: ReportProductTz;
  /**
   * §13.6 прозрачность скоринга (#985/#986). Optional: the report may be
   * 202-pending or a thin assembly may omit the section entirely — guarded at
   * render time (see ForecastScoringBlock / Section6Forecast 6.5).
   */
  scoring?: ReportScoring;
  schema_version?: string;
}

// ── Envelope ─────────────────────────────────────────────────────────────────

export interface ForecastEnvelope {
  status: "ready" | "pending";
  run_id?: number;
  created_at?: string;
  report?: ForecastReport;
}
