-- =============================================================
-- R__042_fn_predict_negative_prices.sql
-- Predikce oken se zvýšeným rizikem záporné spotové ceny (OTE).
-- Volat denně po importu cen a po forecastu FVE; výsledky ukládá do
-- ems.predicted_negative_price_window.
-- =============================================================

CREATE OR REPLACE FUNCTION ems.fn_predict_negative_price_windows(
    p_site_id INT,
    p_days_ahead INT DEFAULT 7
)
RETURNS TABLE (
    predicted_date DATE,
    window_start_hour INT,
    window_end_hour INT,
    probability_pct INT,
    expected_min_price NUMERIC(10, 4),
    reason TEXT
)
LANGUAGE plpgsql
VOLATILE
AS $$
DECLARE
    v_start DATE;
    v_end DATE;
    v_days INT;
BEGIN
    v_days := COALESCE(p_days_ahead, 7);
    IF v_days < 1 OR v_days > 60 THEN
        RAISE EXCEPTION 'p_days_ahead must be between 1 and 60, got %', p_days_ahead;
    END IF;

    v_start := (CURRENT_TIMESTAMP AT TIME ZONE 'Europe/Prague')::DATE + 1;
    v_end := (CURRENT_TIMESTAMP AT TIME ZONE 'Europe/Prague')::DATE + v_days;

    IF NOT EXISTS (SELECT 1 FROM ems.site s WHERE s.id = p_site_id) THEN
        RAISE EXCEPTION 'site not found: %', p_site_id;
    END IF;

    DELETE FROM ems.predicted_negative_price_window p
    WHERE p.site_id = p_site_id
      AND p.predicted_date BETWEEN v_start AND v_end;

    RETURN QUERY
    WITH hist_price AS (
        SELECT
            EXTRACT(DOW FROM mip.interval_start AT TIME ZONE 'Europe/Prague')::INT AS dow,
            EXTRACT(HOUR FROM mip.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
            COUNT(*)::INT AS total_slots,
            COUNT(*) FILTER (WHERE mip.buy_raw_price_czk_kwh < 0)::INT AS neg_slots,
            AVG(mip.buy_raw_price_czk_kwh) AS avg_price,
            MIN(mip.buy_raw_price_czk_kwh) AS min_price
        FROM ems.market_interval_price mip
        WHERE mip.market_source IN ('OTE_CZ', 'OTE_CZ_DAM')
          AND mip.interval_start >= NOW() - INTERVAL '6 months'
        GROUP BY 1, 2
        HAVING COUNT(*) >= 4
    ),
    latest_run AS (
        SELECT fpr.id
        FROM ems.forecast_pv_run fpr
        WHERE fpr.site_id = p_site_id
          AND fpr.status = 'ok'
        ORDER BY fpr.created_at DESC NULLS LAST
        LIMIT 1
    ),
    slot_power_hist AS (
        SELECT
            fpi.interval_start,
            SUM(fpi.power_w)::BIGINT AS total_w
        FROM ems.forecast_pv_interval fpi
        INNER JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
        WHERE fpr.site_id = p_site_id
          AND fpr.status = 'ok'
          AND fpi.interval_start >= NOW() - INTERVAL '6 months'
        GROUP BY fpi.interval_start
    ),
    pv_max_by_hour AS (
        SELECT
            EXTRACT(HOUR FROM sp.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
            MAX(sp.total_w)::BIGINT AS hist_max_w
        FROM slot_power_hist sp
        GROUP BY 1
    ),
    pred_slot AS (
        SELECT
            fpi.interval_start,
            SUM(fpi.power_w)::BIGINT AS total_w
        FROM ems.forecast_pv_interval fpi
        INNER JOIN latest_run lr ON lr.id = fpi.run_id
        GROUP BY fpi.interval_start
    ),
    pred_by_hour AS (
        SELECT
            (ps.interval_start AT TIME ZONE 'Europe/Prague')::DATE AS d,
            EXTRACT(HOUR FROM ps.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
            MAX(ps.total_w)::BIGINT AS pred_max_w
        FROM pred_slot ps
        GROUP BY 1, 2
    ),
    future_days AS (
        SELECT gs::DATE AS d
        FROM generate_series(v_start, v_end, INTERVAL '1 day') AS gs
    ),
    hourly_base AS (
        SELECT
            fd.d AS predicted_date,
            h.hour,
            EXTRACT(DOW FROM fd.d)::INT AS dow,
            LEAST(
                100,
                GREATEST(
                    0,
                    (100.0 * hp.neg_slots / NULLIF(hp.total_slots, 0))::INT
                )
            ) AS base_prob,
            hp.min_price
        FROM future_days fd
        CROSS JOIN generate_series(0, 23) AS h (hour)
        INNER JOIN hist_price hp
            ON hp.dow = EXTRACT(DOW FROM fd.d)::INT
           AND hp.hour = h.hour
    ),
    hourly_adj AS (
        SELECT
            hb.predicted_date,
            hb.hour,
            hb.dow,
            hb.base_prob,
            hb.min_price,
            CASE
                WHEN pm.hist_max_w IS NULL OR pm.hist_max_w <= 0 THEN hb.base_prob
                WHEN ph.pred_max_w IS NULL THEN hb.base_prob
                WHEN ph.pred_max_w::NUMERIC > 0.8 * pm.hist_max_w::NUMERIC THEN
                    LEAST(100, hb.base_prob + 15)
                WHEN ph.pred_max_w::NUMERIC < 0.4 * pm.hist_max_w::NUMERIC THEN
                    GREATEST(0, hb.base_prob - 20)
                ELSE hb.base_prob
            END AS probability_pct
        FROM hourly_base hb
        LEFT JOIN pred_by_hour ph
            ON ph.d = hb.predicted_date
           AND ph.hour = hb.hour
        LEFT JOIN pv_max_by_hour pm ON pm.hour = hb.hour
    ),
    qualified AS (
        SELECT
            ha.predicted_date,
            ha.hour,
            ha.probability_pct,
            ha.min_price AS expected_hour_min,
            ha.hour
                - ROW_NUMBER() OVER (
                    PARTITION BY ha.predicted_date, ha.probability_pct
                    ORDER BY ha.hour
                ) AS grp
        FROM hourly_adj ha
        WHERE ha.probability_pct >= 30
    ),
    windows AS (
        SELECT
            q.predicted_date,
            q.probability_pct,
            MIN(q.hour) AS window_start_hour,
            MAX(q.hour) AS window_end_hour,
            MIN(q.expected_hour_min) AS expected_min_price
        FROM qualified q
        GROUP BY q.predicted_date, q.probability_pct, q.grp
    ),
    final_rows AS (
        SELECT
            w.predicted_date,
            w.window_start_hour,
            w.window_end_hour,
            w.probability_pct,
            ROUND(w.expected_min_price::NUMERIC, 4) AS expected_min_price,
            CASE
                WHEN w.probability_pct >= 70 THEN
                    'Historicky vysoká FVE výroba v tento čas – velmi pravděpodobné'
                WHEN w.probability_pct >= 50 THEN
                    'Víkendový vzor + dobrá předpověď FVE'
                ELSE
                    'Možné na základě historických dat'
            END AS reason
        FROM windows w
        WHERE w.probability_pct >= 25
    ),
    ins AS (
        INSERT INTO ems.predicted_negative_price_window (
            site_id,
            predicted_date,
            window_start_hour,
            window_end_hour,
            probability_pct,
            expected_min_price,
            reason
        )
        SELECT
            p_site_id,
            fr.predicted_date,
            fr.window_start_hour,
            fr.window_end_hour,
            fr.probability_pct,
            fr.expected_min_price,
            fr.reason
        FROM final_rows fr
        RETURNING
            predicted_negative_price_window.predicted_date,
            predicted_negative_price_window.window_start_hour,
            predicted_negative_price_window.window_end_hour,
            predicted_negative_price_window.probability_pct,
            predicted_negative_price_window.expected_min_price,
            predicted_negative_price_window.reason
    )
    SELECT
        ins.predicted_date,
        ins.window_start_hour,
        ins.window_end_hour,
        ins.probability_pct,
        ins.expected_min_price,
        ins.reason
    FROM ins;
END;
$$;

COMMENT ON FUNCTION ems.fn_predict_negative_price_windows(INT, INT) IS
'Predikuje okna se zvýšeným rizikem záporné nákupní ceny z historie OTE (6 měsíců), upraví podle forecastu FVE a zapíše do ems.predicted_negative_price_window. Volat denně po importu cen a forecastu.';
