"use client";

/**
 * ForecastChart — §22 deficit-index trajectory across horizons × scenarios.
 *
 * Renders ABOVE the 6.1 horizons table (chart-then-table) on the Site Finder
 * analysis screen. Visualises what the 6.1 / 6.2 tables show as numbers:
 *
 *   • Primary (left y-axis «Индекс дефицита», ~[-1,1]): one line per scenario
 *     (base / aggressive / conservative), deficit_index at each horizon.
 *     Colours from --viz-1..3 IN ORDER (base = viz-1). Falls back to a single
 *     «Дефицит» line from future_market.forecasts_by_horizon if no scenarios.
 *   • Secondary (right y-axis «Ставка, % годовых»): a dashed key-rate path
 *     (prediction → dashed per ui-conventions). Single line (base scenario, or
 *     the fallback forecast) to keep the chart readable — 3 rate lines clutter.
 *   • markLine (vertical) at the target horizon, labelled «целевой».
 *   • markLine (horizontal) at deficit = 0 («баланс») so the sign reads:
 *     >0 недонасыщенность, <0 затоварка.
 *
 * Lazy-mount + echarts import are handled by ChartShell — we only build a typed
 * `option` and hand it over (no echarts import in this file).
 *
 * Deficit semantics (HARD, see forecast-helpers): >0 недонасыщенность,
 * <0 затоварка, ≈0 баланс.
 */

import { useMemo } from "react";

import { ChartShell } from "@/components/analytics/ChartShell";
import type {
  DemandSupplyForecast,
  ForecastReport,
  ScenarioKey,
} from "@/types/forecast";

import { fmtNum } from "./forecast-helpers";

interface Props {
  report: ForecastReport;
  /** Horizon (мес) to emphasise — the user's HorizonSelector choice wins. */
  selectedHorizon?: number;
}

// Scenario render order + RU labels. viz-1 is always the focused/own series, so
// base (the default expectation) takes viz-1, then aggressive, then conservative.
const SCENARIO_ORDER: ScenarioKey[] = ["base", "aggressive", "conservative"];

const SCENARIO_RU: Record<ScenarioKey, string> = {
  base: "База",
  aggressive: "Агрессивный",
  conservative: "Консервативный",
};

// Series colours — ONLY from --viz-1..3 in order (documented hardcode exception
// for ECharts series colours; values mirror ui-tokens.md).
const VIZ: Record<ScenarioKey, string> = {
  base: "#1D4ED8", // --viz-1
  aggressive: "#0EA5E9", // --viz-2
  conservative: "#14B8A6", // --viz-3
};

// Single-line fallback colour (ui-tokens: одиночная линия — #374151).
const SINGLE_LINE = "#374151";
// Prediction / forecast (dashed) — --prediction-line.
const PREDICTION_LINE = "#0EA5E9";
// Axis chrome.
const FG_SECONDARY = "#5B6066";
const BORDER_STRONG = "#D1D5DB";

// ── Tooltip row type (echarts-for-react gives loose params; narrow ourselves) ──
interface TooltipParam {
  seriesName?: string;
  value?: number | null;
  axisValueLabel?: string;
  marker?: string;
  seriesType?: string;
}

function sortedUniqueHorizons(report: ForecastReport): number[] {
  const set = new Set<number>();
  for (const f of report.future_market.forecasts_by_horizon) {
    set.add(f.horizon_months);
  }
  for (const sc of Object.values(report.scenarios.by_scenario)) {
    for (const f of sc?.forecasts ?? []) set.add(f.horizon_months);
  }
  return Array.from(set).sort((a, b) => a - b);
}

/** deficit_index per horizon for a forecast list, aligned to the horizon axis. */
function deficitByHorizon(
  forecasts: DemandSupplyForecast[],
  horizons: number[],
): (number | null)[] {
  const byH = new Map<number, number>();
  for (const f of forecasts) byH.set(f.horizon_months, f.deficit_index);
  return horizons.map((h) => byH.get(h) ?? null);
}

/** rate_future per horizon for a forecast list, aligned to the horizon axis. */
function rateByHorizon(
  forecasts: DemandSupplyForecast[],
  horizons: number[],
): (number | null)[] {
  const byH = new Map<number, number>();
  for (const f of forecasts) byH.set(f.horizon_months, f.rate_future);
  return horizons.map((h) => byH.get(h) ?? null);
}

