-- Custom PostgreSQL functions used by the metrics pipeline
-- These must exist in the database before running calculate-metrics-new.js
--
-- To install/update: psql -d inventory_db -f functions.sql
-- All functions use CREATE OR REPLACE so they are safe to re-run.

-- =============================================================================
-- safe_divide: Division helper that returns a default value instead of erroring
-- on NULL or zero denominators.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.safe_divide(
    numerator numeric,
    denominator numeric,
    default_value numeric DEFAULT NULL::numeric
)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
    IF denominator IS NULL OR denominator = 0 THEN
        RETURN default_value;
    ELSE
        RETURN numerator / denominator;
    END IF;
END;
$function$;

-- =============================================================================
-- std_numeric: Standardized rounding helper for consistent numeric precision.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.std_numeric(
    value numeric,
    precision_digits integer DEFAULT 2
)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
    IF value IS NULL THEN
        RETURN NULL;
    ELSE
        RETURN ROUND(value, precision_digits);
    END IF;
END;
$function$;

-- =============================================================================
-- calculate_sales_velocity: Daily sales velocity adjusted for stockout days.
-- Ensures at least 14-day denominator for products with sales to avoid
-- inflated velocity from short windows.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.calculate_sales_velocity(
    sales_30d integer,
    stockout_days_30d integer
)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
    RETURN sales_30d /
        NULLIF(
            GREATEST(
                30.0 - stockout_days_30d,
                CASE
                    WHEN sales_30d > 0 THEN 14.0  -- If we have sales, ensure at least 14 days denominator
                    ELSE 30.0                      -- If no sales, use full period
                END
            ),
            0
        );
END;
$function$;

-- =============================================================================
-- get_weighted_avg_cost: Weighted average cost from receivings up to a given date.
-- Prefers receivings from the 365 days before p_date so decade-old costs don't
-- weigh equally with recent ones; falls back to the lifetime average when the
-- product had no receivings in that window.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.get_weighted_avg_cost(
    p_pid bigint,
    p_date date
)
RETURNS numeric
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
    weighted_cost NUMERIC;
BEGIN
    SELECT
        CASE
            WHEN SUM(qty_each) > 0 THEN SUM(cost_each * qty_each) / SUM(qty_each)
            ELSE NULL
        END INTO weighted_cost
    FROM receivings
    WHERE pid = p_pid
        AND received_date <= p_date
        AND received_date > p_date - INTERVAL '365 days'
        AND status != 'canceled';

    IF weighted_cost IS NULL THEN
        SELECT
            CASE
                WHEN SUM(qty_each) > 0 THEN SUM(cost_each * qty_each) / SUM(qty_each)
                ELSE NULL
            END INTO weighted_cost
        FROM receivings
        WHERE pid = p_pid
            AND received_date <= p_date
            AND status != 'canceled';
    END IF;

    RETURN weighted_cost;
END;
$function$;

-- =============================================================================
-- classify_demand_pattern: Classifies demand based on average demand and
-- coefficient of variation (CV). Standard inventory classification:
--   zero:     no demand
--   stable:   CV <= 0.2 (predictable, easy to forecast)
--   variable: CV <= 0.5 (some variability, still forecastable)
--   sporadic: low volume + high CV (intermittent demand)
--   lumpy:    high volume + high CV (unpredictable bursts)
-- =============================================================================
CREATE OR REPLACE FUNCTION public.classify_demand_pattern(
    avg_demand numeric,
    cv numeric
)
RETURNS character varying
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
    IF avg_demand IS NULL OR cv IS NULL THEN
        RETURN NULL;
    ELSIF avg_demand = 0 THEN
        RETURN 'zero';
    ELSIF cv <= 0.2 THEN
        RETURN 'stable';
    ELSIF cv <= 0.5 THEN
        RETURN 'variable';
    ELSIF avg_demand < 1.0 THEN
        RETURN 'sporadic';
    ELSE
        RETURN 'lumpy';
    END IF;
END;
$function$;

