import axios from 'axios'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import {
  getCurrentPlan,
  getForecastPvSlotsRangeCorrected,
  getSitePricesSlotsRange,
} from '../api/backend'
import { getJson } from '../api/postgrest'
import {
  currentSlotIndexInWindow,
  SLOT_COUNT_BACK,
  SLOT_MS,
  TOTAL_SLOTS,
  floorSlotUtcMs,
} from '../components/charts/chartConstants'
import { pragueAddCalendarDays, pragueCalendarDay } from '../lib/pragueDate'
import type { ForecastDayTotal, LiveMetrics, NegPriceItem, SlotData } from '../types/dashboard'
import type {
  AuditTodayHourlyRow,
  HeatPumpLatestRow,
  ModeLogRecentRow,
  SiteStatusRow,
  Telemetry15m7dRow,
} from '../types/ems'
import type { PlanningIntervalDto } from '../types/plan'

const POLL_FULL_MS = 60_000
const POLL_LIVE_MS = 15_000

/** Limit řádků vw_telemetry_15m_7d: jen okno zpět (s rezervou), ne celých 7 dní. */
const TELEMETRY_15M_LIMIT = String(Math.ceil(SLOT_COUNT_BACK * 1.2))

function parseNum(v: string | number | null | undefined): number | null {
  if (v == null) return null
  if (typeof v === 'number' && !Number.isNaN(v)) return v
  const n = Number(v)
  return Number.isFinite(n) ? n : null
}

function numFromWs(v: unknown): number | null {
  if (v == null) return null
  const n = typeof v === 'number' ? v : Number(v)
  return Number.isFinite(n) ? n : null
}

function buildLiveMetrics(
  status: SiteStatusRow | null,
  _hp: HeatPumpLatestRow | null,
): LiveMetrics | null {
  if (status == null) return null
  return {
    pv_w: parseNum(status.pv_power_w),
    load_w: parseNum(status.load_power_w),
    grid_w: parseNum(status.grid_power_w),
    bat_soc: parseNum(status.battery_soc_percent),
    bat_w: parseNum(status.battery_power_w),
  }
}

/** Klíč hodiny v Europe/Prague (pro shodu s vw_audit_today_hourly.hour_local). */
function pragueHourKey(ms: number): string {
  return new Intl.DateTimeFormat('sv-SE', {
    timeZone: 'Europe/Prague',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    hour12: false,
  }).format(new Date(ms))
}

function slotTimeKey(ms: number): string {
  return String(floorSlotUtcMs(ms))
}

function modeAt(logs: ModeLogRecentRow[], tMs: number): string | null {
  let best: ModeLogRecentRow | null = null
  let bestA = -Infinity
  for (const l of logs) {
    const a = new Date(l.activated_at).getTime()
    const d = l.deactivated_at ? new Date(l.deactivated_at).getTime() : Number.POSITIVE_INFINITY
    if (a <= tMs && tMs < d && a >= bestA) {
      bestA = a
      best = l
    }
  }
  return best?.mode_code ?? null
}

function emptySlot(iso: string): SlotData {
  return {
    interval_start: iso,
    pv_power_w: null,
    battery_power_w: null,
    battery_setpoint_w: null,
    grid_power_w: null,
    grid_setpoint_w: null,
    deye_physical_mode: null,
    export_mode: null,
    export_limit_w: null,
    load_power_w: null,
    gen_port_power_w: null,
    pv_a_forecast_w: null,
    pv_b_forecast_w: null,
    load_baseline_w: null,
    ev1_setpoint_w: null,
    ev2_setpoint_w: null,
    heat_pump_setpoint_w: null,
    heat_pump_enabled: null,
    battery_soc_target_pct: null,
    buy_price: null,
    sell_price: null,
    regime_code: null,
    regime_is_planned: false,
    soc_actual_pct: null,
    soc_plan_pct: null,
    tuv_actual_c: null,
    tuv_plan_c: null,
  }
}

