-- jeden EV session objekt pro LP (fn_planning_site_context).
-- Vrací jsonb objekt session na daném wallboxu, nebo null::jsonb pokud session
-- není nebo nemá použitelná data (kapacita vozidla, SoC při připojení).
--
-- KLÍČOVÝ ROZDÍL oproti dřívější inline logice (bug 2026-06-13): session se
-- NEVYŘAZUJE jen proto, že needed_wh = 0 (auto už nad targetem). Plánovač pak
-- neviděl ~6 kW zátěž auta a špatně rozvrhl baterii. Session zůstává v plánu,
-- dokud má oportunistický headroom (cena rozhodne, jestli se nabíjí) — měkký
-- cíl řeší solver dekompozicí Σ == needed − unmet + opp.
--
-- ŽIVÉ SoC (fix 2026-06-14, phantom okna): needed_wh i headroom se počítají z
-- ŽIVÉHO SoC = soc_at_connect + integrovaná dodaná energie (fn_ev_session_delivered_wh),
-- ne ze zamrzlého soc_at_connect. Dřív se odečítalo es.energy_delivered_wh, JENŽE
-- ten sloupec se během session NIKDY nezapisoval (trvale 0) → needed_wh konstantní
-- → plánovač slepý k pokroku nabíjení → 11 kW phantom okna i u plného auta.
-- NEpoužíváme energy_kwh counter (Telto reg 39 na TeltoCharge neakumuluje —
-- ověřeno: 17.4 kWh nabito, counter stál na 0.18 kWh), proto integrál power_w.
-- live_soc clamp 99 % (finální taper k 100 % ignorujeme). Fallback na
-- energy_delivered_wh drží staré fixtures bez telemetrie identické.
--
-- Vyřazení (null) jen když chybí tvrdá data:
--   - žádná otevřená session na wallboxu, nebo
--   - neznámá kapacita vozidla / SoC při připojení (nelze spočítat Wh).
-- target_deadline SMÍ být NULL (žádný tvrdý cíl) — solver to zvládá
--   (deadline constraint se aplikuje jen při needed_wh > 0).

-- Dodaná energie do auta za session = time-weighted integrál power_w z
-- telemetry_ev_charger (1min). dt cap 120 s ať výpadek telemetrie nezkresluje.
-- Wh (AC, bez korekce na AC→DC ztráty — mírně optimistické = méně phantom,
-- žádoucí směr). Vrací 0 bez telemetrie (drží staré chování).
drop function if exists ems.fn_ev_session_delivered_wh;

create or replace function ems.fn_ev_session_delivered_wh(
  p_charger_id int,
  p_since timestamptz
)
returns numeric
language sql
stable
as $fn$
  select coalesce(sum(
    power_w * least(coalesce(dt, 60), 120)
  ) / 3600.0, 0)::numeric
  from (
    select power_w,
      extract(epoch from (
        measured_at - lag(measured_at) over (order by measured_at)
      )) as dt
    from ems.telemetry_ev_charger
    where charger_id = p_charger_id
      and measured_at >= p_since
  ) q;
$fn$;

comment on function ems.fn_ev_session_delivered_wh is
  'Dodaná energie do EV za session (Wh, AC) = time-weighted integrál power_w z telemetry_ev_charger (dt cap 120 s). NEpoužívá energy_kwh counter (Telto reg 39 neakumuluje). Vstup živého SoC ve fn_ev_session_planning_json. 0 bez telemetrie.';

drop function if exists ems.fn_ev_session_planning_json;

create or replace function ems.fn_ev_session_planning_json(
  p_site_id int,
  p_charger_code text
)
returns jsonb
language sql
stable
as $fn$
  with s as (
    select
      es.soc_at_connect_pct,
      es.target_soc_pct,
      es.target_deadline,
      es.energy_delivered_wh,
      es.opportunistic_value_czk_kwh,
      v.battery_capacity_kwh,
      v.default_target_soc_pct,
      v.opportunistic_value_czk_kwh as v_opp,
      ems.fn_ev_session_delivered_wh(es.charger_id, es.session_start) as live_delivered_wh
    from ems.ev_session es
    join ems.asset_ev_charger ch on ch.id = es.charger_id
    left join ems.asset_vehicle v on v.id = es.vehicle_id
    where es.site_id = p_site_id
      and es.session_end is null
      and ch.code = p_charger_code
    limit 1
  ),
  c as (
    select s.*,
      -- živé SoC: SoC při připojení + integrovaná dodaná energie, clamp 99 %.
      -- coalesce(live, energy_delivered_wh, 0): bez telemetrie = staré chování.
      least(99.0, s.soc_at_connect_pct::numeric
        + coalesce(s.live_delivered_wh, s.energy_delivered_wh, 0)::numeric
          / (s.battery_capacity_kwh * 1000) * 100.0) as live_soc_pct
    from s
  )
  select case
    when c.battery_capacity_kwh is null then null::jsonb
    when c.soc_at_connect_pct is null then null::jsonb
    else jsonb_build_object(
      -- tvrdý cíl: jen pokud je nastaven deadline I cílový SoC (jinak null →
      -- solver hard constraint vynechá, energy_needed_wh = 0).
      'target_deadline', case
        when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then null
        else c.target_deadline
      end,
      'energy_needed_wh', case
        when c.target_deadline is null then 0::numeric
        when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then 0::numeric
        else greatest(
          0,
          (coalesce(c.target_soc_pct, c.default_target_soc_pct)::numeric
            - c.live_soc_pct) / 100.0
            * (c.battery_capacity_kwh * 1000)
        )
      end,
      -- headroom do 99 % od max(target, ŽIVÉ SoC): „nenabíjet" (nízký target)
      -- nesmí ZVĚTŠIT oportunistickou vrstvu; auto fyzicky bere jen energii nad
      -- aktuálním SoC. Plné auto (live_soc → 99) → headroom 0. Při vypnutém
      -- oportunismu (value <= 0) headroom = 0.
      'headroom_wh', case
        when coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0) > 0 then greatest(
          0,
          (99 - greatest(
            coalesce(c.target_soc_pct, c.default_target_soc_pct, c.live_soc_pct)::numeric,
            c.live_soc_pct
          )) / 100.0 * (c.battery_capacity_kwh * 1000)
        )
        else 0
      end,
      'opportunistic_value_czk_kwh',
        coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0)
    )
  end
  from c;
$fn$;

comment on function ems.fn_ev_session_planning_json is
  'EV session objekt pro LP (fn_planning_site_context). needed_wh i headroom z ŽIVÉHO SoC = soc_at_connect + fn_ev_session_delivered_wh (integrál power_w), clamp 99 % — ne ze zamrzlého soc_at_connect (energy_delivered_wh se nikdy nezapisoval → phantom 11 kW okna). Session se NEvyřazuje při needed_wh=0 (zůstává jako známá zátěž + oportunistický headroom). Null jen bez použitelných dat (kapacita / soc_at_connect). target_deadline smí být NULL.';
