-- pg_stl extension: time series analysis for PostgreSQL
-- Functions: ACF, PACF, STL decomposition, Holt-Winters forecasting

-- ============================================================
-- C-based functions: ACF, PACF, STL
-- ============================================================

CREATE FUNCTION acf_array(data double precision[], lags integer)
RETURNS double precision[] AS 'MODULE_PATHNAME', 'acf_array' LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION pacf_array(data double precision[], lags integer)
RETURNS double precision[] AS 'MODULE_PATHNAME', 'pacf_array' LANGUAGE C STRICT IMMUTABLE;

-- Composite type for stl_decompose result
CREATE TYPE stl_result AS (
    trend    double precision[],
    seasonal double precision[],
    residual double precision[]
);

CREATE OR REPLACE FUNCTION stl_decompose(
    y            double precision[],
    period       integer,
    seasonal     integer  DEFAULT 7,
    robust       boolean  DEFAULT true,
    trend        integer  DEFAULT 0,
    low_pass     integer  DEFAULT 0,
    inner_iter   integer  DEFAULT 2,
    outer_iter   integer  DEFAULT 0
)
RETURNS stl_result
AS '$libdir/pg_stl', 'stl_decompose'
LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;

CREATE OR REPLACE FUNCTION stl_trend(
    y            double precision[],
    period       integer,
    seasonal     integer  DEFAULT 7,
    robust       boolean  DEFAULT true,
    trend        integer  DEFAULT 0,
    low_pass     integer  DEFAULT 0,
    inner_iter   integer  DEFAULT 2,
    outer_iter   integer  DEFAULT 0
)
RETURNS double precision[]
AS '$libdir/pg_stl', 'stl_trend'
LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;

CREATE OR REPLACE FUNCTION stl_seasonal(
    y            double precision[],
    period       integer,
    seasonal     integer  DEFAULT 7,
    robust       boolean  DEFAULT true,
    trend        integer  DEFAULT 0,
    low_pass     integer  DEFAULT 0,
    inner_iter   integer  DEFAULT 2,
    outer_iter   integer  DEFAULT 0
)
RETURNS double precision[]
AS '$libdir/pg_stl', 'stl_seasonal'
LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;

CREATE OR REPLACE FUNCTION stl_residual(
    y            double precision[],
    period       integer,
    seasonal     integer  DEFAULT 7,
    robust       boolean  DEFAULT true,
    trend        integer  DEFAULT 0,
    low_pass     integer  DEFAULT 0,
    inner_iter   integer  DEFAULT 2,
    outer_iter   integer  DEFAULT 0
)
RETURNS double precision[]
AS '$libdir/pg_stl', 'stl_residual'
LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;

-- Helper: collect column values into ordered array
CREATE OR REPLACE FUNCTION stl_collect_ordered(
    tbl  regclass,
    val  text,
    ord  text
)
RETURNS double precision[]
LANGUAGE plpgsql STABLE AS $$
DECLARE
    result double precision[];
BEGIN
    EXECUTE format(
        'SELECT array_agg(%I ORDER BY %I) FROM %s',
        val, ord, tbl
    ) INTO result;
    RETURN result;
END;
$$;


-- ============================================================
-- Holt-Winters forecasting (PL/pgSQL)
-- Author of algorithm: Pugolovok Alexander (tg: @alexander_pu)
-- Adapted for pg_stl extension: data is passed as array parameter
-- instead of reading from a fixed table.
-- Requires PostgreSQL 16+ (uses random() * x for coefficients).
-- ============================================================

