/**
 * Shared helpers for Section 6 (forecast) blocks — deficit semantics, confidence
 * mapping, RU number formatting. Kept framework-agnostic (no JSX) so the three
 * forecast blocks share one source of truth.
 */

import type { BadgeVariant } from "@/components/ui/Badge";
import type {
  ConfidenceLevel,
  DemandSupplyForecast,
  FutureSupply,
  ProductMixEntry,
} from "@/types/forecast";

/** Below this |deficit_index| we treat the market as balanced (neutral). */
export const DEFICIT_BALANCE_EPS = 0.05;

/**
 * #1958/#1959 honesty-guard — at/above this |deficit_index| the signal is
 * SATURATED («упёрся» в край шкалы). The backend clamps deficit_index to [−1, 1]
 * and saturates the index at balance_ratio=0.5; when supply massively dwarfs
 * demand (ratio ≪ 0.5 на всех горизонтах) every horizon clamps to exactly −1.00
 * (зеркально +1.00 для острого дефицита). Showing the SAME «−1.00» ×4 reads as
 * broken / falsely precise — so we mark such values «на пределе» and раскрываем
 * честную плашку. Mirrors `ForecastChart.DEGENERATE_CLAMP_EPS` (0.02 →
 * |value| ≥ 0.98) but tightened to 0.99 per the §6 brief (а у графика свой порог).
 */
export const DEFICIT_PEG_THRESHOLD = 0.99;

/** Saturated (clamp-floor) signal? `|deficit_index| ≥ DEFICIT_PEG_THRESHOLD`. */
export function isDeficitPegged(deficitIndex: number): boolean {
  return Math.abs(deficitIndex) >= DEFICIT_PEG_THRESHOLD;
}

/**
 * Pegged-signal state across a set of horizon rows (HONESTY-GUARD, #1958/#1959).
 * The signal is «на пределе шкалы» only when EVERY present deficit_index is
 * saturated (|value| ≥ 0.99) AND all саме знака — then «−1.00 ×4» не различает
 * горизонты и плашка честнее точного числа. A single non-pegged / opposite-sign
 * row means there IS differentiation → not pegged (plain table tells the story).
 *
 * Returns `{ pegged, sign }`:
 *   pegged — render the honesty plaque + mark values «на пределе»;
 *   sign   — −1 затоварка (supply ≫ demand) / +1 острый дефицит, или null when
 *            не pegged. Drives which RU plaque text to show (zeroкально).
 * PURE (no JSX) — single source of truth shared by 6.1 (table) and 6.2 (scenarios).
 */
export interface DeficitPegState {
  pegged: boolean;
  sign: -1 | 1 | null;
}

export function deficitPegState(
  forecasts: Pick<DemandSupplyForecast, "deficit_index">[],
): DeficitPegState {
  const present = forecasts
    .map((f) => f.deficit_index)
    .filter((v): v is number => v != null);
  if (present.length === 0) return { pegged: false, sign: null };
  const allPegged = present.every(isDeficitPegged);
  if (!allPegged) return { pegged: false, sign: null };
  const allSameSign =
    present.every((v) => v > 0) || present.every((v) => v < 0);
  if (!allSameSign) return { pegged: false, sign: null };
  return { pegged: true, sign: present[0] > 0 ? 1 : -1 };
}

/**
 * Human RU plaque text for a pegged deficit signal (#1958/#1959). Финдиректору
 * объясняем простым языком, что точное «−1.00 ×4» — артефакт упора в край шкалы
 * (рынок перенасыщен), а не различие горизонтов. Зеркально для +1.0 — острый
 * дефицит. Источник правды для 6.1 и 6.2 (не дублировать строку в TSX).
 */
export function deficitPegPlaque(sign: -1 | 1): string {
  if (sign < 0) {
    return (
      "Сигнал на пределе шкалы: предложение во много раз превышает спрос на всех " +
      "горизонтах — сильная затоварка. Точное −1.00 не различает горизонты (рынок " +
      "перенасыщен), оценка грубая."
    );
  }
  return (
    "Сигнал на пределе шкалы: спрос во много раз превышает предложение на всех " +
    "горизонтах — острый дефицит. Точное +1.00 не различает горизонты (рынок " +
    "сильно недонасыщен), оценка грубая."
  );
}

/**
 * Deficit semantics (HARD): >0 недонасыщенность (повод строить, success);
 * <0 затоварка (warn/danger); ≈0 баланс (neutral).
 */
