-- Jednorázový JSON snapshot pro vysvětlení plánu (typicky nejbližších N hodin).
-- Volání: SELECT ems.fn_plan_explain_bundle(<site_id>, 6);

CREATE OR REPLACE FUNCTION ems.fn_plan_explain_bundle(
    p_site_id INT,
    p_hours NUMERIC DEFAULT 6
)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
    v_hours   NUMERIC;
    v_slot    TIMESTAMPTZ;
    v_win_end TIMESTAMPTZ;
    v_site_ok BOOLEAN;
    v_run     RECORD;
    v_meta    JSONB;
    v_ivals   JSONB;
    v_mode    JSONB;
    v_batt    JSONB;
    v_grid    JSONB;
    v_hp      JSONB;
    v_ev      JSONB;
    v_fc      JSONB;
    v_ov      JSONB;
    v_econ    JSONB;
BEGIN
    IF p_site_id IS NULL THEN
        RETURN jsonb_build_object('error', 'site_id_required');
    END IF;

    v_hours := GREATEST(0.25::NUMERIC, LEAST(COALESCE(NULLIF(p_hours, 0), 6), 96));

    SELECT EXISTS(SELECT 1 FROM ems.site s WHERE s.id = p_site_id) INTO v_site_ok;
    IF NOT v_site_ok THEN
        RETURN jsonb_build_object(
            'error', 'unknown_site_id',
            'site_id', p_site_id
        );
    END IF;

    v_slot := to_timestamp(floor(extract(epoch FROM now()) / 900) * 900)::TIMESTAMPTZ;
    v_win_end := v_slot + (v_hours * INTERVAL '1 hour');

    v_meta := jsonb_build_object(
        'generated_at', now(),
        'current_slot_start_utc', v_slot,
        'window_end_utc', v_win_end,
        'hours_requested', v_hours,
        'slot_minutes', 15,
        'note',
        'interval_start je vždy UTC. Provozní čas viz ems.site.timezone (typicky Europe/Prague).'
    );

    SELECT * INTO v_run
    FROM ems.planning_run pr
    WHERE pr.site_id = p_site_id
      AND pr.status = 'active'
    ORDER BY pr.created_at DESC
    LIMIT 1;

    IF NOT FOUND THEN
        RETURN jsonb_build_object(
            'error', 'no_active_plan',
            'site_id', p_site_id,
            'meta', v_meta
        );
    END IF;

    SELECT COALESCE(
        jsonb_agg(
            to_jsonb(t) || jsonb_build_object(
                'hours_from_plan_horizon_start',
                round(
                    (EXTRACT(EPOCH FROM (t.interval_start - v_run.horizon_start)) / 3600.0)::NUMERIC,
                    4
                )
            )
            ORDER BY t.interval_start
        ),
        '[]'::JSONB
    )
    INTO v_ivals
    FROM (
        SELECT pi.*
        FROM ems.planning_interval pi
        WHERE pi.run_id = v_run.id
          AND pi.interval_start >= v_slot
          AND pi.interval_start < v_win_end
    ) t;

    select jsonb_build_object(
        'window_start_utc', v_slot,
        'window_end_utc', v_win_end,
        'total_import_kwh', coalesce(sum(
          case when pi.grid_setpoint_w > 0
            then pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
        ), 0),
        'total_export_kwh', coalesce(sum(
          case when pi.grid_setpoint_w < 0
            then -pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
        ), 0),
        'total_buy_cost_czk', coalesce(sum(
          case when pi.grid_setpoint_w > 0
            then pi.grid_setpoint_w * pi.effective_buy_price * 0.25 / 1000.0
            else 0 end
        ), 0),
        'total_sell_revenue_czk', coalesce(sum(
          case when pi.grid_setpoint_w < 0
            then -pi.grid_setpoint_w * pi.effective_sell_price * 0.25 / 1000.0
            else 0 end
        ), 0),
        'total_cashflow_czk', coalesce(sum(pi.cashflow_czk), 0),
        'total_battery_arbitrage_czk', coalesce(sum(pi.battery_arbitrage_czk), 0),
        'total_penalty_czk', coalesce(sum(pi.penalty_czk), 0),
        'total_green_bonus_czk', coalesce(sum(pi.green_bonus_czk), 0),
        'net_economic_czk',
            coalesce(-sum(pi.cashflow_czk), 0)
            + coalesce(sum(pi.battery_arbitrage_czk), 0)
            - coalesce(sum(pi.penalty_czk), 0)
            + coalesce(sum(pi.green_bonus_czk), 0),
        'neg_sell_export_slots', count(*) filter (
            where pi.effective_sell_price < 0 and pi.grid_setpoint_w < -500
        ),
        'first_grid_charge_slot_utc', min(pi.interval_start) filter (
            where pi.grid_setpoint_w > 500
        )
    )
    into v_econ
    from ems.planning_interval pi
    where pi.run_id = v_run.id
      and pi.interval_start >= v_slot
      and pi.interval_start < v_win_end;

    SELECT to_jsonb(m.*) || jsonb_build_object('mode_name', d.name)
    INTO v_mode
    FROM ems.site_operating_mode m
    LEFT JOIN ems.operating_mode_def d ON d.code = m.mode_code
    WHERE m.site_id = p_site_id;

    SELECT COALESCE(jsonb_agg(to_jsonb(b) ORDER BY b.id), '[]'::JSONB)
    INTO v_batt
    FROM ems.asset_battery b
    WHERE b.site_id = p_site_id;

    SELECT to_jsonb(g.*)
    INTO v_grid
    FROM ems.site_grid_connection g
    WHERE g.site_id = p_site_id;

    SELECT COALESCE(jsonb_agg(to_jsonb(h) ORDER BY h.id), '[]'::JSONB)
    INTO v_hp
    FROM ems.asset_heat_pump h
    WHERE h.site_id = p_site_id;

    SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.id), '[]'::JSONB)
    INTO v_ev
    FROM ems.ev_session e
    WHERE e.site_id = p_site_id
      AND e.session_end IS NULL;

    SELECT COALESCE(
        jsonb_agg(to_jsonb(f) ORDER BY f.logged_at DESC),
        '[]'::JSONB
    )
    INTO v_fc
    FROM (
        SELECT l.*
        FROM ems.forecast_correction_log l
        WHERE l.site_id = p_site_id
        ORDER BY l.logged_at DESC
        LIMIT 5
    ) f;

    SELECT COALESCE(
        jsonb_agg(to_jsonb(o) ORDER BY o.valid_from DESC),
        '[]'::JSONB
    )
    INTO v_ov
    FROM (
        SELECT o.*
        FROM ems.site_override o
        WHERE o.site_id = p_site_id
          AND o.valid_from < v_win_end
          AND (o.valid_to IS NULL OR o.valid_to > v_slot)
        ORDER BY o.valid_from DESC
        LIMIT 20
    ) o;

    RETURN jsonb_build_object(
        'meta', v_meta,
        'site', (
            SELECT jsonb_build_object(
                'id', s.id,
                'code', s.code,
                'name', s.name,
                'timezone', s.timezone,
                'active', s.active
            )
            FROM ems.site s
            WHERE s.id = p_site_id
        ),
        'active_planning_run', (
            SELECT to_jsonb(pr.*)
            FROM ems.planning_run pr
            WHERE pr.id = v_run.id
        ),
        'intervals_next_window', v_ivals,
        'operating_mode', v_mode,
        'asset_battery', v_batt,
        'site_grid_connection', v_grid,
        'asset_heat_pump', v_hp,
        'ev_sessions_open', v_ev,
        'forecast_correction_log_recent', v_fc,
        'site_overrides_active_in_window', v_ov,
        'economics_summary', v_econ,
        'ai_readme', jsonb_build_object(
            'purpose',
            'Data stačí k vysvětlení „proč plán v dalších hodinách vypadá takto“: ceny v řádcích intervalů, vstupy (baseline, PV), výstupy (bat/grid/EV/TČ), režim a síťové limity.',
            'code_refs',
            ARRAY[
                'backend/services/planning_engine.py (solve_dispatch, run_type rolling/daily)',
                'backend/services/control_exporter.py (mapování na Deye)',
                'CLAUDE.md bod 15–19 (baseline, horizont, režimy, MILP export)'
            ],
            'horizon_and_objective',
            'Dynamický horizont: ems.fn_planning_horizon_end (OTE + strop/min v SQL). Účelovka LP bez vzdálenostních vah; terminal SoC shadow price (avg buy × 0,9 / 1000 × soc_end). Sloupec hours_from_plan_horizon_start u intervalů jen pro čtení.'
        )
    );
END;
$$;

COMMENT ON FUNCTION ems.fn_plan_explain_bundle(INT, NUMERIC) IS
'JSONB balík: aktivní planning_run, planning_interval v okně od aktuálního 15min slotu, režim, baterie/síť/TČ, otevřené EV session, poslední forecast_correction_log, platné site_override. Pro AI / diagnostiku bez opakovaného skládání dotazů.';
