CREATE OR REPLACE FUNCTION ems.fn_fill_forecast_accuracy(
    p_site_id INT,
    p_lookback_hours INT DEFAULT 48
)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
    v_count INT := 0;
BEGIN
    INSERT INTO ems.forecast_accuracy (
        site_id, pv_array_id, interval_start, run_id,
        forecast_power_w, forecast_created_at, lead_time_hours,
        actual_power_w, actual_filled_at,
        error_w, error_pct,
        learning_eligible, learning_exclude_reason
    )
    SELECT
        fpr.site_id,
        fpr.pv_array_id,
        fpi.interval_start,
        fpi.run_id,
        fpi.power_w                                          AS forecast_power_w,
        fpr.created_at                                       AS forecast_created_at,
        ROUND(
            EXTRACT(EPOCH FROM (fpi.interval_start - fpr.created_at))
            / 3600.0, 2
        )                                                    AS lead_time_hours,
        CASE
            WHEN v.exclude_actual_for_learning THEN NULL
            ELSE slot.avg_actual_w::INT
        END                                                  AS actual_power_w,
        now()                                                AS actual_filled_at,
        CASE
            WHEN v.exclude_actual_for_learning THEN NULL
            ELSE fpi.power_w - COALESCE(slot.avg_actual_w::INT, 0)
        END                                                  AS error_w,
        CASE
            WHEN v.exclude_actual_for_learning THEN NULL
            WHEN slot.avg_actual_w IS NOT NULL
                 AND slot.avg_actual_w >= 50
                 AND fpi.power_w >= 50
            THEN ROUND(
                (fpi.power_w::NUMERIC - slot.avg_actual_w::NUMERIC)
                / slot.avg_actual_w::NUMERIC * 100,
                4
            )
            ELSE NULL
        END                                                  AS error_pct,
        v.learning_eligible,
        v.learning_exclude_reason
    FROM ems.forecast_pv_interval fpi
    JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
    JOIN ems.asset_pv_array pa
      ON pa.id = fpr.pv_array_id
     AND pa.site_id = fpr.site_id
    LEFT JOIN ems.site_pv_forecast_calibration cal
      ON cal.site_id = fpr.site_id
    LEFT JOIN LATERAL (
        SELECT
            coalesce(cal.delta_learn_min_ts, timestamptz '2026-04-11T22:00:00Z') AS delta_learn_min_ts,
            cal.pv_curtailment_policy_effective_from AS policy_from
    ) cal_eff ON true
    LEFT JOIN LATERAL (
        SELECT pi.pv_a_curtailed_w, pi.deye_gen_cutoff_enabled
        FROM ems.planning_interval pi
        JOIN ems.planning_run pr ON pr.id = pi.run_id
        WHERE pr.site_id = fpr.site_id
          AND pr.status = 'active'
          AND pi.interval_start = fpi.interval_start
        LIMIT 1
    ) ap ON true
    LEFT JOIN LATERAL (
        SELECT
            (fpi.interval_start < cal_eff.delta_learn_min_ts) AS before_learn_cutoff,
            (
                cal_eff.policy_from IS NOT NULL
                AND fpi.interval_start >= cal_eff.policy_from
                AND (
                    coalesce(ap.pv_a_curtailed_w, 0) > 50
                    OR coalesce(ap.deye_gen_cutoff_enabled, false) IS TRUE
                    OR EXISTS (
                        SELECT 1
                        FROM ems.cutoff_switch_log l
                        WHERE l.site_id = fpr.site_id
                          AND l.switched_at >= fpi.interval_start
                          AND l.switched_at < fpi.interval_start + INTERVAL '15 minutes'
                          AND l.new_state IS FALSE
                    )
                )
            ) AS is_curtailed_learning_slot,
            EXISTS (
                SELECT 1
                FROM ems.telemetry_inverter ti_d
                WHERE ti_d.site_id = fpr.site_id
                  AND ti_d.measured_at >= fpi.interval_start
                  AND ti_d.measured_at < fpi.interval_start + INTERVAL '15 minutes'
                  AND (
                    coalesce(ti_d.is_export_limited, false) IS TRUE
                    OR (ti_d.pv_derating_flags IS NOT NULL AND ti_d.pv_derating_flags <> 0)
                  )
            ) AS is_telemetry_derated_slot
    ) flags ON true
    LEFT JOIN LATERAL (
        SELECT
            CASE
                WHEN flags.before_learn_cutoff THEN false
                WHEN flags.is_curtailed_learning_slot THEN false
                WHEN flags.is_telemetry_derated_slot THEN false
                ELSE true
            END AS learning_eligible,
            CASE
                WHEN flags.before_learn_cutoff THEN 'before_delta_learn_min'
                WHEN flags.is_telemetry_derated_slot THEN 'telemetry_derating'
                WHEN flags.is_curtailed_learning_slot THEN 'curtailment_or_export_cutoff'
                ELSE NULL
            END AS learning_exclude_reason,
            (flags.is_curtailed_learning_slot OR flags.is_telemetry_derated_slot) AS exclude_actual_for_learning
    ) v ON true
    LEFT JOIN LATERAL (
        WITH base AS (
            SELECT AVG(
                CASE coalesce(pa.telemetry_source, '')
                    WHEN 'pv1'        THEN ti.pv1_power_w::NUMERIC
                    WHEN 'pv2'        THEN ti.pv2_power_w::NUMERIC
                    WHEN 'pv_strings' THEN (COALESCE(ti.pv1_power_w, 0) + COALESCE(ti.pv2_power_w, 0))::NUMERIC
                    WHEN 'pv_total'   THEN ti.pv_power_w::NUMERIC
                    WHEN 'gen_port'   THEN ti.gen_port_power_w::NUMERIC
                    ELSE NULL
                END
            ) AS avg_actual_w
            FROM ems.telemetry_inverter ti
            WHERE ti.site_id = fpr.site_id
              AND ti.measured_at >= fpi.interval_start
              AND ti.measured_at < fpi.interval_start + INTERVAL '15 minutes'
        ),
        grp AS (
            -- Pokud více pv_array sdílí stejné měření (např. GEN port rozdělený do více orientací),
            -- rozdělíme actual proporčně podle forecastu v daném slotu.
            SELECT
                sum(fpi2.power_w)::numeric AS forecast_group_w
            FROM ems.forecast_pv_interval fpi2
            JOIN ems.forecast_pv_run fpr2 ON fpr2.id = fpi2.run_id
            JOIN ems.asset_pv_array pa2
              ON pa2.id = fpi2.pv_array_id
             AND pa2.site_id = fpr2.site_id
            WHERE pa.telemetry_group IS NOT NULL
              AND pa2.site_id = fpr.site_id
              AND pa2.telemetry_group = pa.telemetry_group
              AND pa2.telemetry_source = pa.telemetry_source
              AND fpi2.interval_start = fpi.interval_start
              AND fpr2.status = 'ok'
              -- Forecast runs jsou per pv_array_id, ale typicky vznikají v jednom batchi.
              -- Aby group součet seděl, párujeme runy podle "stejného okamžiku vytvoření" (1min bucket).
              AND date_bin(
                    interval '1 minute',
                    fpr2.created_at,
                    timestamptz '1970-01-01T00:00:00Z'
                  )
                  = date_bin(
                      interval '1 minute',
                      fpr.created_at,
                      timestamptz '1970-01-01T00:00:00Z'
                    )
        )
        SELECT
            CASE
                WHEN pa.telemetry_group IS NULL THEN (SELECT avg_actual_w FROM base)
                WHEN (SELECT forecast_group_w FROM grp) IS NULL THEN NULL
                WHEN (SELECT forecast_group_w FROM grp) <= 0 THEN NULL
                WHEN (SELECT avg_actual_w FROM base) IS NULL THEN NULL
                ELSE (SELECT avg_actual_w FROM base) * (fpi.power_w::numeric / (SELECT forecast_group_w FROM grp))
            END AS avg_actual_w
    ) slot ON true
    WHERE fpr.site_id = p_site_id
      AND fpr.status  = 'ok'
      AND fpi.interval_start < now() - INTERVAL '15 minutes'
      AND fpi.interval_start >= now() - make_interval(hours => p_lookback_hours)
    ON CONFLICT (run_id, interval_start) DO UPDATE SET
        actual_power_w            = EXCLUDED.actual_power_w,
        actual_filled_at          = EXCLUDED.actual_filled_at,
        error_w                   = EXCLUDED.error_w,
        error_pct                 = EXCLUDED.error_pct,
        learning_eligible         = EXCLUDED.learning_eligible,
        learning_exclude_reason   = EXCLUDED.learning_exclude_reason;

    GET DIAGNOSTICS v_count = ROW_COUNT;

    perform ems.fn_refresh_site_pv_delta_profile_cache(p_site_id);

    RETURN v_count;
END;
$$;

COMMENT ON FUNCTION ems.fn_fill_forecast_accuracy(INT, INT) IS
'Doplní skutečné hodnoty výroby do forecast_accuracy z telemetrie.
learning_eligible / learning_exclude_reason: před delta_learn_min_ts (kalibrace site) se nepočítá do učení delty;
po pv_curtailment_policy_effective_from sloty s curtailment / gen cutoff / cutoff_switch_log (export off) mají NULL actual a jsou vyloučeny z učení;
telemetrie: is_export_limited nebo pv_derating_flags <> 0 v okně slotu → stejné vyloučení (telemetry_derating).
Po úspěšném INSERT volá fn_refresh_site_pv_delta_profile_cache (V079 cache pro /plan/current).
Volat každých 15 minut (spolu s audit_filler) pro inkrementální plnění.
p_lookback_hours: kolik hodin zpět zpracovat (default 48h pro catch-up).
Pro první backfill: SELECT ems.fn_fill_forecast_accuracy(2, 8760) -- 1 rok';