export function deficitVariant(deficitIndex: number): BadgeVariant {
  if (deficitIndex > DEFICIT_BALANCE_EPS) return "success";
  if (deficitIndex < -DEFICIT_BALANCE_EPS) return "danger";
  return "neutral";
}

export function deficitWord(deficitIndex: number): string {
  if (deficitIndex > DEFICIT_BALANCE_EPS) return "недонасыщенность";
  if (deficitIndex < -DEFICIT_BALANCE_EPS) return "затоварка";
  return "баланс";
}

/**
 * Сигнал-вердикт по deficit_index для таблицы форматов (#1745): «строить»
 * (недонасыщенность) / «баланс» / «избегать» (затоварка). Зеркало backend
 * `report_assembler._build_signal` — фронт-fallback, когда mix-ячейка не несёт
 * готового `signal` (старый прогон). Семантика та же, что у `deficitWord`.
 */
export function deficitSignalWord(deficitIndex: number): string {
  if (deficitIndex > DEFICIT_BALANCE_EPS) return "строить";
  if (deficitIndex < -DEFICIT_BALANCE_EPS) return "избегать";
  return "баланс";
}

// ── Class-level deficit aggregation (#1742 — объяснение выбора класса) ──────────

/** Агрегат дефицита одного класса: среднее deficit_index по его форматам. */
export interface ClassDeficit {
  obj_class: string;
  /** Среднее deficit_index по форматам класса (null — у всех форматов нет индекса). */
  meanDeficitIndex: number | null;
  /** Сколько форматов класса несут не-null deficit_index (для honest-агрегата). */
  nWithIndex: number;
}

/**
 * Агрегировать `mix` (квартирография — ячейки формат×класс) в per-класс дефицит:
 * среднее deficit_index по форматам каждого класса. Зеркало backend
 * `recommendation._recommend_class` (среднее по room-bucket'ам класса). Это даёт
 * мини-таблицу 3 классов (#1742) из уже-доступного `mix` без backend-расширения —
 * `mix` несёт все 15 ячеек (5 форматов × 3 класса) с per-ячейкой deficit_index.
 *
 * Сортировка DESC по meanDeficitIndex (сильнейший дефицит сверху, как в ранкинге);
 * классы без obj_class игнорируются (нечего агрегировать — НЕ фабрикуем класс).
 * Класс, у которого ни у одной ячейки нет индекса → meanDeficitIndex=null (фронт
 * рисует «тонкие данные», НЕ 0).
 */
export function aggregateClassDeficits(mix: ProductMixEntry[]): ClassDeficit[] {
  const byClass = new Map<string, number[]>();
  for (const m of mix) {
    if (m.obj_class == null) continue;
    const list = byClass.get(m.obj_class) ?? [];
    if (m.deficit_index != null) list.push(m.deficit_index);
    byClass.set(m.obj_class, list);
  }
  const out: ClassDeficit[] = [];
  for (const [obj_class, values] of byClass) {
    const meanDeficitIndex =
      values.length > 0
        ? values.reduce((a, b) => a + b, 0) / values.length
        : null;
    out.push({ obj_class, meanDeficitIndex, nWithIndex: values.length });
  }
  // DESC по среднему дефициту; null-индексы — в конец (стабильно). tie-break — класс ASC.
  out.sort((a, b) => {
    const av = a.meanDeficitIndex;
    const bv = b.meanDeficitIndex;
    if (av == null && bv == null) return a.obj_class.localeCompare(b.obj_class);
    if (av == null) return 1;
    if (bv == null) return -1;
    if (bv !== av) return bv - av;
    return a.obj_class.localeCompare(b.obj_class);
  });
  return out;
}

/**
 * Bar width (%) for a signed deficit_index (∈ −1..1) on a 0..100 scale. Uses the
 * magnitude so both недонасыщенность (+) and затоварка (−) read as a filled bar
 * (colour carries the sign via `deficitVariant`). Clamped to [0, 100].
 */
export function deficitBarWidthPct(deficitIndex: number): number {
  const pct = Math.abs(deficitIndex) * 100;
  return Math.max(0, Math.min(100, pct));
}

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

export const CONFIDENCE_RU: Record<ConfidenceLevel, string> = {
  high: "высокая",
  medium: "средняя",
  low: "низкая",
};

export function confidenceVariant(level: ConfidenceLevel): BadgeVariant {
  if (level === "high") return "success";
  if (level === "medium") return "warning";
  return "danger";
}

