-- sloty pro LP: ceny, forecast, baseline, EV připojení + masky allow_charge / allow_discharge_export
-- DROP: změna RETURNS TABLE (nové sloupce) — CREATE OR REPLACE na rozdílný row type v PG neprojde.
-- Musí být plná signatura (pg_proc ukládá int jako integer); DROP bez () funkci se směrem nemaže.

drop function if exists ems.fn_load_planning_slots_full(
  integer, timestamp with time zone, timestamp with time zone, numeric
);

create or replace function ems.fn_load_planning_slots_full(
  p_site_id int,
  p_from timestamptz,
  p_to timestamptz,
  p_current_soc_wh numeric
)
returns table (
  slot_ord int,
  interval_start timestamptz,
  buy_price numeric,
  sell_price numeric,
  is_predicted_price boolean,
  pv_a_forecast_w int,
  pv_b_forecast_w int,
  load_baseline_w int,
  ev1_connected boolean,
  ev2_connected boolean,
  allow_charge boolean,
  allow_discharge_export boolean,
  night_baseload_target_wh numeric,
  night_baseload_buffer_wh numeric,
  safety_soc_target_wh numeric,
  future_avoided_buy_czk_kwh numeric,
  future_sell_opportunity_czk_kwh numeric,
  is_daytime_pv_surplus_slot boolean,
  charge_acquisition_buy_czk_kwh numeric,
  charge_acquisition_cutoff_at timestamptz
)
language plpgsql
volatile
as $fn$
declare
  v_charge_buf numeric;
  v_discharge_buf numeric;
  v_usable numeric;
  v_min_soc_wh numeric;
  v_soc_max_wh numeric;
  v_energy_to_fill numeric;
  v_exportable numeric;
  v_charge_eff numeric;
  v_discharge_eff numeric;
  v_max_charge_w numeric;
  v_max_discharge_w numeric;
  v_per_slot_charge_wh numeric;
  v_per_slot_discharge_wh numeric;
  v_grid_target_wh numeric;
  v_discharge_target_wh numeric;
  v_cum numeric;
  r_slot record;
  v_n_am int;
  v_n_pm int;
  v_chg_am_wh numeric;
  v_chg_pm_wh numeric;
  v_reserve_wh numeric;
  v_daytime_en boolean;
  v_night_buf_pct numeric;
  v_degrad_czk_kwh numeric;
  v_ref_buy_czk_kwh numeric;
  v_ref_buy_am_czk_kwh numeric;
  v_ref_buy_pm_czk_kwh numeric;
  v_purchase_pricing_mode text;
  v_lookahead_slots int := 4;
  v_grid_charge_cap_am int;
  v_grid_charge_cap_pm int;
  v_buy_lookahead_eps numeric := 0.05;
  v_buy_charge_band_czk_kwh numeric := 0.40;
  v_grid_filled_wh numeric := 0;
  v_pv_layer_cap_wh numeric;
  v_grid_slots_am int := 0;
  v_grid_slots_pm int := 0;
  v_acquisition_cutoff timestamptz;
  v_charge_acquisition numeric;
  v_est_grid_wh numeric;
  v_est_pv_wh numeric;
  v_est_grid_cost numeric;
  v_est_pv_cost numeric;
  v_export_window_start timestamptz;
  v_plan_day_prague date;