-- =============================================================================
-- detect_seasonal_pattern: Detects seasonality by comparing monthly average
-- sales across the last 12 months. Uses coefficient of variation across months
-- and peak-to-average ratio to classify patterns.
--
-- Returns:
--   seasonal_pattern: 'none', 'moderate', or 'strong'
--   seasonality_index: peak month avg / overall avg * 100 (100 = no seasonality)
--   peak_season: name of peak month (e.g. 'January'), or NULL if none
-- =============================================================================
CREATE OR REPLACE FUNCTION public.detect_seasonal_pattern(p_pid bigint)
RETURNS TABLE(seasonal_pattern character varying, seasonality_index numeric, peak_season character varying)
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
    v_monthly_cv NUMERIC;
    v_max_month_avg NUMERIC;
    v_overall_avg NUMERIC;
    v_monthly_stddev NUMERIC;
    v_peak_month_num INT;
    v_data_months INT;
    v_seasonality_index NUMERIC;
    v_seasonal_pattern VARCHAR;
    v_peak_season VARCHAR;
BEGIN
    -- Gather monthly average sales and peak month in a single query
    SELECT
        COUNT(*),
        AVG(month_avg),
        STDDEV(month_avg),
        MAX(month_avg),
        (ARRAY_AGG(mo ORDER BY month_avg DESC))[1]::INT
    INTO v_data_months, v_overall_avg, v_monthly_stddev, v_max_month_avg, v_peak_month_num
    FROM (
        SELECT EXTRACT(MONTH FROM snapshot_date) AS mo, AVG(units_sold) AS month_avg
        FROM daily_product_snapshots
        WHERE pid = p_pid AND snapshot_date >= CURRENT_DATE - INTERVAL '365 days'
        GROUP BY EXTRACT(MONTH FROM snapshot_date)
    ) monthly;

    -- Need at least 3 months of data for meaningful seasonality detection
    IF v_data_months < 3 OR v_overall_avg IS NULL OR v_overall_avg = 0 THEN
        RETURN QUERY SELECT 'none'::VARCHAR, 100::NUMERIC, NULL::VARCHAR;
        RETURN;
    END IF;

    -- CV of monthly averages
    v_monthly_cv := v_monthly_stddev / v_overall_avg;

    -- Seasonality index: peak month avg / overall avg * 100
    v_seasonality_index := ROUND((v_max_month_avg / v_overall_avg * 100)::NUMERIC, 2);

    IF v_monthly_cv > 0.5 AND v_seasonality_index > 150 THEN
        v_seasonal_pattern := 'strong';
        v_peak_season := TRIM(TO_CHAR(TO_DATE(v_peak_month_num::TEXT, 'MM'), 'Month'));
    ELSIF v_monthly_cv > 0.3 AND v_seasonality_index > 120 THEN
        v_seasonal_pattern := 'moderate';
        v_peak_season := TRIM(TO_CHAR(TO_DATE(v_peak_month_num::TEXT, 'MM'), 'Month'));
    ELSE
        v_seasonal_pattern := 'none';
        v_peak_season := NULL;
        v_seasonality_index := 100;
    END IF;

    RETURN QUERY SELECT v_seasonal_pattern, v_seasonality_index, v_peak_season;
END;
$function$;

-- =============================================================================
-- category_hierarchy: Materialized view providing a recursive category tree
-- with ancestor paths for efficient rollup queries.
--
-- Refresh after category changes: REFRESH MATERIALIZED VIEW category_hierarchy;
-- =============================================================================
-- DROP MATERIALIZED VIEW IF EXISTS category_hierarchy;
-- CREATE MATERIALIZED VIEW category_hierarchy AS
-- WITH RECURSIVE cat_tree AS (
--     SELECT cat_id, name, type, parent_id,
--            cat_id AS root_id, 0 AS level, ARRAY[cat_id] AS path
--     FROM categories
--     WHERE parent_id IS NULL
--     UNION ALL
--     SELECT c.cat_id, c.name, c.type, c.parent_id,
--            ct.root_id, ct.level + 1, ct.path || c.cat_id
--     FROM categories c
--     JOIN cat_tree ct ON c.parent_id = ct.cat_id
-- )
-- SELECT cat_id, name, type, parent_id, root_id, level, path,
--        (SELECT array_agg(unnest ORDER BY unnest DESC)
--         FROM unnest(cat_tree.path) unnest
--         WHERE unnest <> cat_tree.cat_id) AS ancestor_ids
-- FROM cat_tree;
--
-- CREATE UNIQUE INDEX ON category_hierarchy (cat_id);