function mergeInterval(s: SlotData, p: PlanningIntervalDto): void {
  s.battery_setpoint_w = p.battery_setpoint_w ?? s.battery_setpoint_w
  s.grid_setpoint_w = p.grid_setpoint_w ?? s.grid_setpoint_w
  if (p.deye_physical_mode != null) s.deye_physical_mode = p.deye_physical_mode
  if (p.export_mode != null) s.export_mode = p.export_mode
  if (p.export_limit_w != null) s.export_limit_w = p.export_limit_w
  s.ev1_setpoint_w = p.ev1_setpoint_w ?? s.ev1_setpoint_w
  s.ev2_setpoint_w = p.ev2_setpoint_w ?? s.ev2_setpoint_w
  if (s.ev1_setpoint_w == null && s.ev2_setpoint_w == null && p.ev_charge_power_w != null) {
    s.ev1_setpoint_w = p.ev_charge_power_w
  }
  if (p.heat_pump_enabled === true) {
    s.heat_pump_enabled = true
  } else if (p.heat_pump_enabled === false) {
    s.heat_pump_enabled = false
  }
  if (p.heat_pump_setpoint_w != null) {
    s.heat_pump_setpoint_w = p.heat_pump_setpoint_w
    if (s.heat_pump_enabled == null) {
      s.heat_pump_enabled = p.heat_pump_setpoint_w > 0
    }
  } else if (p.heat_pump_enabled === false) {
    s.heat_pump_setpoint_w = 0
    s.heat_pump_enabled = false
  }
  s.load_baseline_w = p.load_baseline_w ?? s.load_baseline_w
  s.buy_price = parseNum(p.effective_buy_price) ?? s.buy_price
  s.sell_price = parseNum(p.effective_sell_price) ?? s.sell_price
  const tgtSoc = parseNum(p.battery_soc_target_pct)
  if (tgtSoc != null) {
    s.battery_soc_target_pct = tgtSoc
    s.soc_plan_pct = tgtSoc
  }
  const pva = p.pv_forecast_total_w != null ? Math.round(Number(p.pv_forecast_total_w) * 0.6) : null
  const pvb = p.pv_forecast_total_w != null ? Math.round(Number(p.pv_forecast_total_w) * 0.4) : null
  if (s.pv_a_forecast_w == null && pva != null) s.pv_a_forecast_w = pva
  if (s.pv_b_forecast_w == null && pvb != null) s.pv_b_forecast_w = pvb
  if (p.heat_pump_setpoint_w != null && p.heat_pump_setpoint_w > 0) {
    s.tuv_plan_c = 52
  }
}

