-- 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.

drop function if exists ems.fn_load_planning_slots_full;

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
)
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_dis_am_wh numeric;
  v_dis_pm_wh numeric;
  v_reserve_wh numeric;
  v_daytime_en boolean;
  v_night_buf_pct numeric;
begin
  drop table if exists _ems_plan_slot_wk;
  create temp table _ems_plan_slot_wk on commit drop as
  with prof as (
    select ems.fn_pv_forecast_delta_profile(
      p_site_id,
      greatest(p_from, now() - interval '120 days'),
      now()
    ) as j
  ),
  delta_unnest as (
    select (kv.key)::int as pv_array_id,
           (x->>'slot_of_day')::int as slot_of_day,
           (x->>'delta_w')::int as delta_w
    from prof
    cross join lateral jsonb_each((prof.j)->'deltas_by_array') kv(key, value)
    cross join lateral jsonb_array_elements(kv.value->'deltas') x
  ),
  legacy_slot_delta as (
    select (x->>'slot_of_day')::int as slot_of_day,
           (x->>'delta_w')::int as delta_w
    from prof
    cross join lateral jsonb_array_elements((prof.j)->'deltas') x
  ),
  slot_spine as (
    select gs as interval_start
    from generate_series(
      p_from,
      (p_to - interval '15 minutes')::timestamptz,
      interval '15 minutes'
    ) as gs
  )
  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(fpi_a.power_w, 0)::int as pv_a_forecast_w,
    coalesce(fpi_b.power_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(fpi_a.power_w, 0) + coalesce(fpi_b.power_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 ems.vw_site_effective_price ep
    on ep.site_id = p_site_id and ep.interval_start = s.interval_start
  left join lateral (
    with uq as (
      select distinct on (apa.id)
        apa.id as pv_array_id,
        fpi.power_w
      from ems.asset_pv_array apa
      join ems.forecast_pv_run fpr
        on fpr.pv_array_id = apa.id
       and fpr.site_id = apa.site_id
       and fpr.status = 'ok'
      join ems.forecast_pv_interval fpi
        on fpi.run_id = fpr.id
       and fpi.pv_array_id = apa.id
       and fpi.interval_start = s.interval_start
      where apa.site_id = p_site_id
        and apa.controllable is true
      order by apa.id, fpr.created_at desc
    ),
    slot_of as (
      select (
        (extract(hour from (s.interval_start at time zone 'Europe/Prague'))::int * 60)
        + extract(minute from (s.interval_start at time zone 'Europe/Prague'))::int
      ) / 15 as slot_of_day
    ),
    tot as (select coalesce(sum(uq.power_w), 0)::numeric as w from uq)
    select coalesce(sum(
      greatest(
        0,
        uq.power_w - coalesce(
          du.delta_w,
          case
            when exists (select 1 from delta_unnest limit 1) then null
            else round(
              ld.delta_w::numeric * uq.power_w::numeric / nullif((select w from tot), 0)
            )::int
          end,
          0
        )
      )
    ), 0)::int as power_w
    from uq
    cross join slot_of
    cross join tot
    left join delta_unnest du
      on du.pv_array_id = uq.pv_array_id
     and du.slot_of_day = slot_of.slot_of_day
    left join legacy_slot_delta ld
      on ld.slot_of_day = slot_of.slot_of_day
  ) fpi_a on true
  left join lateral (
    with uq as (
      select distinct on (apa.id)
        apa.id as pv_array_id,
        fpi.power_w
      from ems.asset_pv_array apa
      join ems.forecast_pv_run fpr
        on fpr.pv_array_id = apa.id
       and fpr.site_id = apa.site_id
       and fpr.status = 'ok'
      join ems.forecast_pv_interval fpi
        on fpi.run_id = fpr.id
       and fpi.pv_array_id = apa.id
       and fpi.interval_start = s.interval_start
      where apa.site_id = p_site_id
        and apa.controllable is false
      order by apa.id, fpr.created_at desc
    ),
    slot_of as (
      select (
        (extract(hour from (s.interval_start at time zone 'Europe/Prague'))::int * 60)
        + extract(minute from (s.interval_start at time zone 'Europe/Prague'))::int
      ) / 15 as slot_of_day
    ),
    tot as (select coalesce(sum(uq.power_w), 0)::numeric as w from uq)
    select coalesce(sum(
      greatest(
        0,
        uq.power_w - coalesce(
          du.delta_w,
          case
            when exists (select 1 from delta_unnest limit 1) then null
            else round(
              ld.delta_w::numeric * uq.power_w::numeric / nullif((select w from tot), 0)
            )::int
          end,
          0
        )
      )
    ), 0)::int as power_w
    from uq
    cross join slot_of
    cross join tot
    left join delta_unnest du
      on du.pv_array_id = uq.pv_array_id
     and du.slot_of_day = slot_of.slot_of_day
    left join legacy_slot_delta ld
      on ld.slot_of_day = slot_of.slot_of_day
  ) fpi_b 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-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)
  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
  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;

  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;
  v_grid_target_wh := v_energy_to_fill * v_charge_buf;
  v_discharge_target_wh := v_exportable * v_discharge_buf;

  -- Rozpočet na půl dne (Europe/Prague): 00:00–12:00 vs 12:00–24:00; chybějící segment dostane celý budget.
  -- Nabíjecí rozpočet dál dělíme 50/50 (kvůli rozprostření v rámci dne), ale exportní vybíjení volíme globálně podle sell_price.
  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;

  -- charge mask (sloupce temp tabulky kvalifikujeme: RETURNS TABLE dělá PL proměnné stejných jmen)
  if v_charge_buf <= 0 then
    update _ems_plan_slot_wk wk set allow_charge = true;
  elsif v_energy_to_fill <= 0 then
    -- Pokud rolling replan startuje s baterií plnou, nechceme zablokovat budoucí nabíjení po vybití.
    -- Povolit alespoň nabíjení v PV surplus slotech, aby solver mohl vytvořit headroom a pak ho znovu zaplnit z FVE.
    update _ems_plan_slot_wk wk set allow_charge = (wk.pv_surplus_w > 0);
  else
    update _ems_plan_slot_wk wk set allow_charge = (wk.pv_surplus_w > 0);
    v_cum := 0;
    for r_slot in
      select wk.slot_ord
      from _ems_plan_slot_wk wk
      where wk.pv_surplus_w <= 0
        and extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
      order by wk.buy_price, wk.slot_ord
    loop
      exit when v_cum >= v_chg_am_wh;
      exit when v_per_slot_charge_wh <= 0;
      update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
      v_cum := v_cum + v_per_slot_charge_wh;
    end loop;
    v_cum := 0;
    for r_slot in
      select wk.slot_ord
      from _ems_plan_slot_wk wk
      where wk.pv_surplus_w <= 0
        and extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
      order by wk.buy_price, wk.slot_ord
    loop
      exit when v_cum >= v_chg_pm_wh;
      exit when v_per_slot_charge_wh <= 0;
      update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
      v_cum := v_cum + v_per_slot_charge_wh;
    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
      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;

  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
  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). '
  'Masky charge/discharge-export se berou zvlášť pro 00–12 a 12–24 Europe/Prague (polovina budgetu na segment). '
  '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.';
