-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Populates lifecycle forecast columns on product_metrics from product_forecasts.
--              Runs AFTER update_product_metrics.sql so that lead time / days of stock settings are available.
-- Dependencies: product_metrics (fully populated), product_forecasts, settings tables.
-- Frequency: After each metrics run and/or after forecast engine runs.

DO $$
DECLARE
    _module_name TEXT := 'lifecycle_forecasts';
    _start_time TIMESTAMPTZ := clock_timestamp();
    _updated INT;
BEGIN
    RAISE NOTICE 'Running % module. Start Time: %', _module_name, _start_time;

    -- Step 1: Set lifecycle_phase from product_forecasts (one phase per product)
    UPDATE product_metrics pm
    SET lifecycle_phase = sub.lifecycle_phase
    FROM (
        SELECT DISTINCT ON (pid) pid, lifecycle_phase
        FROM product_forecasts
        ORDER BY pid, forecast_date
    ) sub
    WHERE pm.pid = sub.pid
      AND (pm.lifecycle_phase IS DISTINCT FROM sub.lifecycle_phase);

    GET DIAGNOSTICS _updated = ROW_COUNT;
    RAISE NOTICE 'Updated lifecycle_phase for % products', _updated;

    -- Step 2: Compute lifecycle-based lead time and planning period forecasts
    -- Uses each product's configured lead time and days of stock
    WITH forecast_sums AS (
        SELECT
            pf.pid,
            SUM(pf.forecast_units) FILTER (
                WHERE pf.forecast_date <= CURRENT_DATE + s.effective_lead_time
            ) AS lt_forecast,
            SUM(pf.forecast_units) FILTER (
                WHERE pf.forecast_date <= CURRENT_DATE + s.effective_lead_time + s.effective_days_of_stock
            ) AS pp_forecast
        FROM product_forecasts pf
        JOIN (
            SELECT
                p.pid,
                COALESCE(sp.lead_time_days, sv.default_lead_time_days,
                    (SELECT setting_value::int FROM settings_global WHERE setting_key = 'default_lead_time_days'), 14
                ) AS effective_lead_time,
                COALESCE(sp.days_of_stock, sv.default_days_of_stock,
                    (SELECT setting_value::int FROM settings_global WHERE setting_key = 'default_days_of_stock'), 30
                ) AS effective_days_of_stock
            FROM products p
            LEFT JOIN settings_product sp ON p.pid = sp.pid
            LEFT JOIN settings_vendor sv ON p.vendor = sv.vendor
        ) s ON s.pid = pf.pid
        WHERE pf.forecast_date >= CURRENT_DATE
        GROUP BY pf.pid
    )
    UPDATE product_metrics pm
    SET
        lifecycle_lead_time_forecast = COALESCE(fs.lt_forecast, 0),
        lifecycle_planning_period_forecast = COALESCE(fs.pp_forecast, 0)
    FROM forecast_sums fs
    WHERE pm.pid = fs.pid
      AND (pm.lifecycle_lead_time_forecast IS DISTINCT FROM COALESCE(fs.lt_forecast, 0)
        OR pm.lifecycle_planning_period_forecast IS DISTINCT FROM COALESCE(fs.pp_forecast, 0));

    GET DIAGNOSTICS _updated = ROW_COUNT;
    RAISE NOTICE 'Updated lifecycle forecasts for % products', _updated;

    -- Step 3: Reclassify demand_pattern using residual CV (de-trended)
    -- For launch/decay products, raw CV is high because of expected lifecycle decay.
    -- We subtract the expected brand curve value to get residuals, then compute CV on those.
    -- Products that track their brand curve closely → low residual CV → "stable"
    -- Products with erratic deviations from curve → higher residual CV → "variable"/"sporadic"
    WITH product_curve AS (
        -- Get each product's brand curve and age
        SELECT
            pm.pid,
            pm.lifecycle_phase,
            pm.date_first_received,
            blc.amplitude,
            blc.decay_rate,
            blc.baseline
        FROM product_metrics pm
        JOIN products p ON p.pid = pm.pid
        LEFT JOIN brand_lifecycle_curves blc
            ON blc.brand = pm.brand
            AND blc.root_category IS NULL  -- brand-only curve
        WHERE pm.lifecycle_phase IN ('launch', 'decay')
          AND pm.date_first_received IS NOT NULL
          AND blc.amplitude IS NOT NULL
    ),
    daily_residuals AS (
        -- Compute residual = actual - expected for each snapshot day
        -- Curve params are in WEEKLY units; divide by 7 to get daily expected
        SELECT
            dps.pid,
            dps.units_sold,
            (pc.amplitude * EXP(-pc.decay_rate * (dps.snapshot_date - pc.date_first_received)::numeric / 7.0) + pc.baseline) / 7.0 AS expected,
            dps.units_sold - (pc.amplitude * EXP(-pc.decay_rate * (dps.snapshot_date - pc.date_first_received)::numeric / 7.0) + pc.baseline) / 7.0 AS residual
        FROM daily_product_snapshots dps
        JOIN product_curve pc ON pc.pid = dps.pid
        WHERE dps.snapshot_date >= CURRENT_DATE - INTERVAL '29 days'
          AND dps.snapshot_date <= CURRENT_DATE
    ),
    residual_cv AS (
        SELECT
            pid,
            AVG(units_sold) AS avg_sales,
            CASE WHEN COUNT(*) >= 7 AND AVG(ABS(expected)) > 0.01 THEN
                STDDEV_POP(residual) / GREATEST(AVG(ABS(expected)), 0.1)
            END AS res_cv
        FROM daily_residuals
        GROUP BY pid
    )
    UPDATE product_metrics pm
    SET demand_pattern = classify_demand_pattern(rc.avg_sales, rc.res_cv)
    FROM residual_cv rc
    WHERE pm.pid = rc.pid
      AND rc.res_cv IS NOT NULL
      AND pm.demand_pattern IS DISTINCT FROM classify_demand_pattern(rc.avg_sales, rc.res_cv);

    GET DIAGNOSTICS _updated = ROW_COUNT;
    RAISE NOTICE 'Reclassified demand_pattern for % launch/decay products', _updated;

    -- Update tracking
    INSERT INTO public.calculate_status (module_name, last_calculation_timestamp)
    VALUES (_module_name, clock_timestamp())
    ON CONFLICT (module_name) DO UPDATE SET
        last_calculation_timestamp = EXCLUDED.last_calculation_timestamp;

    RAISE NOTICE '% module complete. Duration: %', _module_name, clock_timestamp() - _start_time;
END $$;