export function useDashboardData(siteId: number | null) {
  const [slots, setSlots] = useState<SlotData[]>([])
  const [liveMetrics, setLiveMetrics] = useState<LiveMetrics | null>(null)
  const [forecastWeek, setForecastWeek] = useState<ForecastDayTotal[]>([])
  const [negPrices, setNegPrices] = useState<NegPriceItem[]>([])
  const [error, setError] = useState<string | null>(null)
  const [ready, setReady] = useState(false)
  const [slotsReady, setSlotsReady] = useState(false)

  const wsRef = useRef<WebSocket | null>(null)
  const siteIdRef = useRef(siteId)
  siteIdRef.current = siteId

  const load = useCallback(async () => {
    if (siteId == null) {
      setSlots([])
      setForecastWeek([])
      setNegPrices([])
      setLiveMetrics(null)
      setError(null)
      setReady(true)
      setSlotsReady(true)
      return
    }

    const windowStart = floorSlotUtcMs(Date.now()) - SLOT_COUNT_BACK * SLOT_MS
    const nIdx = currentSlotIndexInWindow(windowStart)

    // Vlna 1 — kritická: vw_site_status (+ TČ) je rychlé, UI se odemkne hned.
    let status: SiteStatusRow | null = null
    try {
      const [statusArr, hpArr] = await Promise.all([
        getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
        getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
      ])
      status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
      const hp = Array.isArray(hpArr) && hpArr[0] ? hpArr[0]! : null
      setLiveMetrics(buildLiveMetrics(status, hp))
      setError(null)
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Chyba načítání dashboardu')
    } finally {
      setReady(true)
    }

    // Vlna 2 — extended: plán, telemetrie, audit, ceny. Při refetchi zůstávají
    // zobrazená stale data (sloty se přepíšou až novými daty, žádné blikání).
    try {
      const todayPrague = pragueCalendarDay()

      const [
        planMaybe,
        telemetry15m7d,
        auditHourly,
        modeLog,
        priceRows,
      ] = await Promise.all([
        getCurrentPlan(siteId).catch((e: unknown) => {
          if (axios.isAxiosError(e) && e.response?.status === 404) {
            return { run: null, intervals: [] as PlanningIntervalDto[], summary: null }
          }
          throw e
        }),
        getJson<Telemetry15m7dRow[]>('/vw_telemetry_15m_7d', {
          site_id: `eq.${siteId}`,
          slot_start: `gte.${new Date(windowStart).toISOString()}`,
          order: 'slot_start.asc',
          limit: TELEMETRY_15M_LIMIT,
        }),
        getJson<AuditTodayHourlyRow[]>('/vw_audit_today_hourly', {
          site_id: `eq.${siteId}`,
          order: 'hour_local.asc',
        }),
        getJson<ModeLogRecentRow[]>('/vw_mode_log_recent', {
          site_id: `eq.${siteId}`,
          order: 'activated_at.asc',
          limit: '200',
        }),
        // Ceny bereme přes FastAPI range endpoint (PostgREST /rest je u vás chráněné → 401).
        getSitePricesSlotsRange(
          siteId,
          new Date(windowStart).toISOString(),
          new Date(windowStart + TOTAL_SLOTS * SLOT_MS).toISOString(),
        ),
      ])

      const plan = planMaybe as { intervals: PlanningIntervalDto[] }
      const planBySlot = new Map<string, PlanningIntervalDto>()
      for (const iv of plan.intervals) {
        planBySlot.set(slotTimeKey(new Date(iv.interval_start).getTime()), iv)
      }

      const priceBySlot = new Map<string, { buy: number | null; sell: number | null }>()
      const flatPrices = Array.isArray(priceRows) ? priceRows : []
      for (const r of flatPrices) {
        const k = slotTimeKey(new Date(r.interval_start).getTime())
        priceBySlot.set(k, {
          buy: parseNum(r.effective_buy_price_czk_kwh),
          sell: parseNum(r.effective_sell_price_czk_kwh),
        })
      }

      const forecastDays: ForecastDayTotal[] = []
      const weekDates = Array.from({ length: 7 }, (_, d) => pragueAddCalendarDays(todayPrague, d))

      // Forecast pro dashboard bereme pouze z range endpointu (jedno volání místo N× /forecast/pv per den).
      // Rozsah musí pokrýt (a) okno slotů pro tabulku a (b) 7denní sumáře.
      const windowFromIso = new Date(windowStart).toISOString()
      const windowToIso = new Date(windowStart + TOTAL_SLOTS * SLOT_MS).toISOString()
      const weekToIso = new Date(floorSlotUtcMs(Date.now()) + 7 * 24 * 60 * 60 * 1000).toISOString()
      const forecastToIso = weekToIso > windowToIso ? weekToIso : windowToIso

      const correctedSlots = await getForecastPvSlotsRangeCorrected(siteId, windowFromIso, forecastToIso).catch(
        () => [] as Awaited<ReturnType<typeof getForecastPvSlotsRangeCorrected>>,
      )
      const correctedBySlot = new Map<string, number>()
      for (const r of correctedSlots) {
        const t = new Date(r.interval_start).getTime()
        if (!Number.isFinite(t)) continue
        const v = r.pv_forecast_corrected_w
        if (v == null) continue
        correctedBySlot.set(slotTimeKey(t), Number(v))
      }
      for (const ymd of weekDates) {
        let kwh = 0
        // Křivka je po 15 min, takže energie = W * 0.25h
        // Použijeme correctedBySlot v časovém okně daného prague dne.
        const dayStart = new Date(ymd + 'T00:00:00').getTime()
        const dayEnd = new Date(pragueAddCalendarDays(ymd, 1) + 'T00:00:00').getTime()
        for (let t = dayStart; t < dayEnd; t += SLOT_MS) {
          const w = correctedBySlot.get(slotTimeKey(t)) ?? 0
          kwh += (w * 0.25) / 1000
        }
        const label = new Date(ymd + 'T12:00:00Z').toLocaleDateString('cs-CZ', {
          weekday: 'short',
          day: 'numeric',
          month: 'numeric',
          timeZone: 'Europe/Prague',
        })
        forecastDays.push({ date: ymd, label, kwh: Math.round(kwh * 10) / 10 })
      }
      setForecastWeek(forecastDays)

      const telemetryBySlot = new Map<string, Telemetry15m7dRow>()
      if (Array.isArray(telemetry15m7d)) {
        for (const r of telemetry15m7d) {
          telemetryBySlot.set(slotTimeKey(new Date(r.slot_start).getTime()), r)
        }
      }

      const auditMap = new Map<string, AuditTodayHourlyRow>()
      if (Array.isArray(auditHourly)) {
        for (const r of auditHourly) {
          auditMap.set(pragueHourKey(new Date(r.hour_local).getTime()), r)
        }
      }

      const logs = Array.isArray(modeLog) ? modeLog : []
      const activeMode = status?.active_mode ?? 'AUTO'

      const built: SlotData[] = []
      for (let i = 0; i < TOTAL_SLOTS; i++) {
        const startMs = windowStart + i * SLOT_MS
        const iso = new Date(startMs).toISOString()
        const base = emptySlot(iso)
        const k = slotTimeKey(startMs)

        const tel = telemetryBySlot.get(k)
        if (tel) {
          base.pv_power_w = tel.avg_pv_w ?? base.pv_power_w
          base.battery_power_w = tel.avg_battery_w ?? base.battery_power_w
          base.grid_power_w = tel.avg_grid_w ?? base.grid_power_w
          base.load_power_w = tel.avg_load_w ?? base.load_power_w
          base.soc_actual_pct = parseNum(tel.last_soc_pct) ?? base.soc_actual_pct
        }

        const aud = auditMap.get(pragueHourKey(startMs))
        if (aud) {
          if (base.pv_power_w == null && aud.avg_pv_kw != null) {
            base.pv_power_w = Math.round((parseNum(aud.avg_pv_kw) ?? 0) * 1000)
          }
          if (base.load_power_w == null && aud.avg_load_kw != null) {
            base.load_power_w = Math.round((parseNum(aud.avg_load_kw) ?? 0) * 1000)
          }
          if (base.battery_power_w == null && aud.avg_battery_kw != null) {
            base.battery_power_w = Math.round((parseNum(aud.avg_battery_kw) ?? 0) * 1000)
          }
          if (base.grid_power_w == null && aud.avg_grid_kw != null) {
            base.grid_power_w = Math.round((parseNum(aud.avg_grid_kw) ?? 0) * 1000)
          }
          if (base.soc_actual_pct == null) {
            base.soc_actual_pct = parseNum(aud.avg_soc_pct) ?? base.soc_actual_pct
          }
        }

        const pr = priceBySlot.get(k)
        if (pr) {
          base.buy_price = pr.buy
          base.sell_price = pr.sell
        }

        const corr = correctedBySlot.get(k)
        if (corr != null) {
          base.pv_forecast_corrected_w = corr
          // Dashboard neřeší přesné rozdělení po polích; pro UI rozpad použijeme stabilní poměr.
          base.pv_a_forecast_w = Math.round(corr * 0.6)
          base.pv_b_forecast_w = Math.round(corr * 0.4)
        }

        const pi = planBySlot.get(k)
        if (pi) mergeInterval(base, pi)

        const past = i <= nIdx
        const regimePast = modeAt(logs, startMs) ?? activeMode
        built.push({
          ...base,
          regime_code: past ? regimePast : activeMode,
          regime_is_planned: !past,
        })
      }

      const liveSoc = parseNum(status?.battery_soc_percent)
      if (liveSoc != null && nIdx >= 0 && nIdx < built.length) {
        const cur = built[nIdx]!
        built[nIdx] = { ...cur, soc_actual_pct: liveSoc }
      }

      const neg: NegPriceItem[] = []
      const nowMs = Date.now()
      for (const r of flatPrices) {
        const t = new Date(r.interval_start).getTime()
        if (t < nowMs) continue
        const buy = parseNum(r.effective_buy_price_czk_kwh)
        const sell = parseNum(r.effective_sell_price_czk_kwh)
        if ((buy != null && buy < 0) || (sell != null && sell < 0)) {
          neg.push({
            interval_start: r.interval_start,
            buy,
            sell,
          })
        }
      }
      neg.sort((a, b) => new Date(a.interval_start).getTime() - new Date(b.interval_start).getTime())
      setNegPrices(neg.slice(0, 32))

      setSlots(built)
      setError(null)
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Chyba načítání dashboardu')
      // Sloty neměnit — během chyby refetche zůstávají zobrazená poslední data.
    } finally {
      setSlotsReady(true)
    }
  }, [siteId])

  useEffect(() => {
    // Změna lokality: data staré site nesmí zůstat zobrazená.
    setSlots([])
    setSlotsReady(false)
    void load()
    const id = window.setInterval(() => void load(), POLL_FULL_MS)
    return () => window.clearInterval(id)
  }, [load])

  useEffect(() => {
    if (siteId == null) {
      setLiveMetrics(null)
      return
    }
    const fetchLive = async () => {
      try {
        const [statusArr, hpArr] = await Promise.all([
          getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
          getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
        ])
        const status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
        const hp = Array.isArray(hpArr) && hpArr[0] ? hpArr[0]! : null
        setLiveMetrics(buildLiveMetrics(status, hp))
      } catch {
        /* ignore */
      }
    }
    void fetchLive()
    const id = window.setInterval(() => void fetchLive(), POLL_LIVE_MS)
    return () => window.clearInterval(id)
  }, [siteId])

  useEffect(() => {
    if (siteId == null) {
      wsRef.current?.close()
      wsRef.current = null
      return
    }
    const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
    const ws = new WebSocket(`${proto}://${window.location.host}/ws/telemetry`)
    wsRef.current = ws
    ws.onmessage = (ev) => {
      try {
        const msg = JSON.parse(ev.data as string) as Record<string, unknown>
        if (msg.type !== 'telemetry' || Number(msg.site_id) !== siteIdRef.current) return

        const pv = numFromWs(msg.pv_power_w)
        const batW = numFromWs(msg.battery_power_w)
        const gridW = numFromWs(msg.grid_power_w)
        const loadW = numFromWs(msg.load_power_w)
        const genW = numFromWs(msg.gen_port_power_w)
        const soc = numFromWs(msg.battery_soc_pct)

        setLiveMetrics((prev) => ({
          pv_w: pv ?? prev?.pv_w ?? null,
          load_w: loadW ?? prev?.load_w ?? null,
          grid_w: gridW ?? prev?.grid_w ?? null,
          bat_soc: soc ?? prev?.bat_soc ?? null,
          bat_w: batW ?? prev?.bat_w ?? null,
        }))

        const tsStr = typeof msg.ts === 'string' ? msg.ts : null
        if (!tsStr) return
        const tsMs = new Date(tsStr).getTime()
        if (!Number.isFinite(tsMs)) return

        setSlots((prev) => {
          const idx = prev.findIndex((s) => {
            const start = new Date(s.interval_start).getTime()
            return start <= tsMs && tsMs < start + SLOT_MS
          })
          if (idx === -1) return prev
          const cur = prev[idx]!
          const updated = [...prev]
          updated[idx] = {
            ...cur,
            pv_power_w: pv ?? cur.pv_power_w,
            battery_power_w: batW ?? cur.battery_power_w,
            grid_power_w: gridW ?? cur.grid_power_w,
            load_power_w: loadW ?? cur.load_power_w,
            gen_port_power_w: genW ?? cur.gen_port_power_w,
            soc_actual_pct: soc ?? cur.soc_actual_pct,
          }
          return updated
        })
      } catch {
        /* ignore */
      }
    }
    ws.onclose = () => {
      if (wsRef.current === ws) wsRef.current = null
    }
    return () => {
      ws.close()
      if (wsRef.current === ws) wsRef.current = null
    }
  }, [siteId])

  const liveNowIndex = useMemo(
    () => currentSlotIndexInWindow(floorSlotUtcMs(Date.now()) - SLOT_COUNT_BACK * SLOT_MS),
    [slots],
  )

  const buyNow =
    slots.length && liveNowIndex >= 0 && liveNowIndex < slots.length
      ? slots[liveNowIndex]!.buy_price
      : null

  const sellNow =
    slots.length && liveNowIndex >= 0 && liveNowIndex < slots.length
      ? slots[liveNowIndex]!.sell_price
      : null

  return {
    slots,
    nowIndex: liveNowIndex,
    forecastWeek,
    negPrices,
    error,
    ready,
    slotsReady,
    reload: load,
    liveMetrics,
    buyNow,
    sellNow,
  }
}