CREATE OR REPLACE FUNCTION holt_winters_mse(
    seasonal_type         text,
    start_data_array      real[],
    row_data_length       int,
    period_length         int,
    mean_data_first_period real,
    levell                real[],
    trend                 real[],
    seasonal              real[],
    forecast              real[],
    alpha                 real,
    beta                  real,
    gamma                 real
)
/*
  Compute total squared error of the Holt-Winters forecast vs. the given series.

  Parameters:
    seasonal_type          : 'mult' or 'add'
    start_data_array       : input time series
    row_data_length        : number of elements in the series
    period_length          : length of one seasonal cycle
    mean_data_first_period : mean of the first period (used for initialisation)
    levell                 : array of level values (pre-initialised)
    trend                  : array of trend values (pre-initialised)
    seasonal               : array of seasonal values (pre-initialised)
    forecast               : array of forecast values (pre-initialised)
    alpha                  : smoothing coefficient for level  (0 <= alpha < 1)
    beta                   : smoothing coefficient for trend  (beta < alpha)
    gamma                  : smoothing coefficient for seasonality (gamma < 1 - alpha)

  Returns:
    Total squared error (real).
*/
RETURNS real
LANGUAGE plpgsql
AS $$
DECLARE
    mse_error real := 0.0;
BEGIN
    IF seasonal_type = 'mult' THEN
        FOR i IN period_length + 1 .. row_data_length LOOP
            levell[i]  := alpha * (start_data_array[i] / seasonal[i - period_length])
                          + (1 - alpha) * (levell[i-1] + trend[i-1]);
            trend[i]   := beta  * (levell[i] - levell[i-1]) + (1 - beta)  * trend[i-1];
            seasonal[i]:= gamma * (start_data_array[i] / levell[i])
                          + (1 - gamma) * seasonal[i - period_length];
            forecast[i]:= (levell[i-1] + trend[i-1]) * seasonal[i - period_length];
        END LOOP;

        FOR i IN period_length + 1 .. row_data_length LOOP
            mse_error := mse_error + (forecast[i] - start_data_array[i])^2;
        END LOOP;
    END IF;

    IF seasonal_type = 'add' THEN
        FOR i IN period_length + 1 .. row_data_length LOOP
            levell[i]  := alpha * (start_data_array[i] - seasonal[i - period_length])
                          + (1 - alpha) * (levell[i-1] + trend[i-1]);
            trend[i]   := beta  * (levell[i] - levell[i-1]) + (1 - beta)  * trend[i-1];
            seasonal[i]:= gamma * (start_data_array[i] - levell[i])
                          + (1 - gamma) * seasonal[i - period_length];
            forecast[i]:= levell[i-1] + trend[i-1] + seasonal[i - period_length];
        END LOOP;

        FOR i IN period_length + 1 .. row_data_length LOOP
            mse_error := mse_error + (forecast[i] - start_data_array[i])^2;
        END LOOP;
    END IF;

    RETURN mse_error;
END;
$$;


CREATE OR REPLACE FUNCTION holt_winters_predict(
    seasonal_type     text,
    period_length     int,
    start_data_array  real[]
)
/*
  Forecast one full seasonal cycle ahead using the Holt-Winters method.

  Parameters:
    seasonal_type    : 'mult' (multiplicative) or 'add' (additive)
    period_length    : length of one seasonal cycle (e.g. 4 for quarterly, 12 for monthly)
    start_data_array : the observed time series as a real[] array

  Returns:
    Table of forecasted values (one row per period step ahead).

  Example:
    SELECT * FROM holt_winters_predict(
        'mult',
        4,
        ARRAY[362,385,432,341,382,409,498,387,473,513,582,474,544,582,681,557,628,707,773,592,627,725,854,661]::real[]
    );

  Notes:
    - Exponential smoothing coefficients (alpha, beta, gamma) are selected automatically
      by minimising the total squared error via 500 random initialisations followed by
      gradient-style refinement in steps of 0.001.
    - Constraints: alpha in [0, 1), beta < alpha, gamma < 1 - alpha.
    - Compatible with PostgreSQL 16+.
*/
RETURNS TABLE(val real)
LANGUAGE plpgsql
AS $$
DECLARE
    row_data_length        integer;
    mean_data_first_period real    := 0.0;
    alpha                  numeric := 0.0;
    beta                   numeric := 0.0;
    gamma                  numeric := 0.0;
    levell                 real[];
    trend                  real[];
    seasonal               real[];
    forecast               real[];
    forecast_ahead         real[];
    err_alf_bet_gam_arr    real[][];
    array_error_args       real[][];
    tmp_mse_error          real    := 0.0;
    mse_error              real    := 0.0;
    tmp_for_trend_1        real    := 0.0;
    tmp_arg_dif            real    := 0.0;
    cnt_in_loop            integer := 0;