begin
  v_plan_day_prague := (p_from at time zone 'Europe/Prague')::date;
  drop table if exists _ems_plan_slot_wk;
  create temp table _ems_plan_slot_wk on commit drop as
  with
  slot_spine as (
    select gs as interval_start
    from generate_series(
      p_from,
      (p_to - interval '15 minutes')::timestamptz,
      interval '15 minutes'
    ) as gs
  ),
  pv_canon as (
    select
      r.interval_start,
      coalesce(r.pv_a_forecast_canonical_w, 0)::int as pv_a_forecast_w,
      coalesce(r.pv_b_forecast_canonical_w, 0)::int as pv_b_forecast_w
    from jsonb_to_recordset(
      ems.fn_forecast_pv_slots_range_canonical_ab(
        p_site_id,
        p_from,
        p_to,
        now(),
        greatest(p_from, now() - interval '120 days'),
        now()
      )
    ) as r(
      interval_start timestamptz,
      pv_a_forecast_canonical_w bigint,
      pv_b_forecast_canonical_w bigint
    )
  )
  select
    (row_number() over (order by s.interval_start) - 1)::int as slot_ord,
    s.interval_start,
    coalesce(
      ep.effective_buy_price_czk_kwh,
      ems.fn_get_predicted_price(p_site_id, s.interval_start)
    ) as buy_price,
    coalesce(
      ep.effective_sell_price_czk_kwh,
      ems.fn_get_predicted_price(p_site_id, s.interval_start) * 0.85
    ) as sell_price,
    (ep.effective_buy_price_czk_kwh is null) as is_predicted_price,
    coalesce(pv.pv_a_forecast_w, 0)::int as pv_a_forecast_w,
    coalesce(pv.pv_b_forecast_w, 0)::int as pv_b_forecast_w,
    coalesce(
      (
        select bs.avg_power_w
        from ems.consumption_baseline_stats bs
        where bs.site_id = p_site_id
          and bs.day_of_week = extract(
            dow from s.interval_start at time zone 'Europe/Prague'
          )::int
          and bs.hour_of_day = extract(
            hour from s.interval_start at time zone 'Europe/Prague'
          )::int
        limit 1
      ),
      500
    )::int as load_baseline_w,
    (coalesce(ev1.status, 'available') not in ('available', 'unavailable')) as ev1_connected,
    (coalesce(ev2.status, 'available') not in ('available', 'unavailable')) as ev2_connected,
    greatest(
      0,
      coalesce(pv.pv_a_forecast_w, 0) + coalesce(pv.pv_b_forecast_w, 0)
      - coalesce(
          (
            select bs.avg_power_w
            from ems.consumption_baseline_stats bs
            where bs.site_id = p_site_id
              and bs.day_of_week = extract(
                dow from s.interval_start at time zone 'Europe/Prague'
              )::int
              and bs.hour_of_day = extract(
                hour from s.interval_start at time zone 'Europe/Prague'
              )::int
            limit 1
          ),
          500
        )
    )::int as pv_surplus_w,
    false::boolean as allow_charge,
    false::boolean as allow_discharge_export
  from slot_spine s
  left join pv_canon pv on pv.interval_start = s.interval_start
  left join ems.vw_site_effective_price ep
    on ep.site_id = p_site_id and ep.interval_start = s.interval_start
  left join lateral (
    select t.status
    from ems.telemetry_ev_charger t
    join ems.asset_ev_charger ch on ch.id = t.charger_id
    where t.site_id = p_site_id and ch.code = 'ev-charger-1'
    order by t.measured_at desc
    limit 1
  ) ev1 on true
  left join lateral (
    select t.status
    from ems.telemetry_ev_charger t
    join ems.asset_ev_charger ch on ch.id = t.charger_id
    where t.site_id = p_site_id and ch.code = 'ev-charger-2'
    order by t.measured_at desc
    limit 1
  ) ev2 on true;

  if not exists (select 1 from _ems_plan_slot_wk) then
    raise exception 'No planning slots available – check market prices and horizon settings';
  end if;

  select
    coalesce(ab.charge_slot_buffer, 0::numeric),
    coalesce(ab.discharge_slot_buffer, 0::numeric),
    ab.usable_capacity_wh::numeric,
    (ab.min_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    (coalesce(ab.planner_max_soc_percent, ab.max_soc_percent) / 100.0 * ab.usable_capacity_wh)::numeric,
    greatest(coalesce(ab.charge_efficiency, 1::numeric), 0.0001::numeric),
    least(
      coalesce(ai.max_battery_charge_w, ai.max_charge_power_w),
      coalesce(
        ab.bms_max_charge_w,
        case when ab.max_charge_c_rate is not null
          then (ab.max_charge_c_rate * ab.usable_capacity_wh)::bigint
        end,
        coalesce(ai.max_battery_charge_w, ai.max_charge_power_w)
      )
    )::numeric,
    least(
      coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w),
      coalesce(
        ab.bms_max_discharge_w,
        case when ab.max_discharge_c_rate is not null
          then (ab.max_discharge_c_rate * ab.usable_capacity_wh)::bigint
        end,
        coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w)
      )
    )::numeric,
    greatest(coalesce(ab.discharge_efficiency, 1::numeric), 0.0001::numeric),
    (ab.reserve_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    coalesce(ab.planner_daytime_charge_target_enabled, true),
    coalesce(ab.planner_night_baseload_buffer_percent, 20::numeric),
    coalesce(ab.degradation_cost_czk_kwh, 0.15::numeric)
  into
    v_charge_buf,
    v_discharge_buf,
    v_usable,
    v_min_soc_wh,
    v_soc_max_wh,
    v_charge_eff,
    v_max_charge_w,
    v_max_discharge_w,
    v_discharge_eff,
    v_reserve_wh,
    v_daytime_en,
    v_night_buf_pct,
    v_degrad_czk_kwh
  from ems.asset_battery ab
  join ems.asset_inverter ai on ai.id = ab.inverter_id and ai.site_id = ab.site_id
  where ab.site_id = p_site_id
  order by ab.id
  limit 1;

  if v_usable is null then
    raise exception 'No asset_battery for site_id=%', p_site_id;
  end if;

  select coalesce(smc.purchase_pricing_mode, 'spot')
  into v_purchase_pricing_mode
  from ems.site_market_config smc
  where smc.site_id = p_site_id
    and smc.valid_from <= p_from
    and (smc.valid_to is null or smc.valid_to > p_from)
  order by smc.valid_from desc
  limit 1;

  v_purchase_pricing_mode := coalesce(v_purchase_pricing_mode, 'spot');

  v_per_slot_charge_wh := v_max_charge_w * v_charge_eff * 0.25;
  v_per_slot_discharge_wh := v_max_discharge_w * v_discharge_eff * 0.25;
  v_energy_to_fill := v_soc_max_wh - p_current_soc_wh;
  v_exportable := v_soc_max_wh - v_min_soc_wh;
  -- Rozpočet masek: charge_slot_buffer zvětší Wh cíl (do soc_max) i cap počtu grid slotů.
  if v_charge_buf > 0 then
    v_grid_target_wh := least(
      greatest(v_energy_to_fill, 0) * v_charge_buf,
      greatest(v_soc_max_wh - p_current_soc_wh, 0)
    );
  elsif p_current_soc_wh >= v_reserve_wh then
    v_grid_target_wh := greatest(v_energy_to_fill, 0);
  else
    v_grid_target_wh := least(
      greatest(v_energy_to_fill, 0) * v_charge_buf,
      greatest(v_energy_to_fill, 0)
    );
  end if;
  v_discharge_target_wh := v_exportable * v_discharge_buf;

  -- Referenční nákup: globální min (export brána) + per AM/PM pás (grid nabíjení).
  select coalesce(min(wk.buy_price), 0)
  into v_ref_buy_czk_kwh
  from _ems_plan_slot_wk wk;

  select coalesce(min(wk.buy_price), v_ref_buy_czk_kwh)
  into v_ref_buy_am_czk_kwh
  from _ems_plan_slot_wk wk
  where extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12;

  select coalesce(min(wk.buy_price), v_ref_buy_czk_kwh)
  into v_ref_buy_pm_czk_kwh
  from _ems_plan_slot_wk wk
  where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;

  -- První výkupní okno: sell nad min(buy) **téhož kalendářního dne** (Prague), ne globální
  -- min(buy) zítra (NT) — jinak okno začne už ~15:30 a dnešní PM grid dostane 1 slot.
  select min(wk.interval_start)
  into v_export_window_start
  from _ems_plan_slot_wk wk
  where wk.sell_price > (
    select coalesce(min(w2.buy_price), wk.buy_price) + v_degrad_czk_kwh
    from _ems_plan_slot_wk w2
    where (w2.interval_start at time zone 'Europe/Prague')::date
      = (wk.interval_start at time zone 'Europe/Prague')::date
  );

  -- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
  alter table _ems_plan_slot_wk
    add column if not exists future_sell_lookahead numeric,
    add column if not exists buy_min_next_n numeric,
    add column if not exists store_score numeric,
    add column if not exists allow_grid_charge boolean default false;

  update _ems_plan_slot_wk wk
  set
    future_sell_lookahead = coalesce(
      (
        select max(w2.sell_price)
        from _ems_plan_slot_wk w2
        where w2.slot_ord > wk.slot_ord
      ),
      wk.sell_price
    ),
    buy_min_next_n = (
      select min(w2.buy_price)
      from _ems_plan_slot_wk w2
      where w2.slot_ord > wk.slot_ord
        and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
        and (
          v_export_window_start is null
          or w2.interval_start < v_export_window_start
        )
    ),
    store_score =
      coalesce(
        (
          select max(w2.sell_price)
          from _ems_plan_slot_wk w2
          where w2.slot_ord > wk.slot_ord
        ),
        wk.sell_price
      )
      - wk.sell_price
      - greatest(0::numeric, wk.buy_price - wk.sell_price);

  -- AM/PM rozpočet grid charging (Europe/Prague 00–12 vs 12–24).
  -- Chybějící segment dostane celý budget.
  select
    coalesce(count(*) filter (
      where extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
    ), 0)::int,
    coalesce(count(*) filter (
      where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
    ), 0)::int
  into v_n_am, v_n_pm
  from _ems_plan_slot_wk wk;

  if v_n_am <= 0 then
    v_chg_am_wh := 0;
    v_chg_pm_wh := v_grid_target_wh;
  elsif v_n_pm <= 0 then
    v_chg_am_wh := v_grid_target_wh;
    v_chg_pm_wh := 0;
  else
    v_chg_am_wh := v_grid_target_wh / 2.0;
    v_chg_pm_wh := v_grid_target_wh - v_chg_am_wh;
  end if;

  if v_per_slot_charge_wh > 0 then
    if v_charge_buf > 0 then
      v_grid_charge_cap_am := greatest(
        1,
        least(24, ceil((v_chg_am_wh / v_per_slot_charge_wh) * v_charge_buf)::int)
      );
      v_grid_charge_cap_pm := greatest(
        1,
        least(24, ceil((v_chg_pm_wh / v_per_slot_charge_wh) * v_charge_buf)::int)
      );
    else
      v_grid_charge_cap_am := greatest(
        1,
        least(24, ceil(v_chg_am_wh / v_per_slot_charge_wh)::int)
      );
      v_grid_charge_cap_pm := greatest(
        1,
        least(24, ceil(v_chg_pm_wh / v_per_slot_charge_wh)::int)
      );
    end if;
  else
    v_grid_charge_cap_am := 6;
    v_grid_charge_cap_pm := 6;
  end if;

  -- charge mask: grid arbitráž (B) před FVE (A); AM/PM rozpočet Wh zůstává 50/50.
  --
  --   B) Grid ze sítě: spot, nejlevnější sloty v AM/PM do Wh rozpočtu (den plánu → před exportním oknem → buy ASC);
  --      i při pv_surplus>0; cap slotů ∝ rozpočet Wh / per_slot_charge_wh.
  --   A) PV-surplus: store_score DESC, doplní jen zbytek do energy_to_fill po vrstvě B.
  if v_charge_buf <= 0 then
    update _ems_plan_slot_wk wk set allow_charge = true;
  elsif v_energy_to_fill <= 0 then
    update _ems_plan_slot_wk wk set allow_charge = true;
  else
    update _ems_plan_slot_wk wk set allow_charge = false;
    v_grid_filled_wh := 0;

    if v_purchase_pricing_mode <> 'fixed' then
      -- B) Grid AM (dříve než PV, vlastní 50 % rozpočtu Wh)
      v_cum := 0;
      v_grid_slots_am := 0;
      for r_slot in
        select wk.slot_ord
        from _ems_plan_slot_wk wk
        where extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
        order by
          case
            when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
            then 0
            else 1
          end,
          case
            when v_export_window_start is not null
              and wk.interval_start < v_export_window_start
            then 0
            else 1
          end,
          wk.is_predicted_price::int,
          wk.buy_price,
          wk.slot_ord
      loop
        exit when v_cum >= v_chg_am_wh;
        exit when v_per_slot_charge_wh <= 0;
        exit when v_grid_slots_am >= v_grid_charge_cap_am;
        update _ems_plan_slot_wk wk
        set allow_charge = true, allow_grid_charge = true
        where wk.slot_ord = r_slot.slot_ord;
        v_cum := v_cum + v_per_slot_charge_wh;
        v_grid_slots_am := v_grid_slots_am + 1;
      end loop;
      v_grid_filled_wh := v_grid_filled_wh + v_cum;

      -- PM dostane i nevyčerpaný AM rozpočet (levné NT dopoledne ≠ vyčerpání celého grid_target).
      v_chg_pm_wh := greatest(v_chg_pm_wh, v_grid_target_wh - v_grid_filled_wh);
      if v_per_slot_charge_wh > 0 and v_charge_buf > 0 then
        v_grid_charge_cap_pm := greatest(
          v_grid_charge_cap_pm,
          least(24, ceil((v_chg_pm_wh / v_per_slot_charge_wh) * v_charge_buf)::int)
        );
      elsif v_per_slot_charge_wh > 0 then
        v_grid_charge_cap_pm := greatest(
          v_grid_charge_cap_pm,
          least(24, ceil(v_chg_pm_wh / v_per_slot_charge_wh)::int)
        );
      end if;

      -- B) Grid PM
      v_cum := 0;
      v_grid_slots_pm := 0;
      for r_slot in
        select wk.slot_ord
        from _ems_plan_slot_wk wk
        where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
        order by
          case
            when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
            then 0
            else 1
          end,
          case
            when v_export_window_start is not null
              and wk.interval_start < v_export_window_start
            then 0
            else 1
          end,
          wk.is_predicted_price::int,
          wk.buy_price,
          wk.slot_ord
      loop
        exit when v_cum >= v_chg_pm_wh;
        exit when v_per_slot_charge_wh <= 0;
        exit when v_grid_slots_pm >= v_grid_charge_cap_pm;
        update _ems_plan_slot_wk wk
        set allow_charge = true, allow_grid_charge = true
        where wk.slot_ord = r_slot.slot_ord;
        v_cum := v_cum + v_per_slot_charge_wh;
        v_grid_slots_pm := v_grid_slots_pm + 1;
      end loop;
      v_grid_filled_wh := v_grid_filled_wh + v_cum;
    end if;

    -- A) PV-surplus: jen zbytek kapacity po grid vrstvě B
    v_pv_layer_cap_wh := greatest(v_energy_to_fill - v_grid_filled_wh, 0);
    v_cum := 0;
    for r_slot in
      select wk.slot_ord, wk.pv_surplus_w
      from _ems_plan_slot_wk wk
      where wk.pv_surplus_w > 0
        and wk.sell_price >= wk.buy_price - v_degrad_czk_kwh
        -- Držet PV na večerní peak: ne nabíjet z FVE když sell výrazně pod budoucím výkupním oknem.
        and wk.sell_price >= wk.future_sell_lookahead - v_degrad_czk_kwh
      order by wk.store_score desc nulls last, wk.slot_ord
    loop
      exit when v_cum >= v_pv_layer_cap_wh;
      update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
      v_cum := v_cum + least(r_slot.pv_surplus_w, v_max_charge_w) * v_charge_eff * 0.25;
    end loop;
  end if;

  -- discharge-export mask
  if v_discharge_buf <= 0 then
    update _ems_plan_slot_wk wk set allow_discharge_export = true;
  elsif v_exportable <= 0 then
    update _ems_plan_slot_wk wk set allow_discharge_export = false;
  else
    update _ems_plan_slot_wk wk set allow_discharge_export = false;
    v_cum := 0;
    for r_slot in
      select wk.slot_ord
      from _ems_plan_slot_wk wk
      where (
        case
          when v_purchase_pricing_mode = 'fixed' then
            wk.sell_price > v_degrad_czk_kwh
          else
            wk.sell_price > v_ref_buy_czk_kwh + v_degrad_czk_kwh
        end
      )
      order by wk.sell_price desc, wk.slot_ord desc
    loop
      exit when v_cum >= v_discharge_target_wh;
      exit when v_per_slot_discharge_wh <= 0;
      update _ems_plan_slot_wk wk set allow_discharge_export = true where wk.slot_ord = r_slot.slot_ord;
      v_cum := v_cum + v_per_slot_discharge_wh;
    end loop;
  end if;

  -- Vážená acquisition cena zásoby (grid + FVE opportunity) jen pro sloty PŘED prvním
  -- plánovaným exportem z baterie — nepočítá nákup po večerním/nočním vybití do sítě.
  select min(wk.interval_start)
  into v_acquisition_cutoff
  from _ems_plan_slot_wk wk
  where wk.allow_discharge_export;

  -- Acquisition: jen grid vrstva B (ne odpolední FVE z vrstvy A) před 1. exportem.
  select
    coalesce(sum(
      case
        when wk.allow_grid_charge
          and (
            v_acquisition_cutoff is null
            or wk.interval_start < v_acquisition_cutoff
          )
        then v_per_slot_charge_wh
        else 0
      end
    ), 0),
    coalesce(sum(
      case
        when wk.allow_grid_charge
          and (
            v_acquisition_cutoff is null
            or wk.interval_start < v_acquisition_cutoff
          )
        then wk.buy_price * v_per_slot_charge_wh
        else 0
      end
    ), 0),
    min(
      case
        when wk.allow_grid_charge
          and (
            v_acquisition_cutoff is null
            or wk.interval_start < v_acquisition_cutoff
          )
        then wk.buy_price
        else null
      end
    )
  into
    v_est_grid_wh,
    v_est_grid_cost,
    v_charge_acquisition
  from _ems_plan_slot_wk wk;

  v_est_pv_wh := 0;
  v_est_pv_cost := 0;

  if v_est_grid_wh > 0 then
    v_charge_acquisition := v_est_grid_cost / v_est_grid_wh;
  elsif v_charge_acquisition is null then
    v_charge_acquisition := coalesce(
      (v_ref_buy_am_czk_kwh + v_ref_buy_pm_czk_kwh) / 2.0,
      v_ref_buy_czk_kwh
    );
  end if;
  -- v_charge_acquisition z min(grid) zůstane, pokud je jen jeden grid slot před exportem

  return query
  with night_tot as (
    select coalesce(sum(w2.load_baseline_w), 0) * 0.25 as night_wh
    from _ems_plan_slot_wk w2
    where extract(hour from w2.interval_start at time zone 'Europe/Prague') >= 20
       or extract(hour from w2.interval_start at time zone 'Europe/Prague') < 6
  ),
  enriched as (
    select
      w.slot_ord,
      w.interval_start,
      w.buy_price,
      w.sell_price,
      w.is_predicted_price,
      w.pv_a_forecast_w,
      w.pv_b_forecast_w,
      w.load_baseline_w,
      w.ev1_connected,
      w.ev2_connected,
      w.allow_charge,
      w.allow_discharge_export,
      nt.night_wh as night_baseload_target_wh,
      nt.night_wh * (v_night_buf_pct / 100.0) as night_baseload_buffer_wh,
      case
        when not v_daytime_en then null::numeric
        when extract(hour from w.interval_start at time zone 'Europe/Prague') between 6 and 19 then
          least(
            v_soc_max_wh,
            v_reserve_wh + (nt.night_wh + nt.night_wh * (v_night_buf_pct / 100.0))
              * greatest(
                0::numeric,
                least(
                  1::numeric,
                  (
                    extract(hour from w.interval_start at time zone 'Europe/Prague')::numeric
                    + (
                      extract(minute from w.interval_start at time zone 'Europe/Prague')::numeric
                      / 60.0
                    )
                    - 6.0
                  ) / 14.0
                )
              )
          )
        else null::numeric
      end as safety_soc_target_wh,
      coalesce(
        max(w.buy_price) over (
          order by w.slot_ord rows between 1 following and unbounded following
        ),
        w.buy_price
      ) as future_avoided_buy_czk_kwh,
      coalesce(
        max(w.sell_price) over (
          order by w.slot_ord rows between 1 following and unbounded following
        ),
        w.sell_price
      ) as future_sell_opportunity_czk_kwh,
      (
        extract(hour from w.interval_start at time zone 'Europe/Prague') between 6 and 18
        and w.pv_surplus_w > 0
      ) as is_daytime_pv_surplus_slot
    from _ems_plan_slot_wk w
    cross join night_tot nt
  )
  select
    e.slot_ord,
    e.interval_start,
    e.buy_price,
    e.sell_price,
    e.is_predicted_price,
    e.pv_a_forecast_w,
    e.pv_b_forecast_w,
    e.load_baseline_w,
    e.ev1_connected,
    e.ev2_connected,
    e.allow_charge,
    e.allow_discharge_export,
    e.night_baseload_target_wh,
    e.night_baseload_buffer_wh,
    e.safety_soc_target_wh,
    e.future_avoided_buy_czk_kwh,
    e.future_sell_opportunity_czk_kwh,
    e.is_daytime_pv_surplus_slot,
    v_charge_acquisition as charge_acquisition_buy_czk_kwh,
    v_acquisition_cutoff as charge_acquisition_cutoff_at
  from enriched e
  order by e.slot_ord;
end;
$fn$;

comment on function ems.fn_load_planning_slots_full is
  '15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
  'Charge mask A: PV-surplus dle store_score DESC (future_sell−sell−max(0,buy−sell)); zbytek → PV export. '
  'Charge mask B: spot, nejlevnější buy v AM/PM do Wh rozpočtu (priorita den plánu, před exportním oknem). '
  'ref_buy = min(buy) horizontu. Discharge-export: nejdražší sell kde sell>ref_buy+degrad (spot). '
  'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
  'Denní safety vstupy: night_baseload_* (20:00–06:00 Europe/Prague), safety_soc_target_wh (6–19), '
  'lookahead max buy/sell pro měkké LP penalizace. '
  'charge_acquisition_buy_czk_kwh: vážený buy v allow_charge slotech před charge_acquisition_cutoff_at. '
  'Grid maska B běží před PV vrstvou A; AM/PM rozpočet Wh 50/50; cap slotů z rozpočtu / per_slot_charge_wh.';
