import axios from 'axios'
import {
  ArrowDownRight,
  ArrowUpRight,
  CloudSun,
  Loader2,
  RefreshCw,
  Sparkles,
  Upload,
} from 'lucide-react'
import { toast } from 'sonner'
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import {
  Area,
  Bar,
  CartesianGrid,
  Cell,
  ComposedChart,
  Line,
  ReferenceArea,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from 'recharts'

import {
  getCurrentPlan,
  getPlanCompare,
  postImportSitePrices,
  postRunForecast,
  postRunPlan,
} from '../api/backend'
import { floorSlotUtcMs, SLOT_MS } from '../components/charts/chartConstants'
import { useIsCoarsePointer } from '../hooks/useMediaQuery'
import { useSiteStatus } from '../hooks/useSiteStatus'
import {
  maskForInterval,
  parsePlanSolverSnapshot,
  type PlanMaskSlot,
  type PlanSolverSnapshot,
} from '../lib/planSolverSnapshot'
import type {
  CurrentPlanResponse,
  PlanningCompareResponse,
  PlanningIntervalDto,
} from '../types/plan'

const TZ = 'Europe/Prague'

function formatLocal(iso: string): string {
  return new Date(iso).toLocaleString('cs-CZ', {
    timeZone: TZ,
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function formatLocalTime(iso: string): string {
  return new Date(iso).toLocaleTimeString('cs-CZ', {
    timeZone: TZ,
    hour: '2-digit',
    minute: '2-digit',
  })
}

function pragueYmd(d: Date): string {
  return new Intl.DateTimeFormat('sv-SE', {
    timeZone: TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(d)
}

function slotStartUtcMs(iso: string): number {
  return new Date(iso).getTime()
}

const PREDICTED_LEAD_MS = 36 * 60 * 60 * 1000
const MAX_FUTURE_SLOTS = 384

function pragueDayKey(iso: string): string {
  return new Intl.DateTimeFormat('sv-SE', {
    timeZone: TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(new Date(iso))
}

function formatPragueDateLabel(iso: string): string {
  return new Date(iso).toLocaleDateString('cs-CZ', {
    timeZone: TZ,
    weekday: 'short',
    day: 'numeric',
    month: 'numeric',
    year: 'numeric',
  })
}

function isPredictedPriceSlot(i: PlanningIntervalDto, nowMs: number): boolean {
  if (i.is_predicted_price === true) return true
  if (i.is_predicted_price === false) return false
  return slotStartUtcMs(i.interval_start) > nowMs + PREDICTED_LEAD_MS
}

function groupByDay(slots: PlanningIntervalDto[]): Record<string, PlanningIntervalDto[]> {
  return slots.reduce(
    (acc, slot) => {
      const day = pragueDayKey(slot.interval_start)
      if (!acc[day]) acc[day] = []
      acc[day].push(slot)
      return acc
    },
    {} as Record<string, PlanningIntervalDto[]>,
  )
}

function dayStats(
  slots: PlanningIntervalDto[],
  nowMs: number,
): {
  fveKwh: number
  exportKwh: number
  avgBuy: number | null
} {
  const slotHours = SLOT_MS / 3_600_000
  let fveWh = 0
  let expWh = 0
  const buys: number[] = []
  for (const s of slots) {
    const fveW = slotFveDisplayW(s, nowMs)
    fveWh += (fveW ?? 0) * slotHours
    const gw = s.grid_setpoint_w ?? 0
    if (gw < 0) expWh += -gw * slotHours
    if (s.effective_buy_price != null) buys.push(s.effective_buy_price)
  }
  const avgBuy = buys.length ? buys.reduce((a, b) => a + b, 0) / buys.length : null
  return { fveKwh: fveWh / 1000, exportKwh: expWh / 1000, avgBuy }
}

type HorizonHours = 24 | 48 | 96

type PlanTableRow =
  | {
      kind: 'summary'
      dayKey: string
      dateLabel: string
      fveKwh: number
      exportKwh: number
      avgBuy: number | null
    }
  | { kind: 'slot'; i: PlanningIntervalDto }

function buildPlanTableRows(
  visibleSlots: PlanningIntervalDto[],
  nowMs: number,
): PlanTableRow[] {
  const groups = groupByDay(visibleSlots)
  const dayKeys = [...new Set(visibleSlots.map((s) => pragueDayKey(s.interval_start)))].sort()
  const rows: PlanTableRow[] = []
  for (const dk of dayKeys) {
    const sl = groups[dk]
    if (!sl?.length) continue
    rows.push({
      kind: 'summary',
      dayKey: dk,
      dateLabel: formatPragueDateLabel(sl[0]!.interval_start),
      ...dayStats(sl, nowMs),
    })
    for (const i of sl) rows.push({ kind: 'slot', i })
  }
  return rows
}

function hasGenCutoff(slots: PlanningIntervalDto[]): boolean {
  return slots.some((s) => s.deye_gen_cutoff_enabled != null)
}

function horizonToggleClass(active: boolean): string {
  return active
    ? 'border-cyan-600 bg-cyan-950/50 text-cyan-100'
    : 'border-slate-600 bg-slate-800/80 text-slate-300 hover:bg-slate-800'
}

/**
 * Budoucí slot: `pv_forecast_total_w` z /plan/current je raw z forecast_pv_interval; pro zobrazení
 * preferujeme korekci z `pv-slots-corrected` (soulad s LP vstupy a přehledem).
 * Pokud je hodnota null (data chybí), proxy z ceny nákupu (W) jen u grafu přes pvAProxyW.
 */
/** Slot jen z řady forecast (za horizontem planning_interval) — doplnění grafu. */
function isForecastExtensionInterval(i: PlanningIntervalDto): boolean {
  return (
    i.battery_setpoint_w == null &&
    i.grid_setpoint_w == null &&
    i.expected_cost_czk == null
  )
}

function pvAProxyW(i: PlanningIntervalDto): number {
  const pv = i.pv_forecast_total_w
  if (pv != null && pv > 0) return pv
  if (pv === 0) return 0
  const buy = i.effective_buy_price
  if (buy == null) return 0
  const w = 6000 - buy * 3500
  return Math.max(0, Math.min(15000, w))
}

/** Křivka FVE ve grafu: korig. / audit, jinak stejná cena-proxy jako dřív. */
function pvChartFveW(i: PlanningIntervalDto, nowMs: number): number {
  const w = slotFveDisplayW(i, nowMs)
  if (w != null && Number.isFinite(w)) return w
  return pvAProxyW(i)
}

/** Budoucí slot (od začátku ještě nenastal): korig. předpověď; proběhlý / probíhající: telemetrie z auditu. */
function slotFveDisplayW(i: PlanningIntervalDto, nowMs: number): number | null {
  const start = slotStartUtcMs(i.interval_start)
  const future = start >= nowMs
  if (future) {
    const f = i.pv_forecast_total_w
    return f != null ? Number(f) : null
  }
  const a = i.pv_power_w
  if (a != null) return Number(a)
  const f = i.pv_forecast_total_w
  return f != null ? Number(f) : null
}

/** Stejná idea jako výkonové buňky: velké hodnoty v kW, jinak W (bez suffixu u malých čísel jako Bat. W). */
function formatPlanPowerW(w: number | null): string {
  if (w == null || Number.isNaN(w)) return '—'
  const v = Math.round(Number(w))
  if (Math.abs(v) >= 1000) {
    const k = v / 1000
    const s = k.toFixed(1).replace(/\.0$/, '')
    return `${s} kW`
  }
  return String(v)
}

function FveWCell({
  i,
  nowMs,
}: {
  i: PlanningIntervalDto
  nowMs: number
}) {
  const w = slotFveDisplayW(i, nowMs)
  const color =
    w == null || Number.isNaN(w) ? 'text-slate-500' : w > 0 ? 'text-emerald-400' : 'text-slate-500'
  return (
    <td className={`pr-2 font-mono tabular-nums ${color}`}>{formatPlanPowerW(w)}</td>
  )
}

function VynosKcCell({ v }: { v: number | null | undefined }) {
  if (v == null || Number.isNaN(Number(v))) {
    return <td className="pr-2 font-mono tabular-nums text-slate-500">—</td>
  }
  const n = Number(v)
  const color = n < 0 ? 'text-emerald-400' : n > 0 ? 'text-red-400' : 'text-slate-500'
  return (
    <td className={`pr-2 font-mono tabular-nums ${color}`}>{n.toFixed(4)}</td>
  )
}

function runTypeBadgeClass(t: string): string {
  const u = t.toLowerCase()
  if (u === 'daily') return 'bg-sky-500/15 text-sky-300 ring-1 ring-sky-500/35'
  if (u === 'rolling') return 'bg-violet-500/15 text-violet-300 ring-1 ring-violet-500/35'
  if (u === 'manual') return 'bg-amber-500/15 text-amber-200 ring-1 ring-amber-500/35'
  return 'bg-slate-600/40 text-slate-300 ring-1 ring-slate-500/30'
}

function axiosDetail(e: unknown): string {
  if (axios.isAxiosError(e)) {
    const d = e.response?.data as { detail?: unknown } | undefined
    const detail = d?.detail
    if (typeof detail === 'string') return detail
    if (Array.isArray(detail)) {
      return detail
        .map((x: { msg?: string }) => (typeof x?.msg === 'string' ? x.msg : ''))
        .filter(Boolean)
        .join(', ')
    }
  }
  return e instanceof Error ? e.message : 'Neznámá chyba'
}

/** Zrcadlí logiku TOU řádků z `write_inverter_setpoints` (PASSIVE/SELL/CHARGE) pro jeden plánovací interval. */
function deyeSetpointLabel(i: PlanningIntervalDto): string {
  const battery_w = i.battery_setpoint_w ?? 0
  const grid_w = i.grid_setpoint_w ?? 0
  const exportLimitW = i.export_limit_w ?? 0
  const tgt = i.battery_soc_target_pct
  const targetSoc = tgt != null ? Math.min(95, Math.round(Number(tgt))) : 80

  const fmtKw = (w: number) => {
    const k = Math.abs(w) / 1000
    const s = k.toFixed(1).replace(/\.0$/, '')
    return `${s}kW`
  }

  const pm = (i.deye_physical_mode ?? '').toString().trim().toUpperCase()
  if (pm === 'SELL') {
    const tpPowerW = Math.abs(battery_w)
    const cap = exportLimitW > 0 ? ` | cap ${fmtKw(exportLimitW)}` : ''
    return `SELL | ⬇ ${fmtKw(tpPowerW)}${cap} | reg142=0 reg178=32`
  }
  if (pm === 'CHARGE') {
    return `CHARGE | ⬆ ${fmtKw(Math.max(0, battery_w))} | grid=yes | SOC→${targetSoc}%`
  }

  // PASSIVE (ZERO): doplň informaci o variantě 108/109 podle pravidel (bez wattových prahů).
  if (grid_w < 0 && battery_w >= 0) {
    const cap = exportLimitW > 0 ? ` | cap ${fmtKw(exportLimitW)}` : ''
    return `PASSIVE | FVE→síť${cap} (108=0)`
  }
  if (grid_w > 0 && battery_w <= 0) return 'PASSIVE | držet bat. (109=0)'
  return 'PASSIVE | max/max'
}

function deyeModeBadge(i: PlanningIntervalDto): { label: string; klass: string; title: string } {
  const m = (i.deye_physical_mode ?? 'PASSIVE').toString().trim().toUpperCase()
  const battery_w = i.battery_setpoint_w ?? 0
  const grid_w = i.grid_setpoint_w ?? 0
  const exportLimitW = i.export_limit_w ?? 0
  const exportMode = (i.export_mode ?? 'NONE').toString().trim().toUpperCase()
  const cap = exportLimitW > 0 ? `; hard cap ${formatPlanPowerW(exportLimitW)}` : ''

  if (m === 'SELL') {
    return {
      label: 'SELL',
      klass: 'bg-orange-500/15 text-orange-200 ring-1 ring-orange-500/35',
      title: `SELL (selling first): reg142=0, reg178=32 (grid peak shaving off)${cap}`,
    }
  }
  if (m === 'CHARGE') {
    return {
      label: 'CHARGE',
      klass: 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/35',
      title: 'CHARGE (grid charge): TOU grid_charge enabled v time pointech; reg178=48',
    }
  }

  let variant = 'max/max'
  if (exportMode === 'PV_SURPLUS' && grid_w < 0) {
    variant = 'FVE→síť'
  } else if (grid_w < 0 && battery_w >= 0) {
    variant = exportMode === 'PV_SURPLUS' ? 'FVE→síť' : 'export'
  }
  else if (grid_w > 0 && battery_w <= 0) variant = 'držet bat. (109=0)'
  return {
    label: 'PASSIVE',
    klass: 'bg-slate-600/40 text-slate-200 ring-1 ring-slate-500/30',
    title: `PASSIVE (ZERO): ${variant}${cap}; reg142=deye_zero_export_mode; reg178=48`,
  }
}

function genCutoffBadge(i: PlanningIntervalDto): { show: boolean; label: string; klass: string; title: string } {
  // Nevizualizovat na site bez GEN cut-off (null/undefined ve všech slotech).
  if (i.deye_gen_cutoff_enabled == null) {
    return { show: false, label: '', klass: '', title: '' }
  }
  if (i.deye_gen_cutoff_enabled === true) {
    return {
      show: true,
      label: 'GEN CUT',
      klass: 'bg-red-500/15 text-red-200 ring-1 ring-red-500/35',
      title: 'GEN port cut-off (BA81): reg178 bits0-1=3 (MI export cutoff ON)',
    }
  }
  return {
    show: true,
    label: 'GEN OK',
    klass: 'bg-slate-700/30 text-slate-400 ring-1 ring-slate-600/30',
    title: 'GEN port připojen (cut-off OFF)',
  }
}

function tableRowClass(
  i: PlanningIntervalDto,
  selected: boolean,
  mask: PlanMaskSlot | null,
): string {
  const parts: string[] = []
  if (selected) parts.push('ring-1 ring-inset ring-cyan-500/50 bg-cyan-950/25')
  const buy = i.effective_buy_price
  const sell = i.effective_sell_price
  if (buy != null && buy < 0) parts.push('bg-green-950/80')
  else if (sell != null && sell < 0) parts.push('bg-red-950/80')
  if ((i.pv_a_curtailed_w ?? 0) > 0) parts.push('border-l-4 border-l-yellow-500')
  else if (mask?.neg_sell_phase === 'prep' || mask?.neg_sell_phase === 'tail') {
    parts.push('border-l-4 border-l-violet-600/70')
  }
  else if (mask?.allow_charge) parts.push('border-l-4 border-l-emerald-600/70')
  else if (mask?.allow_discharge_export) parts.push('border-l-4 border-l-orange-500/70')
  return parts.join(' ')
}

function pvAAllowedW(i: PlanningIntervalDto): number | null {
  const fc = i.pv_a_forecast_solver_w
  if (fc == null) return null
  return Math.max(0, fc - (i.pv_a_curtailed_w ?? 0))
}

function negSellPhaseBadge(mask: PlanMaskSlot | null): {
  label: string
  klass: string
  title: string
} | null {
  const p = mask?.neg_sell_phase
  if (p == null || p === 'none') return null
  if (p === 'prep') {
    return {
      label: 'sell− prep',
      klass: 'bg-violet-500/20 text-violet-100 ring-1 ring-violet-500/40',
      title: 'Fáze přípravy SoC v okně záporného výkupu (cíl z DB, typ. 80 %)',
    }
  }
  return {
    label: 'sell− tail',
    klass: 'bg-fuchsia-500/20 text-fuchsia-100 ring-1 ring-fuchsia-500/40',
    title: 'Závěr okna sell<0: rampa na plné SoC, volitelný ventil pole B',
  }
}

function exportModeBadge(i: PlanningIntervalDto): {
  label: string
  klass: string
  title: string
} {
  const m = (i.export_mode ?? 'NONE').toString().trim().toUpperCase()
  const cap =
    i.export_limit_w != null && i.export_limit_w > 0
      ? ` · limit ${formatPlanPowerW(i.export_limit_w)}`
      : ''
  if (m === 'BATTERY_SELL') {
    return {
      label: 'BAT→síť',
      klass: 'bg-orange-500/20 text-orange-100 ring-1 ring-orange-500/40',
      title: `Export z baterie do sítě (BATTERY_SELL)${cap}`,
    }
  }
  if (m === 'PV_SURPLUS') {
    return {
      label: 'FVE→síť',
      klass: 'bg-amber-500/15 text-amber-100 ring-1 ring-amber-500/35',
      title: `Export přebytku FVE (PV_SURPLUS)${cap}`,
    }
  }
  return {
    label: '—',
    klass: 'text-slate-600',
    title: `Bez exportu do sítě (NONE)${cap}`,
  }
}

function MaskIconsCell({ mask }: { mask: PlanMaskSlot | null }) {
  if (mask == null) {
    return <td className="pr-2 text-slate-600">—</td>
  }
  return (
    <td className="pr-2">
      <span className="inline-flex items-center gap-1.5 font-mono text-[10px]">
        <span
          className={
            mask.allow_charge
              ? 'rounded bg-emerald-950/80 px-1 py-0.5 text-emerald-300 ring-1 ring-emerald-700/50'
              : 'rounded px-1 py-0.5 text-slate-600'
          }
          title={mask.allow_charge ? 'Solver: povoleno grid nabíjení' : 'Grid nabíjení zakázáno'}
        >
          ⚡
        </span>
        <span
          className={
            mask.allow_discharge_export
              ? 'rounded bg-orange-950/80 px-1 py-0.5 text-orange-200 ring-1 ring-orange-700/50'
              : 'rounded px-1 py-0.5 text-slate-600'
          }
          title={
            mask.allow_discharge_export
              ? 'Solver: povolen export baterie do sítě'
              : 'Export baterie do sítě zakázán'
          }
        >
          ↓
        </span>
      </span>
    </td>
  )
}

function PlanSolverInsights({ snap }: { snap: PlanSolverSnapshot }) {
  const cutoffLabel = snap.chargeAcquisitionCutoffAt
    ? formatLocal(snap.chargeAcquisitionCutoffAt)
    : '—'
  return (
    <div className="rounded-lg border border-slate-700/80 bg-slate-950/50 p-3 text-sm">
      <p className="mb-2 text-xs font-medium uppercase tracking-wide text-slate-500">
        Solver — masky a arbitráž
      </p>
      <dl className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
        <div>
          <dt className="text-xs text-slate-500">Nákupní cena zásoby (acquisition)</dt>
          <dd className="font-mono text-slate-100">
            {snap.chargeAcquisitionKwh != null
              ? `${snap.chargeAcquisitionKwh.toFixed(3)} Kč/kWh`
              : '—'}
          </dd>
        </div>
        <div>
          <dt className="text-xs text-slate-500">Řez před 1. exportem</dt>
          <dd className="font-mono text-xs text-slate-200">{cutoffLabel}</dd>
        </div>
        <div>
          <dt className="text-xs text-slate-500">Sloty ⚡ nabíjení</dt>
          <dd className="font-mono text-emerald-300">{snap.chargeMaskCount}</dd>
        </div>
        <div>
          <dt className="text-xs text-slate-500">Sloty ↓ export bat.</dt>
          <dd className="font-mono text-orange-300">{snap.exportMaskCount}</dd>
        </div>
      </dl>
      <p className="mt-2 text-[11px] text-slate-500">
        ⚡ = allow_charge · ↓ = allow_discharge_export (z posledního běhu solveru). Zelený/oranžový okraj
        řádku = stejná legenda.
      </p>
    </div>
  )
}

function PlanSlotDetail({
  i,
  mask,
  compare,
  nowMs,
}: {
  i: PlanningIntervalDto
  mask: PlanMaskSlot | null
  compare: PlanningIntervalDto | undefined
  nowMs: number
}) {
  const ex = exportModeBadge(i)
  const spread =
    i.effective_sell_price != null && i.effective_buy_price != null
      ? i.effective_sell_price - i.effective_buy_price
      : null
  return (
    <div className="mt-3 rounded-lg border border-cyan-800/50 bg-cyan-950/20 p-3 text-sm text-slate-200">
      <p className="mb-2 font-medium text-cyan-100">{formatLocal(i.interval_start)}</p>
      <div className="flex flex-wrap gap-2">
        <span className={`inline-flex rounded-md px-2 py-0.5 text-[10px] font-semibold ${ex.klass}`} title={ex.title}>
          {ex.label}
        </span>
        {(() => {
          const b = deyeModeBadge(i)
          return (
            <span className={`inline-flex rounded-md px-2 py-0.5 text-[10px] font-semibold ${b.klass}`} title={b.title}>
              Deye {b.label}
            </span>
          )
        })()}
        {mask?.allow_charge ? (
          <span className="rounded bg-emerald-950/60 px-2 py-0.5 text-[10px] text-emerald-300">⚡ nabíjení OK</span>
        ) : null}
        {mask?.allow_discharge_export ? (
          <span className="rounded bg-orange-950/60 px-2 py-0.5 text-[10px] text-orange-200">↓ export bat. OK</span>
        ) : null}
        {(() => {
          const pb = negSellPhaseBadge(mask)
          return pb ? (
            <span className={`inline-flex rounded-md px-2 py-0.5 text-[10px] font-semibold ${pb.klass}`} title={pb.title}>
              {pb.label}
            </span>
          ) : null
        })()}
      </div>
      <dl className="mt-3 grid gap-2 font-mono text-xs sm:grid-cols-2 lg:grid-cols-3">
        <div>
          <dt className="text-slate-500">Cena kup / prod</dt>
          <dd>
            {i.effective_buy_price?.toFixed(3) ?? '—'} / {i.effective_sell_price?.toFixed(3) ?? '—'}
            {isPredictedPriceSlot(i, nowMs) ? ' (odhad)' : ''}
          </dd>
        </div>
        <div>
          <dt className="text-slate-500">Spread prod−kup</dt>
          <dd className={spread != null && spread > 0 ? 'text-emerald-400' : ''}>
            {spread != null ? `${spread.toFixed(3)} Kč/kWh` : '—'}
          </dd>
        </div>
        <div>
          <dt className="text-slate-500">Bat. / síť / SoC</dt>
          <dd>
            {formatPlanPowerW(i.battery_setpoint_w)} / {formatPlanPowerW(i.grid_setpoint_w)} /{' '}
            {i.battery_soc_target_pct != null ? `${i.battery_soc_target_pct.toFixed(1)} %` : '—'}
          </dd>
        </div>
        <div>
          <dt className="text-slate-500">FVE / dům</dt>
          <dd>
            {formatPlanPowerW(slotFveDisplayW(i, nowMs))} / {formatPlanPowerW(i.load_baseline_w)}
          </dd>
        </div>
        <div>
          <dt className="text-slate-500">Škrcení A / ≈ reg 340</dt>
          <dd>
            {(i.pv_a_curtailed_w ?? 0) > 0 ? (
              <span className="text-yellow-200">
                CURTAIL {(i.pv_a_curtailed_w ?? 0).toLocaleString('cs-CZ')} W
              </span>
            ) : (
              '—'
            )}
            {pvAAllowedW(i) != null ? (
              <span className="ml-2 text-slate-400">
                · povoleno {pvAAllowedW(i)!.toLocaleString('cs-CZ')} W
              </span>
            ) : null}
          </dd>
        </div>
        <div>
          <dt className="text-slate-500">Výnos slotu</dt>
          <dd>{i.expected_cost_czk != null ? `${i.expected_cost_czk.toFixed(4)} Kč` : '—'}</dd>
        </div>
      </dl>
      <p className="mt-2 text-[11px] text-slate-500">{deyeSetpointLabel(i)}</p>
      {compare ? (
        <p className="mt-1 text-[11px] text-amber-200/90">
          Compare: bat. {compare.battery_setpoint_w ?? '—'} W · síť {compare.grid_setpoint_w ?? '—'} W · export{' '}
          {compare.export_mode ?? '—'}
        </p>
      ) : null}
    </div>
  )
}

type ChartMaskBand = { x1: string; x2: string }

function buildChartMaskBands(
  chartRows: ChartRow[],
  snap: PlanSolverSnapshot | null,
  flag: 'allow_charge' | 'allow_discharge_export',
): ChartMaskBand[] {
  if (snap == null || chartRows.length === 0) return []
  const bands: ChartMaskBand[] = []
  let start: string | null = null
  let prev: string | null = null
  for (const row of chartRows) {
    const m = maskForInterval(snap, row.raw.interval_start)
    const on = m != null && m[flag]
    if (on) {
      if (start == null) start = row.label
      prev = row.label
    } else if (start != null && prev != null) {
      bands.push({ x1: start, x2: prev })
      start = null
      prev = null
    }
  }
  if (start != null && prev != null) bands.push({ x1: start, x2: prev })
  return bands
}

type ChartRow = {
  label: string
  ts: number
  pv_a_w: number
  battery_soc_target_pct: number | null
  battery_setpoint_w: number
  grid_setpoint_w: number
  compare_battery_setpoint_w?: number | null
  effective_buy_price: number | null
  raw: PlanningIntervalDto
}

function recordNumber(value: unknown): number | null {
  if (value == null) return null
  const n = Number(value)
  return Number.isFinite(n) ? n : null
}

function recordString(value: unknown): string | null {
  return typeof value === 'string' && value.length > 0 ? value : null
}

type PlanPrepActionsProps = {
  prepAction: null | 'import' | 'forecast' | 'init'
  replanning: boolean
  importDate: 'today' | 'tomorrow'
  onImportDateChange: (v: 'today' | 'tomorrow') => void
  onImport: () => void
  onForecast: () => void
  onInit: () => void
  wrapClassName?: string
}

function PlanPrepActions({
  prepAction,
  replanning,
  importDate,
  onImportDateChange,
  onImport,
  onForecast,
  onInit,
  wrapClassName = 'flex flex-wrap gap-2',
}: PlanPrepActionsProps) {
  const prepBusy = prepAction !== null
  const dis = prepBusy || replanning
  return (
    <div className={wrapClassName}>
      <button
        type="button"
        onClick={onImport}
        disabled={dis}
        className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-600 bg-slate-800/90 px-3 py-2 text-sm font-medium text-slate-100 transition hover:bg-slate-700 disabled:opacity-50"
      >
        {prepAction === 'import' ? (
          <Loader2 className="h-4 w-4 animate-spin" />
        ) : (
          <Upload className="h-4 w-4" />
        )}
        Importovat ceny
      </button>
      <label className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900/60 px-3 py-2 text-xs text-slate-300">
        Den OTE
        <select
          value={importDate}
          onChange={(e) => onImportDateChange(e.target.value === 'today' ? 'today' : 'tomorrow')}
          disabled={dis}
          className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-xs text-slate-100"
        >
          <option value="today">dnes</option>
          <option value="tomorrow">zítra</option>
        </select>
      </label>
      <button
        type="button"
        onClick={onForecast}
        disabled={dis}
        className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-600 bg-slate-800/90 px-3 py-2 text-sm font-medium text-slate-100 transition hover:bg-slate-700 disabled:opacity-50"
      >
        {prepAction === 'forecast' ? (
          <Loader2 className="h-4 w-4 animate-spin" />
        ) : (
          <CloudSun className="h-4 w-4" />
        )}
        Spustit forecast
      </button>
      <button
        type="button"
        onClick={onInit}
        disabled={dis}
        className="inline-flex items-center justify-center gap-2 rounded-lg border border-emerald-700/60 bg-emerald-900/40 px-3 py-2 text-sm font-medium text-emerald-100 transition hover:bg-emerald-800/50 disabled:opacity-50"
      >
        {prepAction === 'init' ? (
          <Loader2 className="h-4 w-4 animate-spin" />
        ) : (
          <Sparkles className="h-4 w-4" />
        )}
        Inicializovat plán
      </button>
    </div>
  )
}

function PlanTooltip({
  active,
  payload,
  nowMs,
  solverSnap,
}: {
  active?: boolean
  payload?: Array<{ payload: ChartRow }>
  nowMs: number
  solverSnap?: PlanSolverSnapshot | null
}) {
  if (!active || !payload?.length) return null
  const p = payload[0].payload
  const i = p.raw
  const ext = isForecastExtensionInterval(i)
  const buy = i.effective_buy_price
  const sell = i.effective_sell_price
  const pred = isPredictedPriceSlot(i, nowMs)
  const fveStr = formatPlanPowerW(p.pv_a_w)
  const fveDisplay = fveStr === '—' ? '—' : fveStr.includes('kW') ? fveStr : `${fveStr} W`
  const soc = p.battery_soc_target_pct
  const exportLimit = i.export_limit_w
  const exportMode = i.export_mode ?? 'NONE'
  const compareBattery = p.compare_battery_setpoint_w
  const mask = maskForInterval(solverSnap ?? null, i.interval_start)
  return (
    <div className="rounded-lg border border-slate-600 bg-slate-950 px-3 py-2 text-xs text-slate-200 shadow-xl">
      <div className="mb-1 font-medium text-slate-100">{formatLocal(i.interval_start)}</div>
      {ext && (
        <div className="mb-1 text-[10px] font-sans font-normal normal-case text-amber-200/90">
          Mimo uložený horizont plánu — jen předpověď FVE (po korekci delty, stejně jako v přehledu).
        </div>
      )}
      {pred && (
        <div className="mb-1 text-[10px] uppercase tracking-wide text-slate-500">Cena: odhad (predikce)</div>
      )}
      <div className="space-y-0.5 font-mono tabular-nums">
        <div>
          Cena nákup: {buy != null ? `${buy.toFixed(3)} Kč/kWh` : '—'} · prodej:{' '}
          {sell != null ? `${sell.toFixed(3)} Kč/kWh` : '—'}
        </div>
        <div>FVE (korig. předpověď / audit): {fveDisplay}</div>
        {exportMode !== 'NONE' ? (
          <div>
            Export: {exportMode}
            {exportLimit != null ? ` · limit ${formatPlanPowerW(exportLimit)}` : ''}
          </div>
        ) : null}
        {mask ? (
          <div className="text-slate-400">
            Masky: {mask.allow_charge ? '⚡ nabíjení' : '—'} ·{' '}
            {mask.allow_discharge_export ? '↓ export bat.' : '—'}
          </div>
        ) : null}
        <div>SoC cíl: {soc != null && !Number.isNaN(Number(soc)) ? `${Number(soc).toFixed(1)} %` : '—'}</div>
        <div>Dům: {i.load_baseline_w ?? '—'} W</div>
        <div>Baterie: {i.battery_setpoint_w ?? '—'} W</div>
        {compareBattery != null ? <div>Compare baterie: {compareBattery} W</div> : null}
        <div>Síť (čistý EM): {i.grid_setpoint_w ?? '—'} W</div>
        <div>TČ: {i.heat_pump_enabled ? 'zapnuto' : 'vypnuto'}</div>
        <div>
          EV1: {i.ev1_setpoint_w ?? '—'} W · EV2: {i.ev2_setpoint_w ?? '—'} W
        </div>
        <div className="mt-1 border-t border-slate-700 pt-1 text-[10px] font-sans font-normal normal-case text-slate-500">
          Záporná síť = export přes elektroměr (často přebytek FVE). Záporná baterie kryje dům — nemusí jít o prodej
          energie z akumulátoru do sítě.
        </div>
      </div>
    </div>
  )
}

function CenaCell({ i, nowMs }: { i: PlanningIntervalDto; nowMs: number }) {
  const pred = isPredictedPriceSlot(i, nowMs)
  return (
    <td className={`max-w-[200px] pr-2 font-mono text-xs tabular-nums ${pred ? 'text-slate-500' : 'text-slate-300'}`}>
      <span className="inline-flex flex-wrap items-center gap-x-1.5 align-middle">
        {pred && (
          <span
            className="shrink-0 rounded bg-slate-700/70 px-1 py-0.5 text-[10px] font-sans font-semibold uppercase tracking-wide text-slate-400"
            title="Predikovaná cena (mimo přesné OTE)"
          >
            odhad
          </span>
        )}
        <span>
          {i.effective_buy_price != null ? i.effective_buy_price.toFixed(3) : '—'}
          <span className="text-slate-600"> / </span>
          {i.effective_sell_price != null ? i.effective_sell_price.toFixed(3) : '—'}
        </span>
      </span>
    </td>
  )
}

function HorizonToggle({
  value,
  onChange,
  disabled,
}: {
  value: HorizonHours
  onChange: (h: HorizonHours) => void
  disabled?: boolean
}) {
  const opts: HorizonHours[] = [24, 48, 96]
  return (
    <div className="mb-3 flex flex-wrap items-center gap-2">
      <span className="text-xs text-slate-500">Horizont:</span>
      <div className="flex gap-1">
        {opts.map((h) => (
          <button
            key={h}
            type="button"
            disabled={disabled}
            onClick={() => onChange(h)}
            className={`rounded-md border px-2.5 py-1 text-xs font-medium transition disabled:opacity-50 ${horizonToggleClass(value === h)}`}
          >
            {h}h
          </button>
        ))}
      </div>
    </div>
  )
}

export default function Planning() {
  const { site, ready: siteReady } = useSiteStatus()
  const siteId = site?.site_id ?? null
  // Touch zařízení: tooltip grafu na tap (neblokuje scroll), ne hover.
  const isCoarse = useIsCoarsePointer()

  const [data, setData] = useState<CurrentPlanResponse | null>(null)
  const [compareData, setCompareData] = useState<PlanningCompareResponse | null>(null)
  const [loading, setLoading] = useState(true)
  const [compareLoading, setCompareLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [compareError, setCompareError] = useState<string | null>(null)
  const [replanning, setReplanning] = useState(false)
  const [prepAction, setPrepAction] = useState<null | 'import' | 'forecast' | 'init'>(null)
  const [importDate, setImportDate] = useState<'today' | 'tomorrow'>('tomorrow')
  const [selectedStart, setSelectedStart] = useState<string | null>(null)
  const [tableHorizonH, setTableHorizonH] = useState<HorizonHours>(48)
  const [chartHorizonH, setChartHorizonH] = useState<HorizonHours>(48)

  const load = useCallback(async () => {
    if (siteId == null) return
    setLoading(true)
    setCompareLoading(true)
    setError(null)
    setCompareError(null)
    try {
      const [planRes, compareRes] = await Promise.allSettled([
        getCurrentPlan(siteId),
        getPlanCompare(siteId),
      ])

      if (planRes.status === 'fulfilled') {
        setData(planRes.value)
      } else if (axios.isAxiosError(planRes.reason) && planRes.reason.response?.status === 404) {
        setData({ run: null, intervals: [], summary: null })
        setError(null)
      } else {
        throw planRes.reason
      }

      if (compareRes.status === 'fulfilled') {
        setCompareData(compareRes.value)
      } else if (axios.isAxiosError(compareRes.reason) && compareRes.reason.response?.status === 404) {
        setCompareData(null)
      } else {
        setCompareError(axiosDetail(compareRes.reason))
        setCompareData(null)
      }
    } catch (e) {
      if (axios.isAxiosError(e) && e.response?.status === 404) {
        setData({ run: null, intervals: [], summary: null })
        setError(null)
      } else {
        setError(e instanceof Error ? e.message : 'Chyba načtení plánu')
        setData(null)
      }
    } finally {
      setLoading(false)
      setCompareLoading(false)
    }
  }, [siteId])

  useEffect(() => {
    if (siteId != null) void load()
  }, [siteId, load])

  const nowMs = Date.now()
  const slotFloorMs = floorSlotUtcMs(nowMs)

  // PV forecast je kanonicky v /plan/current (DB read-model), takže už netaháme separátní pv-slots-corrected.

  const futureSlots = useMemo(() => {
    if (!data?.intervals?.length) return []
    return data.intervals
      .filter((i) => slotStartUtcMs(i.interval_start) >= slotFloorMs)
      .sort((a, b) => slotStartUtcMs(a.interval_start) - slotStartUtcMs(b.interval_start))
      .slice(0, MAX_FUTURE_SLOTS)
  }, [data?.intervals, slotFloorMs])

  const visibleSlots = useMemo(() => {
    const endMs = nowMs + tableHorizonH * 60 * 60 * 1000
    return futureSlots.filter((s) => slotStartUtcMs(s.interval_start) <= endMs)
  }, [futureSlots, nowMs, tableHorizonH])

  /** Graf: sloty z /plan/current (obsahují i forecast-only řádky za horizontem LP). */
  const chartMergedSlots = useMemo(() => {
    return [...futureSlots].sort((a, b) => slotStartUtcMs(a.interval_start) - slotStartUtcMs(b.interval_start))
  }, [futureSlots])

  const chartIntervals = useMemo(() => {
    const endMs = nowMs + chartHorizonH * 60 * 60 * 1000
    return chartMergedSlots.filter((s) => {
      const t = slotStartUtcMs(s.interval_start)
      return t >= slotFloorMs && t <= endMs
    })
  }, [chartMergedSlots, nowMs, chartHorizonH, slotFloorMs])

  const planTableRows = useMemo(
    () => buildPlanTableRows(visibleSlots, nowMs),
    [visibleSlots, nowMs],
  )
  const showGenCut = useMemo(() => hasGenCutoff(visibleSlots), [visibleSlots])

  const xTicks = useMemo(() => {
    if (!chartIntervals.length) return undefined
    const stepH = chartHorizonH <= 24 ? 2 : chartHorizonH <= 48 ? 4 : 6
    const stepMs = stepH * 60 * 60 * 1000
    const first = slotStartUtcMs(chartIntervals[0].interval_start)
    const last = slotStartUtcMs(chartIntervals[chartIntervals.length - 1].interval_start)
    const ticks: string[] = []
    let t = Math.ceil(first / stepMs) * stepMs
    while (t <= last) {
      const hit = chartIntervals.find((i) => Math.abs(slotStartUtcMs(i.interval_start) - t) < 30 * 60 * 1000)
      if (hit) ticks.push(hit.interval_start)
      t += stepMs
    }
    return ticks.length ? ticks.map((iso) => formatLocalTime(iso)) : undefined
  }, [chartIntervals, chartHorizonH])

  const chartRows: ChartRow[] = useMemo(() => {
    const compareIntervals = compareData?.comparison?.intervals ?? []
    const compareMap = new Map(compareIntervals.map((i) => [i.interval_start, i]))
    return chartIntervals.map((i) => ({
      label: formatLocalTime(i.interval_start),
      ts: slotStartUtcMs(i.interval_start),
      pv_a_w: pvChartFveW(i, nowMs),
      battery_soc_target_pct: i.battery_soc_target_pct,
      battery_setpoint_w: i.battery_setpoint_w ?? 0,
      grid_setpoint_w: i.grid_setpoint_w ?? 0,
      compare_battery_setpoint_w: compareMap.get(i.interval_start)?.battery_setpoint_w ?? null,
      effective_buy_price: i.effective_buy_price,
      raw: i,
    }))
  }, [chartIntervals, nowMs, compareData?.comparison?.intervals])

  const run = data?.run
  const summary = data?.summary

  const solverSnap = useMemo(
    () =>
      parsePlanSolverSnapshot(
        run != null ? (run as unknown as Record<string, unknown>) : undefined,
      ),
    [run],
  )

  const chartChargeBands = useMemo(
    () => buildChartMaskBands(chartRows, solverSnap, 'allow_charge'),
    [chartRows, solverSnap],
  )
  const chartExportBands = useMemo(
    () => buildChartMaskBands(chartRows, solverSnap, 'allow_discharge_export'),
    [chartRows, solverSnap],
  )

  const compareIntervalByStart = useMemo(() => {
    const list = compareData?.comparison?.intervals ?? []
    return new Map(list.map((i) => [i.interval_start, i]))
  }, [compareData?.comparison?.intervals])

  const tableColCount = 14 + (solverSnap != null ? 1 : 0) + (showGenCut ? 1 : 0)

  async function onReplan() {
    if (siteId == null) return
    setReplanning(true)
    setError(null)
    try {
      await postRunPlan(siteId, 'rolling')
      await load()
    } catch (e) {
      setError(axiosDetail(e) || 'Přepočet selhal')
    } finally {
      setReplanning(false)
    }
  }

  async function runRollingReload() {
    if (siteId == null) return
    await postRunPlan(siteId, 'rolling')
    await load()
  }

  async function handleImportPrices() {
    if (siteId == null) return
    setPrepAction('import')
    setError(null)
    try {
      const selectedDate = new Date()
      if (importDate === 'tomorrow') {
        selectedDate.setDate(selectedDate.getDate() + 1)
      }
      const r = await postImportSitePrices(siteId, pragueYmd(selectedDate))
      toast.success(
        `Ceny: ${r.slots_imported} slotů (${r.date}), první ${r.first_price_czk_kwh.toFixed(3)} Kč/kWh`,
      )
      await runRollingReload()
    } catch (e) {
      toast.error('Import cen selhal', { description: axiosDetail(e) })
    } finally {
      setPrepAction(null)
    }
  }

  async function handleRunForecast() {
    if (siteId == null) return
    setPrepAction('forecast')
    setError(null)
    try {
      const r = await postRunForecast(siteId)
      toast.success(`Forecast: ${r.intervals_saved} intervalů, ${r.pv_arrays} FVE polí`)
      await runRollingReload()
    } catch (e) {
      toast.error('Forecast selhal', { description: axiosDetail(e) })
    } finally {
      setPrepAction(null)
    }
  }

  async function handleInitializePlan() {
    if (siteId == null) return
    setPrepAction('init')
    setError(null)
    try {
      const selectedDate = new Date()
      if (importDate === 'tomorrow') {
        selectedDate.setDate(selectedDate.getDate() + 1)
      }
      const imp = await postImportSitePrices(siteId, pragueYmd(selectedDate))
      toast.success(
        `Ceny: ${imp.slots_imported} slotů (${imp.date}), první ${imp.first_price_czk_kwh.toFixed(3)} Kč/kWh`,
      )
      const fc = await postRunForecast(siteId)
      toast.success(`Forecast: ${fc.intervals_saved} intervalů, ${fc.pv_arrays} FVE polí`)
      await runRollingReload()
      toast.success('Plán přepočítán (rolling).')
    } catch (e) {
      toast.error('Inicializace selhala', { description: axiosDetail(e) })
    } finally {
      setPrepAction(null)
    }
  }

  if (!siteReady) {
    return (
      <div className="flex min-h-[40vh] items-center justify-center text-slate-400">
        Načítám lokalitu…
      </div>
    )
  }

  if (siteId == null) {
    return (
      <div className="rounded-lg border border-amber-900/50 bg-amber-950/20 p-4 text-amber-200">
        V PostgREST nebyla nalezena lokalita (vw_site_status). Nelze načíst plán.
      </div>
    )
  }

  const showPrepActions = !loading
  const prepBusy = prepAction !== null

  const correctionPct =
    run?.forecast_correction_factor != null ? run.forecast_correction_factor * 100 : null
  const correctionUp = (run?.forecast_correction_factor ?? 1) >= 1
  const compareActiveSummary = compareData?.active?.summary ?? null
  const comparePeerSummary = compareData?.comparison?.summary ?? null
  const compareDiff = compareData?.diff ?? null
  const compareSlotDiffs = compareData?.slot_diffs ?? []

  return (
    <div className="mx-auto max-w-6xl space-y-8 p-4 md:p-6">
      <header className="space-y-1">
        <h1 className="text-xl font-semibold tracking-tight text-white">Plánování</h1>
        <p className="text-sm text-slate-400">
          Aktuální LP plán ({site?.site_name ?? 'lokalita'}) — tabulka jen z uloženého horizontu; sloupec a křivka FVE
          používají <span className="text-slate-300">korigovanou předpověď</span> (delta profil, stejně jako přehled a
          vstupy solveru). Graf za horizont plánu doplňuje stejnou řadu až do 96 h.
        </p>
      </header>

      {error && (
        <div className="rounded-md border border-red-900/60 bg-red-950/30 px-3 py-2 text-sm text-red-200">
          {error}
        </div>
      )}

      {/* Sekce 1 */}
      <section className="rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg">
        <h2 className="mb-3 text-sm font-medium uppercase tracking-wide text-slate-400">
          Status aktivního plánu
        </h2>
        {loading && !run ? (
          <div className="flex items-center gap-2 text-slate-400">
            <Loader2 className="h-4 w-4 animate-spin" /> Načítám…
          </div>
        ) : !run ? (
          <div className="space-y-3">
            <p className="text-slate-400">Žádný aktivní plán.</p>
            {showPrepActions && (
              <PlanPrepActions
                prepAction={prepAction}
                replanning={replanning}
                importDate={importDate}
                onImportDateChange={setImportDate}
                onImport={() => void handleImportPrices()}
                onForecast={() => void handleRunForecast()}
                onInit={() => void handleInitializePlan()}
              />
            )}
          </div>
        ) : (
          <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
            <div className="min-w-0 flex-1 space-y-3">
              <div className="flex flex-wrap items-center gap-2 text-sm text-slate-200">
                <span className="text-slate-500">Vytvořeno:</span>
                <span className="font-mono">{formatLocal(run.created_at)}</span>
                <span className="text-slate-600">|</span>
                <span className="text-slate-500">Typ:</span>
                <span
                  className={`rounded-md px-2 py-0.5 text-xs font-semibold uppercase tracking-wide ${runTypeBadgeClass(run.run_type)}`}
                >
                  {run.run_type}
                </span>
              </div>
              <div className="text-sm">
                <span className="text-slate-500">Horizont: </span>
                <span className="font-mono text-slate-200">
                  {formatLocal(run.horizon_start)} → {formatLocal(run.horizon_end)}
                </span>
              </div>
              <div className="flex flex-wrap items-center gap-2 text-sm">
                <span className="text-slate-500">Korekce FVE forecastu:</span>
                <span className="inline-flex items-center gap-1 font-mono text-slate-200">
                  {correctionPct != null ? (
                    <>
                      {correctionUp ? (
                        <ArrowUpRight className="h-4 w-4 text-emerald-400" aria-hidden />
                      ) : (
                        <ArrowDownRight className="h-4 w-4 text-amber-400" aria-hidden />
                      )}
                      {Number.isInteger(correctionPct)
                        ? correctionPct
                        : correctionPct.toLocaleString('cs-CZ', { maximumFractionDigits: 1 })}{' '}
                      %
                    </>
                  ) : (
                    '—'
                  )}
                </span>
              </div>
              <div className="text-sm">
                <span className="text-slate-500">Čas výpočtu solveru: </span>
                <span className="font-mono text-slate-200">
                  {run.solver_duration_ms != null ? `${run.solver_duration_ms} ms` : '—'}
                </span>
              </div>
              {summary?.pv_scarcity_factor != null && (
                <div className="text-sm">
                  <span className="text-slate-500">PV scarcity factor: </span>
                  <span className="font-mono text-slate-200">
                    {summary.pv_scarcity_factor.toFixed(3)}
                  </span>
                  <span className="ml-2 text-xs text-slate-500">
                    (nižší = méně očekávaného slunce, ekonomika víc toleruje precharge ze sítě)
                  </span>
                </div>
              )}
              {solverSnap != null && <PlanSolverInsights snap={solverSnap} />}
              {summary && (
                <div className="border-t border-slate-800 pt-3 text-sm">
                  <p className="mb-2 text-slate-500">Summary</p>
                  <dl className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
                    <div>
                      <dt className="text-xs text-slate-500">
                        {summary.total_expected_cost_czk >= 0 ? 'Celkové náklady' : 'Celkový příjem'}
                      </dt>
                      <dd className="font-mono text-slate-100">
                        {summary.total_expected_cost_czk >= 0
                          ? `${summary.total_expected_cost_czk.toFixed(2)} Kč`
                          : `${Math.abs(summary.total_expected_cost_czk).toFixed(2)} Kč`}
                      </dd>
                    </div>
                    <div>
                      <dt className="text-xs text-slate-500">kWh curtailmentu (A)</dt>
                      <dd className="font-mono text-slate-100">
                        {summary.total_pv_curtailed_kwh.toLocaleString('cs-CZ', {
                          minimumFractionDigits: 3,
                          maximumFractionDigits: 3,
                        })}{' '}
                        kWh
                      </dd>
                    </div>
                    <div>
                      <dt className="text-xs text-slate-500">Sloty nabíjení / vybíjení / export</dt>
                      <dd className="font-mono text-slate-100">
                        {summary.charge_slots} / {summary.discharge_slots} / {summary.export_slots}
                      </dd>
                    </div>
                  </dl>
                </div>
              )}
            </div>
            <div className="flex shrink-0 flex-col items-stretch gap-2 sm:items-end">
              {showPrepActions && (
                <PlanPrepActions
                  prepAction={prepAction}
                  replanning={replanning}
                  importDate={importDate}
                  onImportDateChange={setImportDate}
                  onImport={() => void handleImportPrices()}
                  onForecast={() => void handleRunForecast()}
                  onInit={() => void handleInitializePlan()}
                  wrapClassName="flex flex-wrap justify-end gap-2"
                />
              )}
              <button
                type="button"
                onClick={() => void onReplan()}
                disabled={replanning || prepBusy}
                className="inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-emerald-500 disabled:opacity-50"
              >
                {replanning ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
                Přeplánovat
              </button>
            </div>
          </div>
        )}
      </section>

      {/* Sekce 2 */}
      <section className="rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg">
        <h2 className="mb-2 text-sm font-medium uppercase tracking-wide text-slate-400">Porovnání v1 / v2</h2>
        {compareLoading ? (
          <div className="flex items-center gap-2 text-slate-400">
            <Loader2 className="h-4 w-4 animate-spin" /> Načítám compare…
          </div>
        ) : compareError && !compareData ? (
          <div className="rounded-md border border-amber-900/60 bg-amber-950/30 px-3 py-2 text-sm text-amber-200">
            {compareError}
          </div>
        ) : compareData ? (
          <div className="space-y-4">
            {compareError && (
              <div className="rounded-md border border-amber-900/60 bg-amber-950/30 px-3 py-2 text-sm text-amber-200">
                {compareError}
              </div>
            )}
            <div className="grid gap-3 md:grid-cols-3">
              <div className="rounded-lg border border-slate-800 bg-slate-950/40 p-3">
                <p className="text-xs uppercase tracking-wide text-slate-500">Aktivní verze</p>
                <p className="mt-1 font-mono text-lg text-white">
                  {recordString(compareData.active.run?.run_type) ?? '—'}
                </p>
                <p className="text-xs text-slate-500">
                  Solver: {recordNumber(compareData.active.run?.solver_duration_ms) != null
                    ? `${recordNumber(compareData.active.run?.solver_duration_ms)} ms`
                    : '—'}
                </p>
                <p className="mt-2 text-xs text-slate-500">
                  Náklady: {recordNumber(compareActiveSummary?.total_expected_cost_czk) != null
                    ? `${recordNumber(compareActiveSummary?.total_expected_cost_czk)?.toFixed(2)} Kč`
                    : '—'}
                </p>
              </div>
              <div className="rounded-lg border border-slate-800 bg-slate-950/40 p-3">
                <p className="text-xs uppercase tracking-wide text-slate-500">Compare verze</p>
                <p className="mt-1 font-mono text-lg text-white">
                  {recordString(compareData.comparison.run?.run_type) ?? '—'}
                </p>
                <p className="text-xs text-slate-500">
                  Solver: {recordNumber(compareData.comparison.run?.solver_duration_ms) != null
                    ? `${recordNumber(compareData.comparison.run?.solver_duration_ms)} ms`
                    : '—'}
                </p>
                <p className="mt-2 text-xs text-slate-500">
                  Náklady: {recordNumber(comparePeerSummary?.total_expected_cost_czk) != null
                    ? `${recordNumber(comparePeerSummary?.total_expected_cost_czk)?.toFixed(2)} Kč`
                    : '—'}
                </p>
              </div>
              <div className="rounded-lg border border-cyan-900/50 bg-cyan-950/20 p-3">
                <p className="text-xs uppercase tracking-wide text-cyan-200/80">Rozdíl</p>
                <p className="mt-1 font-mono text-lg text-cyan-100">
                  {recordNumber(compareDiff?.total_expected_cost_czk) != null
                    ? `${recordNumber(compareDiff?.total_expected_cost_czk)?.toFixed(2)} Kč`
                    : '—'}
                </p>
                <p className="text-xs text-cyan-100/70">
                  Změněných slotů:{' '}
                  {recordNumber(compareDiff?.changed_slots) != null ? recordNumber(compareDiff?.changed_slots) : '—'}
                </p>
                <p className="text-xs text-cyan-100/70">
                  Aktivní / compare export sloty:{' '}
                  {recordNumber(compareDiff?.active_export_slots) != null
                    ? `${recordNumber(compareDiff?.active_export_slots)} / ${recordNumber(compareDiff?.comparison_export_slots)}`
                    : '—'}
                </p>
              </div>
            </div>

            {compareSlotDiffs.length > 0 ? (
              <div className="max-h-[320px] overflow-auto rounded-lg border border-slate-800/80">
                <table className="w-full border-collapse text-left text-xs">
                  <thead className="sticky top-0 bg-slate-900 text-slate-500 shadow-[0_1px_0_0_rgb(30_41_59)]">
                    <tr>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Slot</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Aktivní bat. W</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Compare bat. W</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Aktivní grid W</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Compare grid W</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Aktivní export</th>
                      <th className="whitespace-nowrap px-2 py-2 font-medium">Compare export</th>
                    </tr>
                  </thead>
                  <tbody>
                    {compareSlotDiffs.slice(0, 48).map((row) => (
                      <tr key={row.interval_start} className="border-b border-slate-800/80">
                        <td className="whitespace-nowrap px-2 py-1.5 font-mono text-slate-300">
                          {formatLocalTime(row.interval_start)}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordNumber(row.active.battery_setpoint_w) != null
                            ? recordNumber(row.active.battery_setpoint_w)
                            : '—'}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordNumber(row.comparison.battery_setpoint_w) != null
                            ? recordNumber(row.comparison.battery_setpoint_w)
                            : '—'}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordNumber(row.active.grid_setpoint_w) != null
                            ? recordNumber(row.active.grid_setpoint_w)
                            : '—'}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordNumber(row.comparison.grid_setpoint_w) != null
                            ? recordNumber(row.comparison.grid_setpoint_w)
                            : '—'}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordString(row.active.export_mode) ?? '—'}
                        </td>
                        <td className="px-2 py-1.5 font-mono text-slate-200">
                          {recordString(row.comparison.export_mode) ?? '—'}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            ) : (
              <p className="text-sm text-slate-500">Compare běh je uložen, ale nemá slotové rozdíly k zobrazení.</p>
            )}
          </div>
        ) : (
          <p className="text-sm text-slate-500">
            Compare plán zatím není k dispozici. Spusťte plánování s aktivním režimem v1/v2 compare.
          </p>
        )}
      </section>

      {/* Sekce 3 */}
      <section className="rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg">
        <h2 className="mb-1 text-sm font-medium uppercase tracking-wide text-slate-400">Graf plánu</h2>
        {solverSnap != null && (
          <p className="mb-2 text-[11px] text-slate-500">
            Pásy: <span className="text-emerald-400/90">zelená</span> = okno grid nabíjení (⚡) ·{' '}
            <span className="text-orange-400/90">oranžová</span> = okno exportu baterie (↓).
          </p>
        )}
        <HorizonToggle
          value={chartHorizonH}
          onChange={setChartHorizonH}
          disabled={chartMergedSlots.length === 0}
        />
        {!chartRows.length ? (
          <p className="text-sm text-slate-500">
            Žádná data pro graf (plán + korig. FVE do {chartHorizonH} h od aktuálního slotu). Spusťte forecast, pokud
            chybí křivka výroby.
          </p>
        ) : (
          <div className="h-[350px] w-full">
            <ResponsiveContainer width="100%" height="100%">
              <ComposedChart data={chartRows} margin={{ top: 8, right: 72, left: 8, bottom: 4 }}>
                <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
                <XAxis
                  dataKey="label"
                  ticks={xTicks}
                  tick={{ fill: '#94a3b8', fontSize: 9 }}
                  interval={0}
                  angle={-35}
                  textAnchor="end"
                  height={52}
                />
                <YAxis
                  yAxisId="power"
                  tick={{ fill: '#94a3b8', fontSize: 10 }}
                  label={{ value: 'W', angle: -90, position: 'insideLeft', fill: '#64748b', fontSize: 11 }}
                />
                <YAxis
                  yAxisId="soc"
                  orientation="right"
                  domain={[0, 100]}
                  tick={{ fill: '#22c55e', fontSize: 10 }}
                  label={{ value: 'SoC %', angle: 90, position: 'insideRight', fill: '#22c55e', fontSize: 11 }}
                />
                <YAxis
                  yAxisId="price"
                  orientation="right"
                  width={52}
                  tick={{ fill: '#94a3b8', fontSize: 9 }}
                  axisLine={{ stroke: '#64748b' }}
                  tickLine={{ stroke: '#64748b' }}
                  label={{
                    value: 'Kč/kWh',
                    angle: 90,
                    position: 'insideRight',
                    fill: '#94a3b8',
                    fontSize: 10,
                    offset: 10,
                  }}
                />
                <Tooltip
                  trigger={isCoarse ? 'click' : 'hover'}
                  content={<PlanTooltip nowMs={nowMs} solverSnap={solverSnap} />}
                />
                {chartChargeBands.map((b) => (
                  <ReferenceArea
                    key={`chg-${b.x1}-${b.x2}`}
                    yAxisId="power"
                    x1={b.x1}
                    x2={b.x2}
                    strokeOpacity={0}
                    fill="#10b981"
                    fillOpacity={0.08}
                  />
                ))}
                {chartExportBands.map((b) => (
                  <ReferenceArea
                    key={`exp-${b.x1}-${b.x2}`}
                    x1={b.x1}
                    x2={b.x2}
                    yAxisId="power"
                    strokeOpacity={0}
                    fill="#f97316"
                    fillOpacity={0.1}
                  />
                ))}
                <Area
                  yAxisId="power"
                  type="monotone"
                  dataKey="pv_a_w"
                  name="FVE / korig. předpověď"
                  stroke="#ca8a04"
                  fill="#eab308"
                  fillOpacity={0.35}
                />
                <Bar yAxisId="power" dataKey="battery_setpoint_w" name="Baterie W" barSize={10} isAnimationActive={false}>
                  {chartRows.map((e) => (
                    <Cell
                      key={e.ts}
                      fill={e.battery_setpoint_w >= 0 ? '#22c55e' : '#f97316'}
                      fillOpacity={0.85}
                    />
                  ))}
                </Bar>
                <Line
                  yAxisId="power"
                  type="monotone"
                  dataKey="grid_setpoint_w"
                  name="Síť W (+odb / −exp)"
                  stroke="#f87171"
                  dot={false}
                  strokeWidth={2}
                  connectNulls
                />
                <Line
                  yAxisId="power"
                  type="monotone"
                  dataKey="compare_battery_setpoint_w"
                  name="Compare baterie W"
                  stroke="#fb923c"
                  dot={false}
                  strokeWidth={2}
                  strokeDasharray="6 4"
                  connectNulls
                />
                <Line
                  yAxisId="soc"
                  type="monotone"
                  dataKey="battery_soc_target_pct"
                  name="SoC %"
                  stroke="#4ade80"
                  dot={false}
                  strokeWidth={2}
                  connectNulls
                />
                <Line
                  yAxisId="price"
                  type="monotone"
                  dataKey="effective_buy_price"
                  name="Cena nákup"
                  stroke="#94a3b8"
                  strokeDasharray="5 4"
                  dot={false}
                  strokeWidth={2}
                  connectNulls
                />
              </ComposedChart>
            </ResponsiveContainer>
          </div>
        )}
      </section>

      {/* Sekce 4 */}
      <section className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg">
        <h2 className="mb-1 text-sm font-medium uppercase tracking-wide text-slate-400">Tabulka slotů</h2>
        <HorizonToggle value={tableHorizonH} onChange={setTableHorizonH} disabled={futureSlots.length === 0} />
        <div className="max-h-[min(70vh,720px)] overflow-y-auto overflow-x-auto rounded-lg border border-slate-800/80">
          <table className="w-full min-w-[1100px] border-collapse text-left text-xs">
            <thead className="sticky top-0 z-10 bg-slate-900 shadow-[0_1px_0_0_rgb(30_41_59)]">
              <tr className="text-slate-500">
                <th className="whitespace-nowrap py-2 pl-2 pr-2 font-medium">Čas</th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">
                  <span className="block">Cena</span>
                  <span className="block text-[10px] font-normal normal-case text-slate-600">Kč/kWh · kup / prod</span>
                </th>
                <th
                  className="whitespace-nowrap py-2 pr-2 font-medium"
                  title="Setpoint výkonu baterie/invertoru: kladně nabíjení, záporně vybíjení (do spotřeby domu nebo dle regulace do přebytku)."
                >
                  <span className="block">Bat. W</span>
                  <span className="block text-[10px] font-normal normal-case text-slate-600">
                    + nabíj · − vybíj
                  </span>
                </th>
                <th
                  className="whitespace-nowrap py-2 pr-2 font-medium"
                  title="Záměr exportu do sítě z LP (NONE / PV_SURPLUS / BATTERY_SELL)"
                >
                  Export
                </th>
                {solverSnap != null ? (
                  <th
                    className="whitespace-nowrap py-2 pr-2 font-medium"
                    title="Masky z posledního solveru: ⚡ allow_charge, ↓ allow_discharge_export"
                  >
                    Masky
                  </th>
                ) : null}
                <th className="whitespace-nowrap py-2 pr-2 font-medium">Deye setpoint</th>
                {showGenCut ? (
                  <th className="whitespace-nowrap py-2 pr-2 font-medium" title="GEN port cut-off (BA81 / mikroinvertory)">
                    GEN
                  </th>
                ) : null}
                <th className="whitespace-nowrap py-2 pr-2 font-medium">SoC %</th>
                <th
                  className="whitespace-nowrap py-2 pr-2 font-medium"
                  title="Povolený výkon pole A (forecast − curtail) ≈ Deye reg 340 max solar"
                >
                  PV A
                </th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium" title="Budoucí sloty: korigovaná předpověď z delta profilu; proběhlé sloty z auditu.">
                  <span className="block">FVE W</span>
                  <span className="block text-[10px] font-normal normal-case text-slate-600">delta · audit</span>
                </th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">Dům W</th>
                <th
                  className="whitespace-nowrap py-2 pr-2 font-medium"
                  title="Čistý výkon na hlavním elektroměru (point of supply): kladně odběr ze sítě, záporně export. Záporná hodnota často znamená export FVE při spotřebě domu; u záporného Bat. W to nemusí být „prodej z baterie“, ale výsledek bilance FVE + dům + baterie."
                >
                  <span className="block">Síť W</span>
                  <span className="block text-[10px] font-normal normal-case text-slate-600">
                    čistý EM · +odb / −exp
                  </span>
                </th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">EV1 W</th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">EV2 W</th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">TČ</th>
                <th className="whitespace-nowrap py-2 pr-2 font-medium">Výnos Kč</th>
              </tr>
            </thead>
            <tbody>
              {planTableRows.map((row) => {
                if (row.kind === 'summary') {
                  return (
                    <tr
                      key={`sum-${row.dayKey}`}
                      className="border-b border-slate-700/90 bg-slate-800/70 text-slate-200"
                    >
                      <td colSpan={tableColCount} className="px-2 py-2 text-xs font-medium">
                        <span className="text-slate-100">{row.dateLabel}</span>
                        <span className="mx-2 text-slate-600">·</span>
                        <span className="text-slate-400">FVE celkem</span>{' '}
                        <span className="font-mono tabular-nums text-slate-200">
                          {row.fveKwh.toLocaleString('cs-CZ', { maximumFractionDigits: 3 })} kWh
                        </span>
                        <span className="mx-2 text-slate-600">·</span>
                        <span className="text-slate-400">Export celkem</span>{' '}
                        <span className="font-mono tabular-nums text-slate-200">
                          {row.exportKwh.toLocaleString('cs-CZ', { maximumFractionDigits: 3 })} kWh
                        </span>
                        <span className="mx-2 text-slate-600">·</span>
                        <span className="text-slate-400">Prům. cena nákup</span>{' '}
                        <span className="font-mono tabular-nums text-slate-200">
                          {row.avgBuy != null ? `${row.avgBuy.toFixed(3)} Kč/kWh` : '—'}
                        </span>
                      </td>
                    </tr>
                  )
                }
                const i = row.i
                const sel = selectedStart === i.interval_start
                const slotMask = maskForInterval(solverSnap, i.interval_start)
                const exBadge = exportModeBadge(i)
                const phaseBadge = negSellPhaseBadge(slotMask)
                const pvAllowed = pvAAllowedW(i)
                return (
                  <Fragment key={i.interval_start}>
                  <tr
                    role="button"
                    tabIndex={0}
                    onClick={() => setSelectedStart((prev) => (prev === i.interval_start ? null : i.interval_start))}
                    onKeyDown={(ev) => {
                      if (ev.key === 'Enter' || ev.key === ' ') {
                        ev.preventDefault()
                        setSelectedStart((prev) => (prev === i.interval_start ? null : i.interval_start))
                      }
                    }}
                    className={`cursor-pointer border-b border-slate-800/80 transition hover:bg-slate-800/40 ${tableRowClass(i, sel, slotMask)}`}
                  >
                    <td className="whitespace-nowrap py-1.5 pl-2 pr-2 font-mono text-slate-300">
                      {formatLocalTime(i.interval_start)}
                    </td>
                    <CenaCell i={i} nowMs={nowMs} />
                    <td className="pr-2 font-mono tabular-nums text-slate-300">{i.battery_setpoint_w ?? '—'}</td>
                    <td className="pr-2">
                      {exBadge.label !== '—' ? (
                        <span
                          className={`inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-semibold ${exBadge.klass}`}
                          title={exBadge.title}
                        >
                          {exBadge.label}
                        </span>
                      ) : (
                        <span className="text-slate-600" title={exBadge.title}>
                          —
                        </span>
                      )}
                    </td>
                    {solverSnap != null ? <MaskIconsCell mask={slotMask} /> : null}
                    <td className="max-w-[200px] whitespace-normal break-words pr-2 text-slate-300">
                      <div className="flex flex-wrap items-center gap-2">
                        {(() => {
                          const b = deyeModeBadge(i)
                          return (
                            <span
                              className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-semibold ${b.klass}`}
                              title={b.title}
                            >
                              {b.label}
                            </span>
                          )
                        })()}
                        <span className="text-slate-400" title={deyeSetpointLabel(i)}>
                          {deyeSetpointLabel(i)}
                        </span>
                      </div>
                    </td>
                    {showGenCut ? (
                      <td className="pr-2">
                        {(() => {
                          const g = genCutoffBadge(i)
                          if (!g.show) return <span className="text-slate-600">—</span>
                          return (
                            <span
                              className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-semibold ${g.klass}`}
                              title={g.title}
                            >
                              {g.label}
                            </span>
                          )
                        })()}
                      </td>
                    ) : null}
                    <td className="pr-2 font-mono tabular-nums text-slate-300">
                      {i.battery_soc_target_pct != null
                        ? `${i.battery_soc_target_pct.toFixed(1)}`
                        : '—'}
                    </td>
                    <td className="pr-2 font-mono tabular-nums text-slate-300">
                      <div className="flex flex-wrap items-center gap-1">
                        {pvAllowed != null ? pvAllowed.toLocaleString('cs-CZ') : '—'}
                        {(i.pv_a_curtailed_w ?? 0) > 0 ? (
                          <span className="rounded bg-yellow-500/20 px-1 py-0.5 text-[9px] font-semibold text-yellow-200">
                            CURTAIL
                          </span>
                        ) : null}
                        {phaseBadge ? (
                          <span
                            className={`rounded px-1 py-0.5 text-[9px] font-semibold ${phaseBadge.klass}`}
                            title={phaseBadge.title}
                          >
                            {phaseBadge.label}
                          </span>
                        ) : null}
                      </div>
                    </td>
                    <FveWCell i={i} nowMs={nowMs} />
                    <td className="pr-2 font-mono tabular-nums text-slate-300">
                      {formatPlanPowerW(i.load_baseline_w)}
                    </td>
                    <td className="pr-2 font-mono tabular-nums text-slate-300">{i.grid_setpoint_w ?? '—'}</td>
                    <td className="pr-2 font-mono tabular-nums text-slate-300">{i.ev1_setpoint_w ?? '—'}</td>
                    <td className="pr-2 font-mono tabular-nums text-slate-300">{i.ev2_setpoint_w ?? '—'}</td>
                    <td className="pr-2 text-slate-300">{i.heat_pump_enabled ? 'on' : 'off'}</td>
                    <VynosKcCell v={i.expected_cost_czk} />
                  </tr>
                  {sel ? (
                    // Detail jako plnoširoký řádek pod vybraným slotem (žádný překryv);
                    // sticky left drží blok ve viewportu i při horizontálním scrollu tabulky.
                    <tr className="border-b border-slate-800/80">
                      <td colSpan={tableColCount} className="p-0">
                        <div className="sticky left-0 w-[min(100%,calc(100vw-4rem))] px-2 pb-3">
                          <PlanSlotDetail
                            i={i}
                            mask={slotMask}
                            compare={compareIntervalByStart.get(i.interval_start)}
                            nowMs={nowMs}
                          />
                        </div>
                      </td>
                    </tr>
                  ) : null}
                  </Fragment>
                )
              })}
            </tbody>
          </table>
        </div>
        {!visibleSlots.length && !loading && (
          <p className="mt-2 text-sm text-slate-500">
            Žádné budoucí sloty v horizontu {tableHorizonH} h (aktivní plán může být prázdný nebo starý).
          </p>
        )}
        {!solverSnap && run != null && (
          <p className="mt-2 text-[11px] text-slate-500">
            Masky solveru nejsou v tomto běhu — spusťte nový rolling/denní plán po nasazení arbitráže.
          </p>
        )}
      </section>
    </div>
  )
}