BEGIN
    row_data_length := array_length(start_data_array, 1);

    -- Mean of first period
    mean_data_first_period := (
        SELECT ROUND(AVG(x)::numeric, 3)
        FROM unnest(start_data_array[1:period_length]) AS x
    );

    -- Initial seasonality
    IF seasonal_type = 'mult' THEN
        FOR i IN 1 .. period_length LOOP
            seasonal := array_append(seasonal, start_data_array[i] / mean_data_first_period);
        END LOOP;
    END IF;
    IF seasonal_type = 'add' THEN
        FOR i IN 1 .. period_length LOOP
            seasonal := array_append(seasonal, start_data_array[i] - mean_data_first_period);
        END LOOP;
    END IF;
    FOR i IN period_length + 1 .. row_data_length LOOP
        seasonal := array_append(seasonal, NULL);
    END LOOP;

    -- Initial level
    FOR i IN 1 .. period_length - 1 LOOP
        levell := array_append(levell, NULL);
    END LOOP;
    levell := array_append(levell, mean_data_first_period);
    FOR i IN period_length + 1 .. row_data_length LOOP
        levell := array_append(levell, NULL);
    END LOOP;

    -- Initial trend
    FOR i IN 1 .. period_length - 1 LOOP
        trend := array_append(trend, NULL);
    END LOOP;
    FOR i IN 1 .. period_length LOOP
        tmp_for_trend_1 := tmp_for_trend_1
            + (start_data_array[period_length + i] - start_data_array[i]) / period_length^2;
    END LOOP;
    trend := array_append(trend, tmp_for_trend_1);
    FOR i IN period_length + 1 .. row_data_length LOOP
        trend := array_append(trend, NULL);
    END LOOP;

    -- Initial forecast array
    forecast := array_fill(0.0, ARRAY[row_data_length]);
    FOR i IN 1 .. period_length LOOP
        forecast[i] := NULL;
    END LOOP;

    -- Step 1: random search for best alpha/beta/gamma (500 iterations)
    FOR _ IN 1 .. 500 LOOP
        alpha     := random() * 0.999;
        beta      := random() * alpha;
        gamma     := random() * (1.0 - alpha);
        mse_error := holt_winters_mse(
            seasonal_type, start_data_array, row_data_length, period_length,
            mean_data_first_period, levell, trend, seasonal, forecast,
            alpha::real, beta::real, gamma::real
        );
        err_alf_bet_gam_arr := array_cat(
            err_alf_bet_gam_arr,
            ARRAY[ARRAY[mse_error, alpha::real, beta::real, gamma::real]]
        );
    END LOOP;

    -- Pick best from random search
    SELECT e[1], e[2], e[3], e[4]
    INTO mse_error, alpha, beta, gamma
    FROM (
        SELECT ARRAY[t[1], t[2], t[3], t[4]] AS e
        FROM unnest(err_alf_bet_gam_arr) WITH ORDINALITY AS u(t, ord)
        ORDER BY t[1]
        LIMIT 1
    ) sub;

    -- Recompute with best coefficients
    mse_error := holt_winters_mse(
        seasonal_type, start_data_array, row_data_length, period_length,
        mean_data_first_period, levell, trend, seasonal, forecast,
        alpha::real, beta::real, gamma::real
    );

    -- Step 2: gradient refinement in steps of 0.001
    LOOP
        cnt_in_loop := cnt_in_loop + 1;
        EXIT WHEN cnt_in_loop >= 500;

        array_error_args := NULL;

        -- alpha + 0.001
        IF alpha < 0.999 THEN
            tmp_arg_dif   := alpha + 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, tmp_arg_dif::real, beta::real, gamma::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := alpha;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, tmp_arg_dif::real, beta::real, gamma::real]]);

        -- alpha - 0.001
        IF alpha >= 0.001 THEN
            tmp_arg_dif   := alpha - 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, tmp_arg_dif::real, beta::real, gamma::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := alpha;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, tmp_arg_dif::real, beta::real, gamma::real]]);

        -- beta + 0.001
        IF beta < alpha - 0.001 THEN
            tmp_arg_dif   := beta + 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, alpha::real, tmp_arg_dif::real, gamma::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := beta;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, alpha::real, tmp_arg_dif::real, gamma::real]]);

        -- beta - 0.001
        IF beta >= 0.001 THEN
            tmp_arg_dif   := beta - 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, alpha::real, tmp_arg_dif::real, gamma::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := beta;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, alpha::real, tmp_arg_dif::real, gamma::real]]);

        -- gamma + 0.001
        IF gamma < 0.999 - alpha THEN
            tmp_arg_dif   := gamma + 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, alpha::real, beta::real, tmp_arg_dif::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := gamma;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, alpha::real, beta::real, tmp_arg_dif::real]]);

        -- gamma - 0.001
        IF gamma >= 0.001 THEN
            tmp_arg_dif   := gamma - 0.001;
            tmp_mse_error := holt_winters_mse(seasonal_type, start_data_array, row_data_length, period_length,
                mean_data_first_period, levell, trend, seasonal, forecast, alpha::real, beta::real, tmp_arg_dif::real);
        ELSE
            tmp_mse_error := mse_error; tmp_arg_dif := gamma;
        END IF;
        array_error_args := array_cat(array_error_args, ARRAY[ARRAY[tmp_mse_error, alpha::real, beta::real, tmp_arg_dif::real]]);

        -- Pick best candidate
        SELECT e[1], e[2], e[3], e[4]
        INTO tmp_mse_error, tmp_arg_dif, beta, gamma
        FROM (
            SELECT ARRAY[t[1], t[2], t[3], t[4]] AS e
            FROM unnest(array_error_args) WITH ORDINALITY AS u(t, ord)
            ORDER BY t[1]
            LIMIT 1
        ) sub;

        EXIT WHEN tmp_mse_error >= mse_error;

        mse_error := tmp_mse_error;
        alpha     := tmp_arg_dif;
    END LOOP;

    -- Final pass: compute level/trend/seasonal with optimised coefficients
    IF seasonal_type = 'mult' THEN
        FOR i IN period_length + 1 .. row_data_length LOOP
            levell[i]  := alpha::real * (start_data_array[i] / seasonal[i - period_length])
                          + (1 - alpha::real) * (levell[i-1] + trend[i-1]);
            trend[i]   := beta::real  * (levell[i] - levell[i-1]) + (1 - beta::real)  * trend[i-1];
            seasonal[i]:= gamma::real * (start_data_array[i] / levell[i])
                          + (1 - gamma::real) * seasonal[i - period_length];
        END LOOP;

        FOR i IN row_data_length + 1 .. row_data_length + period_length LOOP
            forecast_ahead := array_append(forecast_ahead,
                (levell[row_data_length] + trend[row_data_length] * (i - row_data_length))
                * seasonal[i - period_length]);
        END LOOP;
    END IF;

    IF seasonal_type = 'add' THEN
        FOR i IN period_length + 1 .. row_data_length LOOP
            levell[i]  := alpha::real * (start_data_array[i] - seasonal[i - period_length])
                          + (1 - alpha::real) * (levell[i-1] + trend[i-1]);
            trend[i]   := beta::real  * (levell[i] - levell[i-1]) + (1 - beta::real)  * trend[i-1];
            seasonal[i]:= gamma::real * (start_data_array[i] - levell[i])
                          + (1 - gamma::real) * seasonal[i - period_length];
        END LOOP;

        FOR i IN row_data_length + 1 .. row_data_length + period_length LOOP
            forecast_ahead := array_append(forecast_ahead,
                levell[row_data_length] + trend[row_data_length] * (i - row_data_length)
                + seasonal[i - period_length]);
        END LOOP;
    END IF;

    -- Return forecast
    FOR i IN 1 .. period_length LOOP
        val := forecast_ahead[i];
        RETURN NEXT;
    END LOOP;
END;
$$;
