-- jeden jsonb snapshot pro LP: režim, baterie, síť, EV, TČ, tuv stats

create or replace function ems.fn_planning_site_context(p_site_id int)
returns jsonb
language plpgsql
stable
as $fn$
declare
  v_mode text;
  v_b jsonb;
  v_hp jsonb;
  v_grid jsonb;
  v_market jsonb;
  v_veh jsonb;
  v_ev jsonb;
  v_soc_pct numeric;
  v_soc_wh numeric;
  v_tuv numeric;
  v_tuv_stats jsonb;
  v_uc numeric;
  v_min_soc_wh numeric;
  v_arb_wh numeric;
  v_soc_max_wh numeric;
begin
  if not exists (select 1 from ems.site s where s.id = p_site_id) then
    return jsonb_build_object('error', 'unknown_site');
  end if;

  select som.mode_code
  into v_mode
  from ems.site_operating_mode som
  where som.site_id = p_site_id;

  select jsonb_build_object(
    'usable_capacity_wh', ab.usable_capacity_wh,
    'min_soc_wh', (ab.min_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    'arb_floor_wh', (ab.reserve_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    'reserve_soc_wh', (ab.reserve_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    'soc_max_wh', (ab.max_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
    'planner_soc_max_wh', (
      coalesce(ab.planner_max_soc_percent, ab.max_soc_percent) / 100.0 * ab.usable_capacity_wh
    )::numeric,
    'planner_extreme_buy_threshold_czk_kwh', coalesce(ab.planner_extreme_buy_threshold_czk_kwh, -5.0),
    'planner_discharge_floor_percent', ab.planner_discharge_floor_percent,
    'planner_discharge_relax_prewindow_slots', coalesce(ab.planner_discharge_relax_prewindow_slots, 8),
    'charge_efficiency', ab.charge_efficiency,
    'discharge_efficiency', ab.discharge_efficiency,
    'degradation_cost_czk_kwh', ab.degradation_cost_czk_kwh,
    'max_charge_power_w', 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)
      )
    )::int,
    'max_discharge_power_w', 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)
      )
    )::int,
    'charge_slot_buffer', ab.charge_slot_buffer,
    'discharge_slot_buffer', ab.discharge_slot_buffer,
    'planner_terminal_soc_value_factor', ab.planner_terminal_soc_value_factor,
    'planner_daytime_charge_target_enabled', coalesce(ab.planner_daytime_charge_target_enabled, true),
    'planner_night_baseload_buffer_percent', coalesce(ab.planner_night_baseload_buffer_percent, 20::numeric),
    'planner_daytime_charge_price_quantile', coalesce(ab.planner_daytime_charge_price_quantile, 0.70::numeric),
    'planner_charge_commitment_penalty_czk_kwh', coalesce(ab.planner_charge_commitment_penalty_czk_kwh, 0.20::numeric),
    'planner_neg_sell_prep_soc_percent', coalesce(ab.planner_neg_sell_prep_soc_percent, 80::numeric),
    'planner_neg_sell_full_soc_tail_slots', coalesce(ab.planner_neg_sell_full_soc_tail_slots, 4),
    'planner_neg_sell_vent_min_sell_czk_kwh', ab.planner_neg_sell_vent_min_sell_czk_kwh,
    'planner_pv_risk_frontload_czk_kwh', coalesce(ab.planner_pv_risk_frontload_czk_kwh, 0.01),
    'planner_safety_soc_risk_factor', coalesce(ab.planner_safety_soc_risk_factor, 0.05)
  )
  into v_b
  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_b is null then
    raise exception 'No asset_battery for site_id=%', p_site_id;
  end if;

  v_uc := (v_b->>'usable_capacity_wh')::numeric;
  v_min_soc_wh := (v_b->>'min_soc_wh')::numeric;
  v_soc_max_wh := (v_b->>'soc_max_wh')::numeric;

  if (v_b->>'max_charge_power_w')::int <= 0 or (v_b->>'max_discharge_power_w')::int <= 0 then
    raise exception 'Invalid battery effective limits for site_id=%', p_site_id;
  end if;

  select jsonb_build_object(
    'rated_heating_power_w', greatest(coalesce(hp.rated_heating_power_w, 8000), 0)::int,
    'tuv_min_temp_c', coalesce(hp.tuv_min_temp_c, 45)::numeric,
    'tuv_target_temp_c', coalesce(hp.tuv_target_temp_c, 55)::numeric
  )
  into v_hp
  from ems.asset_heat_pump hp
  where hp.site_id = p_site_id
  order by hp.id
  limit 1;

  if v_hp is null then
    v_hp := jsonb_build_object(
      'rated_heating_power_w', 0,
      'tuv_min_temp_c', 0,
      'tuv_target_temp_c', 55
    );
  end if;

  select jsonb_build_object(
    'max_import_power_w', sgc.max_import_power_w,
    'max_export_power_w', sgc.max_export_power_w,
    'block_export_on_negative_sell', coalesce(sgc.block_export_on_negative_sell, false),
    'deye_gen_microinverter_cutoff_enabled', coalesce(
      (
        select ai.deye_gen_microinverter_cutoff_enabled
        from ems.asset_inverter ai
        where ai.site_id = p_site_id
          and ai.code = 'deye-main'
        limit 1
      ),
      false
    )
  )
  into v_grid
  from ems.site_grid_connection sgc
  where sgc.site_id = p_site_id
  order by sgc.id
  limit 1;

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

  select jsonb_build_object(
    'purchase_pricing_mode', lower(trim(coalesce(smc.purchase_pricing_mode, 'spot'))),
    'sale_pricing_mode', lower(trim(coalesce(smc.sale_pricing_mode, 'spot')))
  )
  into v_market
  from ems.site_market_config smc
  where smc.site_id = p_site_id
    and smc.valid_to is null
  order by smc.valid_from desc
  limit 1;

  v_market := coalesce(
    v_market,
    jsonb_build_object(
      'purchase_pricing_mode', 'spot',
      'sale_pricing_mode', 'spot'
    )
  );

  select coalesce(
    jsonb_agg(
      jsonb_build_object(
        'max_charge_power_w', v.max_charge_power_w,
        'min_power_w', coalesce(ch.min_power_w, 0),
        'battery_capacity_kwh', v.battery_capacity_kwh,
        'default_target_soc_pct', v.default_target_soc_pct
      )
      order by ch.code
    ),
    '[]'::jsonb
  )
  into v_veh
  from ems.asset_vehicle v
  join ems.asset_ev_charger ch on ch.id = v.default_charger_id
  where v.site_id = p_site_id
    and ch.code in ('ev-charger-1', 'ev-charger-2');

  v_ev := jsonb_build_array(
    (
      select case
        when es.target_deadline is null then null::jsonb
        when v.battery_capacity_kwh is null then null::jsonb
        when es.soc_at_connect_pct is null then null::jsonb
        when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then null::jsonb
        when greatest(
          0,
          (coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
            - es.soc_at_connect_pct::numeric) / 100.0
            * (v.battery_capacity_kwh * 1000)
            - coalesce(es.energy_delivered_wh, 0)::numeric
        ) <= 0
        and (
          coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
          or (100 - greatest(
            coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
            es.soc_at_connect_pct::numeric
          )) <= 0
        ) then null::jsonb
        else jsonb_build_object(
          'target_deadline', es.target_deadline,
          'energy_needed_wh', greatest(
            0,
            (coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
              - es.soc_at_connect_pct::numeric) / 100.0
              * (v.battery_capacity_kwh * 1000)
            - coalesce(es.energy_delivered_wh, 0)::numeric
          ),
          -- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
          -- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
          -- fyzicky vzít jen energii nad svým aktuálním SoC.
          'headroom_wh', case
            when coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
              0,
              (100 - greatest(
                coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
                es.soc_at_connect_pct::numeric
              )) / 100.0 * (v.battery_capacity_kwh * 1000)
            )
            else 0
          end,
          'opportunistic_value_czk_kwh', coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
        )
      end
      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 = 'ev-charger-1'
      limit 1
    ),
    (
      select case
        when es.target_deadline is null then null::jsonb
        when v.battery_capacity_kwh is null then null::jsonb
        when es.soc_at_connect_pct is null then null::jsonb
        when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then null::jsonb
        when greatest(
          0,
          (coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
            - es.soc_at_connect_pct::numeric) / 100.0
            * (v.battery_capacity_kwh * 1000)
            - coalesce(es.energy_delivered_wh, 0)::numeric
        ) <= 0
        and (
          coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
          or (100 - greatest(
            coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
            es.soc_at_connect_pct::numeric
          )) <= 0
        ) then null::jsonb
        else jsonb_build_object(
          'target_deadline', es.target_deadline,
          'energy_needed_wh', greatest(
            0,
            (coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
              - es.soc_at_connect_pct::numeric) / 100.0
              * (v.battery_capacity_kwh * 1000)
            - coalesce(es.energy_delivered_wh, 0)::numeric
          ),
          -- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
          -- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
          -- fyzicky vzít jen energii nad svým aktuálním SoC.
          'headroom_wh', case
            when coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
              0,
              (100 - greatest(
                coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
                es.soc_at_connect_pct::numeric
              )) / 100.0 * (v.battery_capacity_kwh * 1000)
            )
            else 0
          end,
          'opportunistic_value_czk_kwh', coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
        )
      end
      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 = 'ev-charger-2'
      limit 1
    )
  );

  select ti.battery_soc_percent
  into v_soc_pct
  from ems.telemetry_inverter ti
  where ti.site_id = p_site_id
  order by ti.measured_at desc
  limit 1;

  if v_soc_pct is null then
    v_soc_wh := v_uc * 0.5;
  else
    v_soc_wh := v_soc_pct::numeric / 100.0 * v_uc;
  end if;

  v_soc_wh := greatest(v_min_soc_wh, least(v_soc_wh, v_soc_max_wh));

  select thp.tuv_tank_temp_c
  into v_tuv
  from ems.telemetry_heat_pump thp
  where thp.site_id = p_site_id
  order by thp.measured_at desc
  limit 1;

  v_tuv := coalesce(v_tuv::numeric, 50::numeric);

  select coalesce(
    jsonb_agg(
      jsonb_build_object(
        'dow', tu.day_of_week,
        'hour', tu.hour_of_day,
        'delta', tu.avg_temp_delta_c
      )
    ),
    '[]'::jsonb
  )
  into v_tuv_stats
  from ems.tuv_usage_stats tu
  where tu.site_id = p_site_id;

  return jsonb_build_object(
    'operating_mode', v_mode,
    'battery', v_b,
    'heat_pump', v_hp,
    'grid', v_grid,
    'market', v_market,
    'vehicles', v_veh,
    'ev_sessions', v_ev,
    'soc_wh', v_soc_wh,
    'tuv_temp', v_tuv,
    'tuv_delta_stats', v_tuv_stats,
    'planning_config', coalesce(
      (
        select pc.config
        from ems.planning_config pc
        where pc.site_id = p_site_id
        limit 1
      ),
      '{}'::jsonb
    )
  );
end;
$fn$;

comment on function ems.fn_planning_site_context is
  'Kontext pro planning_engine / LP (bez samotného solveru). EV session: opportunistic_value = coalesce(session, vehicle); headroom_wh od max(target, soc_at_connect), 0 při vypnutém oportunismu; vehicles nesou min_power_w wallboxu.';