export function ForecastChart({ report, selectedHorizon }: Props) {
  const fm = report.future_market;
  const byScenario = report.scenarios.by_scenario;

  const presentScenarios = SCENARIO_ORDER.filter(
    (k) => (byScenario[k]?.forecasts.length ?? 0) > 0,
  );
  const hasScenarios = presentScenarios.length > 0;
  const hasHorizons = fm.forecasts_by_horizon.length > 0;

  // Target horizon: user's selector wins, else future_supply, else 12.
  const targetHorizon =
    selectedHorizon ?? fm.future_supply?.horizon_months ?? 12;

  const option = useMemo<Record<string, unknown>>(() => {
    const horizons = sortedUniqueHorizons(report);
    const xLabels = horizons.map((h) => `${h} мес`);

    // ── Deficit series ──────────────────────────────────────────────────────
    // Per-scenario lines when available, else a single fallback line.
    const deficitSeries: Record<string, unknown>[] = hasScenarios
      ? presentScenarios.map((key) => ({
          name: SCENARIO_RU[key],
          type: "line",
          yAxisIndex: 0,
          data: deficitByHorizon(byScenario[key]?.forecasts ?? [], horizons),
          symbol: "circle",
          symbolSize: 6,
          smooth: false,
          connectNulls: true,
          lineStyle: { color: VIZ[key], width: 2 },
          itemStyle: { color: VIZ[key] },
          emphasis: { focus: "series" },
          z: 3,
        }))
      : [
          {
            name: "Дефицит",
            type: "line",
            yAxisIndex: 0,
            data: deficitByHorizon(fm.forecasts_by_horizon, horizons),
            symbol: "circle",
            symbolSize: 6,
            smooth: false,
            connectNulls: true,
            lineStyle: { color: SINGLE_LINE, width: 2 },
            itemStyle: { color: SINGLE_LINE },
            z: 3,
          },
        ];

    // ── Rate path (secondary axis, dashed = prediction) ─────────────────────
    // One line only (base scenario preferred, else fallback forecast) — three
    // rate lines clutter the deficit story.
    const rateSource: DemandSupplyForecast[] =
      byScenario.base?.forecasts ??
      (hasScenarios
        ? (byScenario[presentScenarios[0]]?.forecasts ?? [])
        : fm.forecasts_by_horizon);
    const rateData = rateByHorizon(rateSource, horizons);
    const hasRate = rateData.some((v) => v != null);

    const rateSeries: Record<string, unknown>[] = hasRate
      ? [
          {
            name: "Ставка",
            type: "line",
            yAxisIndex: 1,
            data: rateData,
            symbol: "emptyCircle",
            symbolSize: 5,
            smooth: false,
            connectNulls: true,
            // Forecast / prediction → dashed (ui-conventions HARD).
            lineStyle: { color: PREDICTION_LINE, width: 2, type: "dashed" },
            itemStyle: { color: PREDICTION_LINE },
            z: 2,
          },
        ]
      : [];

    // ── markLines: vertical «целевой» + horizontal «баланс» (deficit = 0) ────
    // Attached to the first deficit series so they paint once.
    const targetLabel = xLabels[horizons.indexOf(targetHorizon)];
    const markLineData: Record<string, unknown>[] = [
      {
        yAxis: 0,
        label: {
          formatter: "баланс",
          position: "insideEndTop",
          color: FG_SECONDARY,
          fontSize: 11,
        },
        lineStyle: { color: BORDER_STRONG, type: "solid", width: 1 },
      },
    ];
    if (targetLabel != null) {
      markLineData.push({
        xAxis: targetLabel,
        label: {
          formatter: "целевой",
          color: FG_SECONDARY,
          fontSize: 11,
        },
        lineStyle: { color: BORDER_STRONG, type: "dashed", width: 1 },
      });
    }
    const firstDeficit = deficitSeries[0] as Record<string, unknown>;
    firstDeficit.markLine = {
      silent: true,
      symbol: "none",
      data: markLineData,
    };

    const yDeficit: Record<string, unknown> = {
      type: "value",
      name: "Индекс дефицита",
      position: "left",
      min: -1,
      max: 1,
      interval: 0.5,
      nameTextStyle: { color: FG_SECONDARY, fontSize: 11, align: "left" },
      axisLabel: {
        color: FG_SECONDARY,
        formatter: (v: number) => fmtNum(v, 1),
      },
    };

    const yAxis: Record<string, unknown>[] = [yDeficit];
    if (hasRate) {
      yAxis.push({
        type: "value",
        name: "Ставка, % годовых",
        position: "right",
        scale: true,
        nameTextStyle: { color: FG_SECONDARY, fontSize: 11, align: "right" },
        axisLabel: {
          color: FG_SECONDARY,
          formatter: (v: number) => `${fmtNum(v, 0)} %`,
        },
        splitLine: { show: false },
      });
    }

    return {
      tooltip: {
        trigger: "axis",
        axisPointer: { type: "cross" },
        // Units MANDATORY: «−0.42» for deficit, «14.5 %» for ставка.
        formatter: (raw: unknown) => {
          const params = Array.isArray(raw) ? (raw as TooltipParam[]) : [];
          if (params.length === 0) return "";
          const header = params[0]?.axisValueLabel ?? "";
          const lines = params
            .filter((p) => p.value != null)
            .map((p) => {
              const v = p.value as number;
              const isRate = p.seriesName === "Ставка";
              const text = isRate ? `${fmtNum(v, 2)} %` : fmtNum(v, 2);
              return `${p.marker ?? ""}${p.seriesName}: <b>${text}</b>`;
            });
          return [header, ...lines].join("<br/>");
        },
        textStyle: { fontSize: 12 },
      },
      legend: {
        top: 0,
        textStyle: { color: FG_SECONDARY, fontSize: 12 },
      },
      grid: { left: 56, right: hasRate ? 64 : 24, top: 40, bottom: 32 },
      xAxis: {
        type: "category",
        data: xLabels,
        boundaryGap: false,
        axisLabel: { color: FG_SECONDARY },
        axisLine: { lineStyle: { color: BORDER_STRONG } },
      },
      yAxis,
      series: [...deficitSeries, ...rateSeries],
    };
  }, [
    report,
    fm,
    byScenario,
    hasScenarios,
    presentScenarios,
    targetHorizon,
  ]);

  // Thin reports (no horizons AND no scenarios) → render nothing.
  if (!hasHorizons && !hasScenarios) return null;

  return <ChartShell option={option} height={300} notMerge />;
}
