CREATE OR REPLACE FUNCTION ems.fn_update_baseline_stats(
    p_site_id INT,
    p_lookback_days INT DEFAULT 30
)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
    v_count INT;
BEGIN
    WITH raw AS (
        SELECT
            EXTRACT(DOW  FROM ti.measured_at AT TIME ZONE 'Europe/Prague')::INT AS dow,
            EXTRACT(HOUR FROM ti.measured_at AT TIME ZONE 'Europe/Prague')::INT AS hour,
            GREATEST(0,
                ti.load_power_w
                - COALESCE((
                    SELECT AVG(tev.power_w)
                    FROM ems.telemetry_ev_charger tev
                    WHERE tev.site_id = ti.site_id
                      AND tev.measured_at BETWEEN
                          ti.measured_at - INTERVAL '30 seconds'
                          AND ti.measured_at + INTERVAL '30 seconds'
                ), 0)::INT
                - COALESCE((
                    SELECT AVG(thp.power_w)
                    FROM ems.telemetry_heat_pump thp
                    WHERE thp.site_id = ti.site_id
                      AND thp.measured_at BETWEEN
                          ti.measured_at - INTERVAL '30 seconds'
                          AND ti.measured_at + INTERVAL '30 seconds'
                ), 0)::INT
                -- bazénové čerpadlo (Shelly): měřená řízená zátěž nepatří do bazálu
                - COALESCE((
                    SELECT AVG(tpp.power_w)
                    FROM ems.telemetry_pool_pump tpp
                    WHERE tpp.site_id = ti.site_id
                      AND tpp.measured_at BETWEEN
                          ti.measured_at - INTERVAL '30 seconds'
                          AND ti.measured_at + INTERVAL '30 seconds'
                ), 0)::INT
            ) AS baseline_w
        FROM ems.telemetry_inverter ti
        WHERE ti.site_id = p_site_id
          AND ti.measured_at >= now() - make_interval(days => p_lookback_days)
          AND ti.load_power_w IS NOT NULL
          AND ti.load_power_w > 0
    ),
    agg AS (
        SELECT
            dow,
            hour,
            AVG(baseline_w)    AS avg_w,
            STDDEV(baseline_w) AS stddev_w,
            COUNT(*)           AS samples
        FROM raw
        GROUP BY dow, hour
        HAVING COUNT(*) >= 4
    )
    INSERT INTO ems.consumption_baseline_stats
        (site_id, day_of_week, hour_of_day,
         avg_power_w, stddev_power_w, sample_count, last_updated)
    SELECT
        p_site_id, dow, hour,
        ROUND(avg_w::NUMERIC, 2),
        ROUND(stddev_w::NUMERIC, 2),
        samples,
        now()
    FROM agg
    ON CONFLICT (site_id, day_of_week, hour_of_day) DO UPDATE SET
        avg_power_w    = ROUND(
            0.7 * ems.consumption_baseline_stats.avg_power_w
            + 0.3 * EXCLUDED.avg_power_w, 2),
        stddev_power_w = ROUND(
            COALESCE(0.7 * ems.consumption_baseline_stats.stddev_power_w
            + 0.3 * EXCLUDED.stddev_power_w,
            EXCLUDED.stddev_power_w), 2),
        sample_count   = ems.consumption_baseline_stats.sample_count
                         + EXCLUDED.sample_count,
        last_updated   = now();

    GET DIAGNOSTICS v_count = ROW_COUNT;
    RETURN v_count;
END;
$$;

COMMENT ON FUNCTION ems.fn_update_baseline_stats(INT, INT) IS
'Aktualizuje průměry bazální spotřeby z telemetrie posledních N dní.
Používá exponenciální klouzavý průměr (EMA 70/30) pro postupné zpřesňování.
Volat denně po půlnoci. Pro první naplnění: fn_update_baseline_stats(2, 90).
Pro úplný reset bucketů bez „ocasu“ EMA smaž řádky a znovu volej, nebo ems.fn_rebuild_consumption_baseline_stats.';


CREATE OR REPLACE FUNCTION ems.fn_get_baseline_forecast(
    p_site_id       INT,
    p_from          TIMESTAMPTZ,
    p_to            TIMESTAMPTZ
)
RETURNS TABLE (
    interval_start  TIMESTAMPTZ,
    forecast_w      INT,
    confidence_w    INT
)
LANGUAGE sql
STABLE
AS $$
    SELECT
        gs.slot                                           AS interval_start,
        COALESCE(cbs.avg_power_w, 500)::INT              AS forecast_w,
        COALESCE(
            cbs.avg_power_w + 0.5 * COALESCE(cbs.stddev_power_w, 100),
            550
        )::INT                                            AS confidence_w
    FROM generate_series(
      date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z'),
      date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z') - interval '15 minutes',
      interval '15 minutes'
    ) AS gs(slot)
    LEFT JOIN ems.consumption_baseline_stats cbs
        ON  cbs.site_id    = p_site_id
        AND cbs.day_of_week = EXTRACT(DOW  FROM gs.slot AT TIME ZONE 'Europe/Prague')::INT
        AND cbs.hour_of_day = EXTRACT(HOUR FROM gs.slot AT TIME ZONE 'Europe/Prague')::INT
    ORDER BY gs.slot;
$$;

COMMENT ON FUNCTION ems.fn_get_baseline_forecast(INT, TIMESTAMPTZ, TIMESTAMPTZ) IS
'Vrátí předpověď bazální spotřeby pro zadaný horizont jako 15min sloty.
forecast_w = průměr dle DOW+hodina z historických dat.
confidence_w = konzervativní odhad (avg + 0.5*stddev).
Fallback 500W pokud nejsou historická data.
Použití v solveru: nahrazuje pevný fallback 500W v _load_slots().';