/**
 * #1926 — качественная полуширина confidence-band для deficit-графика (ось [−1, 1]).
 *
 * В §22-payload НЕТ численных квантилей (p25/p75) — только качественный уровень
 * уверенности per-горизонт (`DemandSupplyForecast.confidence` ∈ high|medium|low).
 * ui-conventions требует показывать прогноз с интервалом (точка без диапазона
 * «слишком уверенно»). Здесь строим band, ширина которого ОТРАЖАЕТ уверенность,
 * а НЕ выдуманные квантили: ниже уверенность → шире band. Значения — доли шкалы
 * дефицита (span 2): high ±0.10, medium ±0.20, low ±0.32. Band подписан в
 * легенде/тултипе как качественный (по уровню уверенности), не как p25/p75.
 */
export const CONFIDENCE_BAND_HALFWIDTH: Record<ConfidenceLevel, number> = {
  high: 0.1,
  medium: 0.2,
  low: 0.32,
};

export function confidenceBandHalfWidth(level: ConfidenceLevel): number {
  return CONFIDENCE_BAND_HALFWIDTH[level];
}

// ── Scoring (§13.6 — product scores #985 + special indices #986) ───────────────

/**
 * Product-score semantics (HARD): every §14.2 score is ∈ [0,1] with «выше =
 * лучше для девелопера» (risk scores supply_risk / future_competition /
 * mortgage_sensitivity are pre-inverted at the source — so score>0.5 =
 * favourable). The backend anchors quality on 0.5 = баланс/midpoint
 * (`product_scoring.py:127` `_MARKET_FIT_MIDPOINT = 0.5`; inverted-risk reasons
 * flip favourable/unfavourable at risk<0.5 → score>0.5), so the gradient is
 * anchored on 0.5 too: a narrow ±0.05 neutral band straddles the balance point —
 * ≥0.55 success / 0.45–0.55 warning / <0.45 danger. This is NOT the deficit
 * mapping (signed [−1,1]) — do not reuse deficitVariant here.
 */
export const SCORE_GOOD_THRESHOLD = 0.55;
export const SCORE_WEAK_THRESHOLD = 0.45;

export function scoreVariant(value: number): BadgeVariant {
  if (value >= SCORE_GOOD_THRESHOLD) return "success";
  if (value >= SCORE_WEAK_THRESHOLD) return "warning";
  return "danger";
}

/**
 * Bar width (%) for a 0..1 score on a 0..100 scale (direct, no magnitude trick —
 * the score is already unsigned 0..1). Clamped to [0, 100].
 */
export function scoreBarWidthPct(value: number): number {
  return Math.max(0, Math.min(100, value * 100));
}

/**
 * RU labels for the 10 §14.2 product-score keys (#985). The MD/PDF exporters
 * render the raw English key — these are the established in-app RU names, kept
 * here so the block and any future reuse share one source of truth. Unknown
 * keys fall back to the raw key (graceful).
 */
export const PRODUCT_SCORE_RU: Record<string, string> = {
  market_fit: "Соответствие рынку",
  demand: "Спрос",
  // #1963: «Риск …» сбивал (скор инвертирован, выше=лучше) → «Запас по предложению».
  supply_risk: "Запас по предложению",
  future_competition: "Будущая конкуренция",
  price_feasibility: "Доступность цены",
  infra_fit: "Инфраструктура",
  mortgage_sensitivity: "Чувствительность к ставке",
  differentiation: "Дифференциация",
  commercial: "Коммерция",
  confidence: "Надёжность данных",
};

/**
 * RU labels for the 6 §25 special-index keys (#986). The per-entry `label` field
 * is a value-descriptor («6 мес» / «Комфорт»), not a metric name, so it is shown
 * as supplementary context — these are the metric names. Unknown keys fall back
 * to the raw key (graceful).
 */
export const SPECIAL_INDEX_RU: Record<string, string> = {
  launch_window: "Окно запуска",
  product_void: "Белые пятна продукта",
  cannibalization: "Каннибализация",
  competitor_strength: "Сила конкурентов",
  artificial_demand: "Искусственный спрос",
  cost_of_error: "Цена ошибки",
};

/**
 * #1963 — однострочное «что это + куда лучше» для 6 спец-индексов. Раньше индексы
 * показывались только сырым именем + числом без объяснения, что значит «выше» —
 * а направление у них РАЗНОЕ (часть растёт к лучшему, часть к худшему). Здесь
 * проговариваем смысл и желаемую сторону. Источник правды до появления поля от
 * бэкенда — этот map (graceful: незнакомый ключ → пусто, строка просто без подписи).
 */
export const SPECIAL_INDEX_DESC: Record<string, string> = {
  product_void:
    "Доля незакрытых ниш (форматов с дефицитом) — выше лучше: больше места для нового продукта.",
  cost_of_error:
    "Цена ошибки выхода = риск затоварки × средний чек лота — ниже лучше: дешевле ошибиться.",
  launch_window:
    "Лучший момент для старта по горизонтам прогноза — раньше окно, тем лучше.",
  cannibalization:
    "Насколько проект отъедает спрос у соседних ЖК того же класса — ниже лучше.",
  artificial_demand:
    "Доля спроса, держащегося на льготной ипотеке (уязвима к смене ставки) — ниже лучше.",
  competitor_strength:
    "Сила топ-конкурентов рядом — ниже лучше: слабее конкуренты, проще выйти.",
};

/**
 * #1963 — вынести §-ссылки («§10.4», «§9.6», «§16») из основного reason в отдельную
 * деталь/тултип. Возвращает {clean, refs}: `clean` — текст без §-маркеров и пустых
 * скобок, `refs` — извлечённые ссылки (через запятую) для tooltip. PURE.
 */
export function stripSectionRefs(reason: string): {
  clean: string;
  refs: string | null;
} {
  // Match «§9.6» / «§10.4» / «§13.6a» but NOT the sentence-final dot after it.
  // Trailing letter only when the ref ends at a separator/EOL (не «съедает» букву
  // следующего слова, если § приклеена без пробела).
  const SECTION_RE = /§\s*\d+(?:\.\d+)*(?:[a-zа-я](?=$|[\s.,;)]))?/gi;
  const refs = reason.match(SECTION_RE) ?? [];
  let clean = reason.replace(SECTION_RE, "");
  // Подчистить осиротевшие скобки/запятые/двойные пробелы после выреза ссылки.
  clean = clean
    .replace(/\(\s*[,;]?\s*\)/g, "")
    .replace(/,\s*\)/g, ")")
    .replace(/\(\s+/g, "(")
    .replace(/\s+\)/g, ")")
    .replace(/\s{2,}/g, " ")
    .replace(/\s+([.,;)])/g, "$1")
    .trim();
  return { clean, refs: refs.length > 0 ? refs.join(", ") : null };
}

// ── Future supply (§9.3 — 6.6 evidence panel) ──────────────────────────────────

/**
 * Does the §9.3 future-supply payload carry a GENUINELY-COMPUTED pressure signal
 * worth rendering the supply panel for? Single source of truth — gates both the
 * 6.6 block's panel and Section6Forecast's 6.6 sub-block (so they can't drift).
 *
 * Gates on the `_round_or_none` metrics ONLY (index / months_of_pressure /
 * future_units_by_horizon / monthly_absorption_units) — DELIBERATELY excludes
 * `open_units` / `hidden_units`. Those are integer stocks that are 0-NOT-NULL at
 * the source: when `supply_layers` is empty (e.g. before the Monday 06:00 worker
 * loads it) the SQL returns 0 rows → open/hidden = 0 while every computed metric
 * is null. Triggering on open/hidden would render «Открытый сток: 0 ед. / Скрытый
 * запас: 0 ед.», falsely reading as "zero future supply = safe" when the truth is
 * "supply data not loaded yet" (null ≠ 0). Gating on the computed metrics keeps
 * the panel OFF until a real pressure index exists; open/hidden are still DISPLAYED
 * once it does (a genuine 0 then is honest).
 */
export function hasSupplySignal(fs: FutureSupply | null): boolean {
  if (fs == null) return false;
  const b = fs.breakdown;
  return (
    fs.index != null ||
    b.months_of_pressure != null ||
    b.future_units_by_horizon != null ||
    b.monthly_absorption_units != null
  );
}

// ── Number formatting ─────────────────────────────────────────────────────────

/**
 * Fixed-decimal RU number (тонкая неразрывная группировка тысяч). Нормализует
 * ведущий знак минуса в Unicode «−» (U+2212) per ui-microcopy (≥ ≤ ± Unicode).
 * `.replace` бьёт только первое вхождение = ведущий знак (RU-группировка — NBSP,
 * не дефис), так что числовая часть не затрагивается.
 */
export function fmtNum(value: number, digits = 1): string {
  return value
    .toLocaleString("ru", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    })
    .replace("-", "−");
}
