-- 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: Calculates and updates the main product_metrics table based on current data
--              and aggregated daily snapshots. Uses UPSERT for idempotency.
-- Dependencies: Core import tables, daily_product_snapshots, configuration tables, calculate_status.
-- Frequency: Hourly (Run AFTER update_daily_snapshots.sql completes).

DO $$
DECLARE
    _module_name TEXT := 'product_metrics';
    _start_time TIMESTAMPTZ := clock_timestamp();
    _last_calc_time TIMESTAMPTZ;
    _current_date DATE := CURRENT_DATE;
BEGIN
    -- Get the timestamp before the last successful run of this module
    SELECT last_calculation_timestamp INTO _last_calc_time
    FROM public.calculate_status
    WHERE module_name = _module_name;

    RAISE NOTICE 'Running % module. Start Time: %', _module_name, _start_time;

    -- Use CTEs to gather all necessary information
    WITH CurrentInfo AS (
        SELECT
            p.pid,
            p.sku,
            p.title,
            p.brand,
            p.vendor,
            COALESCE(p.image_175, p.image) as image_url,
            p.visible as is_visible,
            p.replenishable as is_replenishable,
            -- Add new product fields
            p.barcode,
            p.harmonized_tariff_code,
            p.vendor_reference,
            p.notions_reference,
            p.line,
            p.subline,
            p.artist,
            p.moq,
            p.rating,
            p.reviews,
            p.weight,
            p.length,
            p.width,
            p.height,
            p.country_of_origin,
            p.location,
            p.baskets,
            p.notifies,
            p.preorder_count,
            p.notions_inv_count,
            COALESCE(p.price, 0.00) as current_price,
            COALESCE(p.regular_price, 0.00) as current_regular_price,
            COALESCE(p.cost_price, 0.00) as current_cost_price,
            COALESCE(p.cost_price, 0.00) as current_effective_cost,
            p.stock_quantity as current_stock,
            p.created_at,
            p.first_received,
            p.date_last_sold,
            p.total_sold as historical_total_sold, -- Add historical total_sold from products table
            p.uom -- Assuming UOM logic is handled elsewhere or simple (e.g., 1=each)
        FROM public.products p
    ),
    -- Stale POs: open, >90 days past expected, AND a newer PO exists for the same product.
    -- These are likely abandoned/superseded and should not consume receivings in FIFO.
    StalePOLines AS (
        SELECT po.po_id, po.pid
        FROM public.purchase_orders po
        WHERE po.status IN ('created', 'ordered', 'preordered', 'electronically_sent',
                            'electronically_ready_send', 'receiving_started')
          AND po.expected_date IS NOT NULL
          AND po.expected_date < _current_date - INTERVAL '90 days'
          AND EXISTS (
              SELECT 1 FROM public.purchase_orders newer
              WHERE newer.pid = po.pid
                AND newer.status NOT IN ('canceled', 'done')
                AND COALESCE(newer.date_ordered, newer.date_created)
                    > COALESCE(po.date_ordered, po.date_created)
          )
    ),
    -- All non-canceled, non-stale POs in FIFO order per (pid, supplier).
    -- Includes closed ('done') POs so they consume receivings before open POs.
    POFifo AS (
        SELECT
            po.pid, po.supplier_id, po.po_id, po.ordered, po.status,
            po.po_cost_price, po.expected_date,
            SUM(po.ordered) OVER (
                PARTITION BY po.pid, po.supplier_id
                ORDER BY COALESCE(po.date_ordered, po.date_created), po.po_id
            ) - po.ordered AS cumulative_before
        FROM public.purchase_orders po
        WHERE po.status != 'canceled'
          AND NOT EXISTS (
              SELECT 1 FROM StalePOLines s
              WHERE s.po_id = po.po_id AND s.pid = po.pid
          )
    ),
    -- Total received per (product, supplier) across all receivings.
    SupplierReceived AS (
        SELECT pid, supplier_id, SUM(qty_each) AS total_received
        FROM public.receivings
        WHERE status IN ('partial_received', 'full_received', 'paid')
        GROUP BY pid, supplier_id
    ),
    -- FIFO allocation: receivings fill oldest POs first per (pid, supplier).
    -- Only open PO lines are reported; closed POs just absorb receivings.
    OnOrderInfo AS (
        SELECT
            po.pid,
            SUM(GREATEST(0,
                po.ordered - GREATEST(0, LEAST(po.ordered,
                    COALESCE(sr.total_received, 0) - po.cumulative_before
                ))
            )) AS on_order_qty,
            SUM(GREATEST(0,
                po.ordered - GREATEST(0, LEAST(po.ordered,
                    COALESCE(sr.total_received, 0) - po.cumulative_before
                ))
            ) * po.po_cost_price) AS on_order_cost,
            MIN(po.expected_date) FILTER (WHERE
                po.ordered > GREATEST(0, LEAST(po.ordered,
                    COALESCE(sr.total_received, 0) - po.cumulative_before
                ))
            ) AS earliest_expected_date
        FROM POFifo po
        LEFT JOIN SupplierReceived sr ON sr.pid = po.pid AND sr.supplier_id = po.supplier_id
        WHERE po.status IN ('created', 'ordered', 'preordered', 'electronically_sent',
                            'electronically_ready_send', 'receiving_started')
        GROUP BY po.pid
    ),
    HistoricalDates AS (
        -- Note: Calculating these MIN/MAX values hourly can be slow on large tables.
        -- Consider calculating periodically or storing on products if import can populate them.
        -- Dates are bucketed in business time (America/Chicago) to match daily snapshots.
        SELECT
            p.pid,
            MIN((o.date AT TIME ZONE 'America/Chicago'))::date AS date_first_sold,
            MAX((o.date AT TIME ZONE 'America/Chicago'))::date AS max_order_date, -- Use MAX for potential recalc of date_last_sold

            -- For first received, use the new receivings table
            MIN((r.received_date AT TIME ZONE 'America/Chicago'))::date AS date_first_received_calc,

            -- For last received, use the new receivings table
            MAX((r.received_date AT TIME ZONE 'America/Chicago'))::date AS date_last_received_calc
        FROM public.products p
        LEFT JOIN public.orders o ON p.pid = o.pid AND o.quantity > 0 AND o.status NOT IN ('canceled', 'returned', 'combined')
        LEFT JOIN public.receivings r ON p.pid = r.pid
        GROUP BY p.pid
    ),
    SnapshotAggregates AS (
        SELECT
            pid,
            -- Get the counts of all available data 
            COUNT(DISTINCT snapshot_date) AS available_days,
            
            -- Rolling periods with no time constraint - just sum everything we have
            SUM(units_sold) AS total_units_sold,
            SUM(net_revenue) AS total_net_revenue,
            
            -- Specific time windows using date range boundaries precisely
            -- Use _current_date - INTERVAL '6 days' to include 7 days (today + 6 previous days)
            -- This ensures we count exactly the right number of days in each period
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '6 days' AND snapshot_date <= _current_date THEN units_sold ELSE 0 END) AS sales_7d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '6 days' AND snapshot_date <= _current_date THEN net_revenue ELSE 0 END) AS revenue_7d,
            
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '13 days' AND snapshot_date <= _current_date THEN units_sold ELSE 0 END) AS sales_14d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '13 days' AND snapshot_date <= _current_date THEN net_revenue ELSE 0 END) AS revenue_14d,
            
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN units_sold ELSE 0 END) AS sales_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN net_revenue ELSE 0 END) AS revenue_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN cogs ELSE 0 END) AS cogs_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN profit ELSE 0 END) AS profit_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN units_returned ELSE 0 END) AS returns_units_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN returns_revenue ELSE 0 END) AS returns_revenue_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN discounts ELSE 0 END) AS discounts_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN gross_revenue ELSE 0 END) AS gross_revenue_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN gross_regular_revenue ELSE 0 END) AS gross_regular_revenue_30d,
            -- NOTE: stockout days and avg stock units/cost now come from StockCoverage
            -- (stock_snapshots has full daily coverage; these activity-only snapshots
            -- only exist on days with sales/receivings, which made stockout_days ~0
            -- exactly when stockouts mattered and biased stock averages upward).

            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '364 days' AND snapshot_date <= _current_date THEN units_sold ELSE 0 END) AS sales_365d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '364 days' AND snapshot_date <= _current_date THEN net_revenue ELSE 0 END) AS revenue_365d,
            
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN units_received ELSE 0 END) AS received_qty_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN cost_received ELSE 0 END) AS received_cost_30d,

            -- Retail/gross stock averages stay on activity snapshots: stock_snapshots
            -- has no eod_stock_retail equivalent (cost-only source table).
            AVG(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN eod_stock_retail END) AS avg_stock_retail_30d,
            AVG(CASE WHEN snapshot_date >= _current_date - INTERVAL '29 days' AND snapshot_date <= _current_date THEN eod_stock_gross END) AS avg_stock_gross_30d,

            -- Lifetime - should match total values above
            SUM(units_sold) AS lifetime_sales,
            SUM(net_revenue) AS lifetime_revenue,

            -- Yesterday
            SUM(CASE WHEN snapshot_date = _current_date - INTERVAL '1 day' THEN units_sold ELSE 0 END) as yesterday_sales

        FROM public.daily_product_snapshots
        GROUP BY pid
    ),
    BeginningStock AS (
        -- Get stock level from 30 days ago for sell-through calculation.
        -- Uses the closest available snapshot if exact date is missing (activity-only snapshots).
        SELECT DISTINCT ON (pid)
            pid,
            eod_stock_quantity AS beginning_stock_30d
        FROM public.daily_product_snapshots
        WHERE snapshot_date <= _current_date - INTERVAL '30 days'
          AND snapshot_date >= _current_date - INTERVAL '37 days'
        ORDER BY pid, snapshot_date DESC
    ),
    FirstPeriodMetrics AS (
      SELECT
          pid,
          date_first_sold,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '6 days' THEN units_sold ELSE 0 END) AS first_7_days_sales,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '6 days' THEN net_revenue ELSE 0 END) AS first_7_days_revenue,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '29 days' THEN units_sold ELSE 0 END) AS first_30_days_sales,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '29 days' THEN net_revenue ELSE 0 END) AS first_30_days_revenue,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '59 days' THEN units_sold ELSE 0 END) AS first_60_days_sales,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '59 days' THEN net_revenue ELSE 0 END) AS first_60_days_revenue,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '89 days' THEN units_sold ELSE 0 END) AS first_90_days_sales,
          SUM(CASE WHEN snapshot_date >= date_first_sold AND snapshot_date <= date_first_sold + INTERVAL '89 days' THEN net_revenue ELSE 0 END) AS first_90_days_revenue
      FROM public.daily_product_snapshots ds
      JOIN HistoricalDates hd USING(pid)
      WHERE date_first_sold IS NOT NULL
        AND snapshot_date >= date_first_sold
        AND snapshot_date <= date_first_sold + INTERVAL '90 days' -- Limit scan range
      GROUP BY pid, date_first_sold
    ),
    Settings AS (
      SELECT
        p.pid,
        COALESCE(sp.lead_time_days, sv.default_lead_time_days, (SELECT setting_value FROM settings_global WHERE setting_key = 'default_lead_time_days')::int, 14) AS effective_lead_time,
        COALESCE(sp.days_of_stock, sv.default_days_of_stock, (SELECT setting_value FROM settings_global WHERE setting_key = 'default_days_of_stock')::int, 30) AS effective_days_of_stock,
        COALESCE(sp.safety_stock, 0) AS effective_safety_stock, -- Assuming safety stock is units, not days from global for now
        COALESCE(sp.exclude_from_forecast, FALSE) AS exclude_forecast
      FROM public.products p
      LEFT JOIN public.settings_product sp ON p.pid = sp.pid
      LEFT JOIN public.settings_vendor sv ON p.vendor = sv.vendor
    ),
    LifetimeRevenue AS (
        -- Calculate actual revenue from orders table. Negative-quantity rows
        -- (returns) are included so lifetime revenue nets out returns;
        -- price * quantity is already signed.
        SELECT
            o.pid,
            SUM(o.price * o.quantity - COALESCE(o.discount, 0)) AS lifetime_revenue_from_orders,
            SUM(o.quantity) AS lifetime_units_from_orders
        FROM public.orders o
        WHERE o.status NOT IN ('canceled', 'returned', 'combined')
        GROUP BY o.pid
    ),
    -- Full-coverage stock presence from stock_snapshots (MySQL snap_product_value).
    -- That source only writes rows for products WITH stock on hand, so a product
    -- missing from a day the cron ran was out of stock that day. Days before the
    -- product was created are not counted against it.
    StockCoverage AS (
        SELECT
            pid,
            eligible_days_30d,
            days_in_stock_30d,
            CASE WHEN eligible_days_30d > 0
                 THEN GREATEST(0, eligible_days_30d - days_in_stock_30d)
            END AS stockout_days_30d,
            -- Absent days count as zero stock (the old activity-only average was
            -- biased toward in-stock days)
            CASE WHEN eligible_days_30d > 0
                 THEN sum_qty::numeric / eligible_days_30d
            END AS avg_stock_units_30d,
            CASE WHEN eligible_days_30d > 0
                 THEN sum_value::numeric / eligible_days_30d
            END AS avg_stock_cost_30d
        FROM (
            SELECT
                p.pid,
                LEAST(
                    cal.covered_days,
                    CASE WHEN p.created_at IS NULL THEN cal.covered_days
                         ELSE GREATEST(0, (_current_date - GREATEST(p.created_at::date, _current_date - 29) + 1))
                    END
                ) AS eligible_days_30d,
                COALESCE(pres.days_in_stock, 0) AS days_in_stock_30d,
                COALESCE(pres.sum_qty, 0) AS sum_qty,
                COALESCE(pres.sum_value, 0) AS sum_value
            FROM public.products p
            CROSS JOIN (
                SELECT COUNT(DISTINCT snapshot_date) AS covered_days
                FROM public.stock_snapshots
                WHERE snapshot_date >= _current_date - INTERVAL '29 days'
                  AND snapshot_date <= _current_date
            ) cal
            LEFT JOIN (
                SELECT pid,
                       COUNT(*) AS days_in_stock,
                       SUM(stock_quantity) AS sum_qty,
                       SUM(stock_value) AS sum_value
                FROM public.stock_snapshots
                WHERE snapshot_date >= _current_date - INTERVAL '29 days'
                  AND snapshot_date <= _current_date
                GROUP BY pid
            ) pres ON pres.pid = p.pid
        ) base
    ),
    -- Sales that happened on out-of-stock days (per the stock snapshot), for
    -- lost-sales incidents and the fill-rate heuristic. Restricted to days the
    -- stock cron actually ran so e.g. today's sales aren't misread as stockouts.
    SalesDayStock AS (
        SELECT
            dps.pid,
            SUM(dps.units_sold) AS units_sold_covered,
            COUNT(*) FILTER (WHERE dps.units_sold > 0 AND ss.pid IS NULL) AS lost_sales_incidents_30d,
            SUM(CASE WHEN ss.pid IS NULL THEN dps.units_sold ELSE 0 END) AS units_sold_on_stockout_days
        FROM public.daily_product_snapshots dps
        JOIN (
            SELECT DISTINCT snapshot_date FROM public.stock_snapshots
            WHERE snapshot_date >= _current_date - INTERVAL '29 days'
              AND snapshot_date <= _current_date
        ) cal ON cal.snapshot_date = dps.snapshot_date
        LEFT JOIN public.stock_snapshots ss
            ON ss.pid = dps.pid AND ss.snapshot_date = dps.snapshot_date
        WHERE dps.snapshot_date >= _current_date - INTERVAL '29 days'
          AND dps.snapshot_date <= _current_date
        GROUP BY dps.pid
    ),
    PreviousPeriodMetrics AS (
        -- Calculate metrics for previous 30-day period for growth comparison
        SELECT
            pid,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '59 days' 
                     AND snapshot_date < _current_date - INTERVAL '29 days' 
                THEN units_sold ELSE 0 END) AS sales_prev_30d,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '59 days' 
                     AND snapshot_date < _current_date - INTERVAL '29 days' 
                THEN net_revenue ELSE 0 END) AS revenue_prev_30d,
            -- Year-over-year comparison
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '395 days' 
                     AND snapshot_date < _current_date - INTERVAL '365 days' 
                THEN units_sold ELSE 0 END) AS sales_30d_last_year,
            SUM(CASE WHEN snapshot_date >= _current_date - INTERVAL '395 days' 
                     AND snapshot_date < _current_date - INTERVAL '365 days' 
                THEN net_revenue ELSE 0 END) AS revenue_30d_last_year
        FROM public.daily_product_snapshots
        GROUP BY pid
    ),
    DemandVariability AS (
        -- Calculate variance and standard deviation of daily sales over the full 30-day window
        -- including zero-sales days (not just activity days) for accurate variability metrics.
        -- Uses algebraic equivalents to avoid expensive CROSS JOIN with generate_series.
        -- For N=30 total days, k active days, sum S, sum_sq SS:
        --   mean = S/N, variance = (SS/N) - (S/N)^2 (population variance over all N days)
        SELECT
            pid,
            COUNT(*) AS days_with_data,
            SUM(units_sold)::numeric / 30.0 AS avg_daily_sales,
            CASE WHEN SUM(units_sold) > 0 THEN
                (SUM(units_sold::numeric * units_sold::numeric) / 30.0)
                - (SUM(units_sold)::numeric / 30.0) * (SUM(units_sold)::numeric / 30.0)
            END AS sales_variance,
            CASE WHEN SUM(units_sold) > 0 THEN
                (|/ GREATEST(0,
                    (SUM(units_sold::numeric * units_sold::numeric) / 30.0)
                    - (SUM(units_sold)::numeric / 30.0) * (SUM(units_sold)::numeric / 30.0)
                ))::numeric
            END AS sales_std_dev,
            CASE WHEN SUM(units_sold) > 0 THEN
                ((|/ GREATEST(0,
                    (SUM(units_sold::numeric * units_sold::numeric) / 30.0)
                    - (SUM(units_sold)::numeric / 30.0) * (SUM(units_sold)::numeric / 30.0)
                )) / (SUM(units_sold)::numeric / 30.0))::numeric
            END AS sales_cv
        FROM public.daily_product_snapshots
        WHERE snapshot_date >= _current_date - INTERVAL '29 days'
          AND snapshot_date <= _current_date
        GROUP BY pid
    ),
    ServiceLevels AS (
        -- Service level and fill rate built on full-coverage stock data
        -- (StockCoverage / SalesDayStock) instead of activity-only snapshots.
        SELECT
            sc.pid,
            sc.stockout_days_30d AS stockout_incidents_30d,
            sds.lost_sales_incidents_30d,
            -- Service level: percentage of covered days the product was in stock
            CASE WHEN sc.eligible_days_30d > 0 THEN
                (1.0 - (sc.stockout_days_30d::NUMERIC / sc.eligible_days_30d)) * 100
            END AS service_level_30d,
            -- Fill rate: units sold / (units sold + potential lost sales).
            -- The 0.2 lost-sales factor is an arbitrary heuristic: each unit sold on
            -- an out-of-stock day is assumed to represent 20% additional missed demand.
            CASE
                WHEN COALESCE(sds.units_sold_covered, 0) > 0 THEN
                    (sds.units_sold_covered::NUMERIC /
                     (sds.units_sold_covered + COALESCE(sds.units_sold_on_stockout_days, 0) * 0.2)) * 100
                ELSE NULL
            END AS fill_rate_30d
        FROM StockCoverage sc
        LEFT JOIN SalesDayStock sds ON sds.pid = sc.pid
    ),
    ProductVelocity AS (
        -- Single source for sales velocity so every replenishment/cover column stays
        -- consistent. NULL when the product is excluded from forecasting: excluded
        -- products now still get a product_metrics row (they used to be filtered out
        -- entirely and vanished from brand/vendor/category rollups), but their
        -- forecast-derived columns go NULL / zero.
        SELECT
            ci.pid,
            CASE WHEN COALESCE(s.exclude_forecast, FALSE) THEN NULL
                 ELSE calculate_sales_velocity(sa.sales_30d::int, COALESCE(sc.stockout_days_30d, 0)::int)
            END AS daily
        FROM CurrentInfo ci
        LEFT JOIN SnapshotAggregates sa ON ci.pid = sa.pid
        LEFT JOIN StockCoverage sc ON ci.pid = sc.pid
        LEFT JOIN Settings s ON ci.pid = s.pid
    ),
    SeasonalityAnalysis AS (
        -- Set-based seasonality detection (replaces per-product function calls)
        -- Computes monthly CV and peak-to-average ratio across the last 12 months
        SELECT
            pid,
            CASE
                WHEN monthly_cv > 0.5 AND seasonality_index > 150 THEN 'strong'
                WHEN monthly_cv > 0.3 AND seasonality_index > 120 THEN 'moderate'
                ELSE 'none'
            END::varchar AS seasonal_pattern,
            CASE
                WHEN monthly_cv > 0.3 AND seasonality_index > 120 THEN seasonality_index
                ELSE 100::numeric
            END AS seasonality_index,
            CASE
                WHEN monthly_cv > 0.3 AND seasonality_index > 120
                THEN TRIM(TO_CHAR(TO_DATE(peak_month::text, 'MM'), 'Month'))
                ELSE NULL
            END::varchar AS peak_season
        FROM (
            SELECT
                pid,
                CASE WHEN overall_avg > 0 AND monthly_stddev IS NOT NULL
                     THEN monthly_stddev / overall_avg ELSE 0 END AS monthly_cv,
                CASE WHEN overall_avg > 0
                     THEN ROUND((max_month_avg / overall_avg * 100)::numeric, 2)
                     ELSE 100 END AS seasonality_index,
                peak_month
            FROM (
                SELECT
                    ms.pid,
                    AVG(ms.month_avg) AS overall_avg,
                    STDDEV(ms.month_avg) AS monthly_stddev,
                    MAX(ms.month_avg) AS max_month_avg,
                    (ARRAY_AGG(ms.mo ORDER BY ms.month_avg DESC))[1] AS peak_month
                FROM (
                    SELECT pid, EXTRACT(MONTH FROM snapshot_date)::int AS mo, AVG(units_sold) AS month_avg
                    FROM daily_product_snapshots
                    WHERE snapshot_date >= CURRENT_DATE - INTERVAL '365 days'
                      AND units_sold > 0
                    GROUP BY pid, EXTRACT(MONTH FROM snapshot_date)
                ) ms
                GROUP BY ms.pid
                HAVING COUNT(*) >= 3  -- Need at least 3 months for meaningful seasonality
            ) agg
        ) classified
    )
    -- Final UPSERT into product_metrics
    INSERT INTO public.product_metrics (
        pid, last_calculated, sku, title, brand, vendor, image_url, is_visible, is_replenishable,
        barcode, harmonized_tariff_code, vendor_reference, notions_reference, line, subline, artist, 
        moq, rating, reviews, weight, length, width, height, country_of_origin, location, 
        baskets, notifies, preorder_count, notions_inv_count,
        current_price, current_regular_price, current_cost_price,
        current_stock, current_stock_cost, current_stock_retail, current_stock_gross,
        on_order_qty, on_order_cost, on_order_retail, earliest_expected_date,
        date_created, date_first_received, date_last_received, date_first_sold, date_last_sold, age_days,
        sales_7d, revenue_7d, sales_14d, revenue_14d, sales_30d, revenue_30d, cogs_30d, profit_30d,
        returns_units_30d, returns_revenue_30d, discounts_30d, gross_revenue_30d, gross_regular_revenue_30d,
        stockout_days_30d, sales_365d, revenue_365d,
        avg_stock_units_30d, avg_stock_cost_30d, avg_stock_retail_30d, avg_stock_gross_30d,
        received_qty_30d, received_cost_30d,
        lifetime_sales, lifetime_revenue, lifetime_revenue_quality,
        first_7_days_sales, first_7_days_revenue, first_30_days_sales, first_30_days_revenue,
        first_60_days_sales, first_60_days_revenue, first_90_days_sales, first_90_days_revenue,
        asp_30d, acp_30d, avg_ros_30d, avg_sales_per_day_30d, avg_sales_per_month_30d,
        margin_30d, markup_30d, gmroi_30d, stockturn_30d, return_rate_30d, discount_rate_30d,
        stockout_rate_30d, markdown_30d, markdown_rate_30d, sell_through_30d,
        -- avg_lead_time_days, -- Calculated periodically
        -- abc_class, -- Calculated periodically
        sales_velocity_daily, config_lead_time, config_days_of_stock, config_safety_stock,
        planning_period_days, lead_time_forecast_units, days_of_stock_forecast_units,
        planning_period_forecast_units, lead_time_closing_stock, days_of_stock_closing_stock,
        replenishment_needed_raw, replenishment_units, replenishment_cost, replenishment_retail, replenishment_profit,
        to_order_units, forecast_lost_sales_units, forecast_lost_revenue,
        stock_cover_in_days, po_cover_in_days, sells_out_in_days, replenish_date,
        overstocked_units, overstocked_cost, overstocked_retail, is_old_stock,
        yesterday_sales,
        status,  -- Add status field for calculated status
        -- New fields
        sales_growth_30d_vs_prev, revenue_growth_30d_vs_prev,
        sales_growth_yoy, revenue_growth_yoy,
        sales_variance_30d, sales_std_dev_30d, sales_cv_30d, demand_pattern,
        fill_rate_30d, stockout_incidents_30d, service_level_30d, lost_sales_incidents_30d,
        seasonality_index, seasonal_pattern, peak_season
    )
    SELECT
        ci.pid, _start_time, ci.sku, ci.title, ci.brand, ci.vendor, ci.image_url, ci.is_visible, ci.is_replenishable,
        ci.barcode, ci.harmonized_tariff_code, ci.vendor_reference, ci.notions_reference, ci.line, ci.subline, ci.artist,
        ci.moq, ci.rating, ci.reviews, ci.weight, ci.length, ci.width, ci.height, ci.country_of_origin, ci.location,
        ci.baskets, ci.notifies, ci.preorder_count, ci.notions_inv_count,
        ci.current_price, ci.current_regular_price, ci.current_cost_price,
        ci.current_stock, ci.current_stock * ci.current_effective_cost, ci.current_stock * ci.current_price, ci.current_stock * ci.current_regular_price,
        COALESCE(ooi.on_order_qty, 0), COALESCE(ooi.on_order_cost, 0.00), COALESCE(ooi.on_order_qty, 0) * ci.current_price, ooi.earliest_expected_date,
        ci.created_at::date, COALESCE(ci.first_received::date, hd.date_first_received_calc), hd.date_last_received_calc, hd.date_first_sold, COALESCE(ci.date_last_sold, hd.max_order_date), 
        CASE 
            WHEN ci.created_at IS NULL AND hd.date_first_sold IS NULL THEN 0
            WHEN ci.created_at IS NULL THEN (_current_date - hd.date_first_sold)::integer
            WHEN hd.date_first_sold IS NULL THEN (_current_date - ci.created_at::date)::integer
            ELSE (_current_date - LEAST(ci.created_at::date, hd.date_first_sold))::integer
        END AS age_days,
        sa.sales_7d, sa.revenue_7d, sa.sales_14d, sa.revenue_14d, sa.sales_30d, sa.revenue_30d, sa.cogs_30d, sa.profit_30d,
        sa.returns_units_30d, sa.returns_revenue_30d, sa.discounts_30d, sa.gross_revenue_30d, sa.gross_regular_revenue_30d,
        sc.stockout_days_30d, sa.sales_365d, sa.revenue_365d,
        sc.avg_stock_units_30d, sc.avg_stock_cost_30d, sa.avg_stock_retail_30d, sa.avg_stock_gross_30d,
        sa.received_qty_30d, sa.received_cost_30d,
        -- Use total_sold from products table as the source of truth for lifetime sales
        -- This includes all historical data from the production database
        ci.historical_total_sold AS lifetime_sales,
        -- Calculate lifetime revenue using actual historical prices where available
        CASE 
            WHEN lr.lifetime_revenue_from_orders IS NOT NULL THEN
                -- We have some order history - use it plus estimate for remaining
                lr.lifetime_revenue_from_orders + 
                (GREATEST(0, ci.historical_total_sold - COALESCE(lr.lifetime_units_from_orders, 0)) * 
                 COALESCE(
                    -- Use oldest known price from snapshots as proxy
                    (SELECT net_revenue / NULLIF(units_sold, 0)
                     FROM daily_product_snapshots
                     WHERE pid = ci.pid AND units_sold > 0
                     ORDER BY snapshot_date ASC
                     LIMIT 1),
                    ci.current_price
                 ))
            ELSE
                -- No order history - estimate using current price
                ci.historical_total_sold * ci.current_price
        END AS lifetime_revenue,
        CASE 
            WHEN lr.lifetime_units_from_orders >= ci.historical_total_sold * 0.9 THEN 'exact'
            WHEN lr.lifetime_units_from_orders >= ci.historical_total_sold * 0.5 THEN 'partial'
            ELSE 'estimated'
        END AS lifetime_revenue_quality,
        fpm.first_7_days_sales, fpm.first_7_days_revenue, fpm.first_30_days_sales, fpm.first_30_days_revenue,
        fpm.first_60_days_sales, fpm.first_60_days_revenue, fpm.first_90_days_sales, fpm.first_90_days_revenue,
        sa.revenue_30d / NULLIF(sa.sales_30d, 0) AS asp_30d,
        sa.cogs_30d / NULLIF(sa.sales_30d, 0) AS acp_30d,
        sa.profit_30d / NULLIF(sa.sales_30d, 0) AS avg_ros_30d,
        sa.sales_30d / 30.0 AS avg_sales_per_day_30d,
        sa.sales_30d AS avg_sales_per_month_30d, -- Using 30d sales as proxy for month
        (sa.profit_30d / NULLIF(sa.revenue_30d, 0)) * 100 AS margin_30d,
        (sa.profit_30d / NULLIF(sa.cogs_30d, 0)) * 100 AS markup_30d,
        -- Annualized GMROI (30-day profit extrapolated to a year: × 365/30).
        -- Conventional benchmark for healthy retail is ≥ 2-3 on this scale.
        (sa.profit_30d / NULLIF(sc.avg_stock_cost_30d, 0)) * 12.17 AS gmroi_30d,
        sa.sales_30d / NULLIF(sc.avg_stock_units_30d, 0) AS stockturn_30d,
        -- Industry-standard definition: returns / sales (not returns / (sales+returns))
        (sa.returns_units_30d / NULLIF(sa.sales_30d, 0)) * 100 AS return_rate_30d,
        (sa.discounts_30d / NULLIF(sa.gross_revenue_30d, 0)) * 100 AS discount_rate_30d,
        (sc.stockout_days_30d::numeric / NULLIF(sc.eligible_days_30d, 0)) * 100 AS stockout_rate_30d,
        sa.gross_regular_revenue_30d - sa.gross_revenue_30d AS markdown_30d,
        ((sa.gross_regular_revenue_30d - sa.gross_revenue_30d) / NULLIF(sa.gross_regular_revenue_30d, 0)) * 100 AS markdown_rate_30d,
        -- Sell-through rate: Industry standard is Units Sold / (Beginning Inventory + Units Received)
        -- Uses actual snapshot from 30 days ago as beginning stock, falls back to avg_stock_units_30d
        (sa.sales_30d / NULLIF(
            COALESCE(bs.beginning_stock_30d, sc.avg_stock_units_30d::int, 0) + sa.received_qty_30d,
            0
        )) * 100 AS sell_through_30d,

        -- Forecasting intermediate values (ProductVelocity; NULL when excluded from forecast)
        vel.daily AS sales_velocity_daily,
        s.effective_lead_time AS config_lead_time,
        s.effective_days_of_stock AS config_days_of_stock,
        s.effective_safety_stock AS config_safety_stock,
        (s.effective_lead_time + s.effective_days_of_stock) AS planning_period_days,
        
        vel.daily * s.effective_lead_time AS lead_time_forecast_units,
        
        vel.daily * s.effective_days_of_stock AS days_of_stock_forecast_units,
        
        vel.daily * (s.effective_lead_time + s.effective_days_of_stock) AS planning_period_forecast_units,
        
        (ci.current_stock + COALESCE(ooi.on_order_qty, 0) - (vel.daily * s.effective_lead_time)) AS lead_time_closing_stock,
        
        ((ci.current_stock + COALESCE(ooi.on_order_qty, 0) - (vel.daily * s.effective_lead_time))) - (vel.daily * s.effective_days_of_stock) AS days_of_stock_closing_stock,
        
        ((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0) AS replenishment_needed_raw,

        -- Final Forecasting / Replenishment Metrics
        CEILING(GREATEST(0, (((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0))))::int AS replenishment_units,
        (CEILING(GREATEST(0, (((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0))))::int) * ci.current_effective_cost AS replenishment_cost,
        (CEILING(GREATEST(0, (((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0))))::int) * ci.current_price AS replenishment_retail,
        (CEILING(GREATEST(0, (((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0))))::int) * (ci.current_price - ci.current_effective_cost) AS replenishment_profit,

        -- To Order (Apply MOQ/UOM logic here if needed, otherwise equals replenishment)
        CEILING(GREATEST(0, (((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)) + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0))))::int AS to_order_units,

        GREATEST(0, - (ci.current_stock + COALESCE(ooi.on_order_qty, 0) - (vel.daily * s.effective_lead_time))) AS forecast_lost_sales_units,
        GREATEST(0, - (ci.current_stock + COALESCE(ooi.on_order_qty, 0) - (vel.daily * s.effective_lead_time))) * ci.current_price AS forecast_lost_revenue,

        ci.current_stock / NULLIF(vel.daily, 0) AS stock_cover_in_days,
        COALESCE(ooi.on_order_qty, 0) / NULLIF(vel.daily, 0) AS po_cover_in_days,
        (ci.current_stock + COALESCE(ooi.on_order_qty, 0)) / NULLIF(vel.daily, 0) AS sells_out_in_days,

        -- Replenish Date: Date when stock is projected to hit safety stock, minus lead time
        CASE
            WHEN vel.daily > 0
            THEN _current_date + FLOOR(GREATEST(0, ci.current_stock - s.effective_safety_stock) / vel.daily)::int - s.effective_lead_time
            ELSE NULL
        END AS replenish_date,

        GREATEST(0, ci.current_stock - s.effective_safety_stock - ((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)))::int AS overstocked_units,
        (GREATEST(0, ci.current_stock - s.effective_safety_stock - ((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)))) * ci.current_effective_cost AS overstocked_cost,
        (GREATEST(0, ci.current_stock - s.effective_safety_stock - ((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock)))) * ci.current_price AS overstocked_retail,

        -- Old Stock Flag
        (ci.created_at::date < _current_date - INTERVAL '60 day') AND
        (COALESCE(ci.date_last_sold, hd.max_order_date) IS NULL OR COALESCE(ci.date_last_sold, hd.max_order_date) < _current_date - INTERVAL '60 day') AND
        (hd.date_last_received_calc IS NULL OR hd.date_last_received_calc < _current_date - INTERVAL '60 day') AND
        COALESCE(ooi.on_order_qty, 0) = 0
        AS is_old_stock,

        sa.yesterday_sales,

        -- Calculate status using direct CASE statements (inline logic)
        CASE
            -- Non-replenishable items default to Healthy
            WHEN NOT ci.is_replenishable THEN 'Healthy'
            
            -- Calculate lead time and thresholds
            ELSE
                CASE
                    -- Check for overstock first 
                    WHEN GREATEST(0, ci.current_stock - s.effective_safety_stock - ((vel.daily * s.effective_lead_time) + (vel.daily * s.effective_days_of_stock))) > 0 THEN 'Overstock'
                    
                    -- Check for Critical stock
                    WHEN ci.current_stock <= 0 OR 
                         (ci.current_stock / NULLIF(vel.daily, 0)) <= 0 THEN 'Critical'
                    
                    WHEN (ci.current_stock / NULLIF(vel.daily, 0)) < (COALESCE(s.effective_lead_time, 30) * 0.5) THEN 'Critical'
                    
                    -- Check for reorder soon
                    WHEN ((ci.current_stock + COALESCE(ooi.on_order_qty, 0)) / NULLIF(vel.daily, 0)) < (COALESCE(s.effective_lead_time, 30) + 7) THEN 
                         CASE
                           WHEN (ci.current_stock / NULLIF(vel.daily, 0)) < (COALESCE(s.effective_lead_time, 30) * 0.5) THEN 'Critical'
                           ELSE 'Reorder Soon'
                         END
                    
                    -- Check for 'At Risk' - old stock
                    WHEN (ci.created_at::date < _current_date - INTERVAL '60 day') AND
                         (COALESCE(ci.date_last_sold, hd.max_order_date) IS NULL OR COALESCE(ci.date_last_sold, hd.max_order_date) < _current_date - INTERVAL '60 day') AND
                         (hd.date_last_received_calc IS NULL OR hd.date_last_received_calc < _current_date - INTERVAL '60 day') AND
                         COALESCE(ooi.on_order_qty, 0) = 0 THEN 'At Risk'
                         
                    -- Check for 'At Risk' - hasn't sold in a long time
                    WHEN COALESCE(ci.date_last_sold, hd.max_order_date) IS NOT NULL 
                      AND COALESCE(ci.date_last_sold, hd.max_order_date) < (_current_date - INTERVAL '90 days')
                      AND (CASE 
                            WHEN ci.created_at IS NULL AND hd.date_first_sold IS NULL THEN 0
                            WHEN ci.created_at IS NULL THEN (_current_date - hd.date_first_sold)::integer
                            WHEN hd.date_first_sold IS NULL THEN (_current_date - ci.created_at::date)::integer
                            ELSE (_current_date - LEAST(ci.created_at::date, hd.date_first_sold))::integer
                          END) > 180 THEN 'At Risk'
                          
                    -- Very high stock cover is at risk too
                    WHEN (ci.current_stock / NULLIF(vel.daily, 0)) > 365 THEN 'At Risk'
                         
                    -- New products (less than 30 days old)
                    WHEN (CASE 
                           WHEN ci.created_at IS NULL AND hd.date_first_sold IS NULL THEN 0
                           WHEN ci.created_at IS NULL THEN (_current_date - hd.date_first_sold)::integer
                           WHEN hd.date_first_sold IS NULL THEN (_current_date - ci.created_at::date)::integer
                           ELSE (_current_date - LEAST(ci.created_at::date, hd.date_first_sold))::integer
                         END) <= 30 THEN 'New'
                         
                    -- If none of the above, assume Healthy
                    ELSE 'Healthy'
                END
        END AS status,

        -- Growth Metrics (P3) - using safe_divide and std_numeric for consistency
        std_numeric(safe_divide((sa.sales_30d - ppm.sales_prev_30d) * 100.0, ppm.sales_prev_30d), 2) AS sales_growth_30d_vs_prev,
        std_numeric(safe_divide((sa.revenue_30d - ppm.revenue_prev_30d) * 100.0, ppm.revenue_prev_30d), 2) AS revenue_growth_30d_vs_prev,
        std_numeric(safe_divide((sa.sales_30d - ppm.sales_30d_last_year) * 100.0, ppm.sales_30d_last_year), 2) AS sales_growth_yoy,
        std_numeric(safe_divide((sa.revenue_30d - ppm.revenue_30d_last_year) * 100.0, ppm.revenue_30d_last_year), 2) AS revenue_growth_yoy,

        -- Demand Variability (P3)
        std_numeric(dv.sales_variance, 2) AS sales_variance_30d,
        std_numeric(dv.sales_std_dev, 2) AS sales_std_dev_30d,
        std_numeric(dv.sales_cv, 2) AS sales_cv_30d,
        classify_demand_pattern(dv.avg_daily_sales, dv.sales_cv) AS demand_pattern,

        -- Service Levels (P5)
        std_numeric(COALESCE(sl.fill_rate_30d, 100), 2) AS fill_rate_30d,
        COALESCE(sl.stockout_incidents_30d, 0)::int AS stockout_incidents_30d,
        std_numeric(COALESCE(sl.service_level_30d, 100), 2) AS service_level_30d,
        COALESCE(sl.lost_sales_incidents_30d, 0)::int AS lost_sales_incidents_30d,

        -- Seasonality (P5)
        std_numeric(season.seasonality_index, 2) AS seasonality_index,
        COALESCE(season.seasonal_pattern, 'none') AS seasonal_pattern,
        season.peak_season

    FROM CurrentInfo ci
    LEFT JOIN OnOrderInfo ooi ON ci.pid = ooi.pid
    LEFT JOIN HistoricalDates hd ON ci.pid = hd.pid
    LEFT JOIN SnapshotAggregates sa ON ci.pid = sa.pid
    LEFT JOIN FirstPeriodMetrics fpm ON ci.pid = fpm.pid
    LEFT JOIN Settings s ON ci.pid = s.pid
    LEFT JOIN LifetimeRevenue lr ON ci.pid = lr.pid
    LEFT JOIN PreviousPeriodMetrics ppm ON ci.pid = ppm.pid
    LEFT JOIN DemandVariability dv ON ci.pid = dv.pid
    LEFT JOIN ServiceLevels sl ON ci.pid = sl.pid
    LEFT JOIN BeginningStock bs ON ci.pid = bs.pid
    LEFT JOIN SeasonalityAnalysis season ON ci.pid = season.pid
    LEFT JOIN StockCoverage sc ON ci.pid = sc.pid
    LEFT JOIN ProductVelocity vel ON ci.pid = vel.pid
    -- NOTE: products with exclude_from_forecast still get a metrics row (so they
    -- appear in brand/vendor/category rollups); only their forecast-derived
    -- columns are NULLed via ProductVelocity.

    ON CONFLICT (pid) DO UPDATE SET
        last_calculated = EXCLUDED.last_calculated,
        sku = EXCLUDED.sku, title = EXCLUDED.title, brand = EXCLUDED.brand, vendor = EXCLUDED.vendor, image_url = EXCLUDED.image_url, is_visible = EXCLUDED.is_visible, is_replenishable = EXCLUDED.is_replenishable,
        barcode = EXCLUDED.barcode, harmonized_tariff_code = EXCLUDED.harmonized_tariff_code, vendor_reference = EXCLUDED.vendor_reference, notions_reference = EXCLUDED.notions_reference, line = EXCLUDED.line, subline = EXCLUDED.subline, artist = EXCLUDED.artist,
        moq = EXCLUDED.moq, rating = EXCLUDED.rating, reviews = EXCLUDED.reviews, weight = EXCLUDED.weight, length = EXCLUDED.length, width = EXCLUDED.width, height = EXCLUDED.height, country_of_origin = EXCLUDED.country_of_origin, location = EXCLUDED.location,
        baskets = EXCLUDED.baskets, notifies = EXCLUDED.notifies, preorder_count = EXCLUDED.preorder_count, notions_inv_count = EXCLUDED.notions_inv_count,
        current_price = EXCLUDED.current_price, current_regular_price = EXCLUDED.current_regular_price, current_cost_price = EXCLUDED.current_cost_price,
        current_stock = EXCLUDED.current_stock, current_stock_cost = EXCLUDED.current_stock_cost, current_stock_retail = EXCLUDED.current_stock_retail, current_stock_gross = EXCLUDED.current_stock_gross,
        on_order_qty = EXCLUDED.on_order_qty, on_order_cost = EXCLUDED.on_order_cost, on_order_retail = EXCLUDED.on_order_retail, earliest_expected_date = EXCLUDED.earliest_expected_date,
        date_created = EXCLUDED.date_created, date_first_received = EXCLUDED.date_first_received, date_last_received = EXCLUDED.date_last_received, date_first_sold = EXCLUDED.date_first_sold, date_last_sold = EXCLUDED.date_last_sold, age_days = EXCLUDED.age_days,
        sales_7d = EXCLUDED.sales_7d, revenue_7d = EXCLUDED.revenue_7d, sales_14d = EXCLUDED.sales_14d, revenue_14d = EXCLUDED.revenue_14d, sales_30d = EXCLUDED.sales_30d, revenue_30d = EXCLUDED.revenue_30d, cogs_30d = EXCLUDED.cogs_30d, profit_30d = EXCLUDED.profit_30d,
        returns_units_30d = EXCLUDED.returns_units_30d, returns_revenue_30d = EXCLUDED.returns_revenue_30d, discounts_30d = EXCLUDED.discounts_30d, gross_revenue_30d = EXCLUDED.gross_revenue_30d, gross_regular_revenue_30d = EXCLUDED.gross_regular_revenue_30d,
        stockout_days_30d = EXCLUDED.stockout_days_30d, sales_365d = EXCLUDED.sales_365d, revenue_365d = EXCLUDED.revenue_365d,
        avg_stock_units_30d = EXCLUDED.avg_stock_units_30d, avg_stock_cost_30d = EXCLUDED.avg_stock_cost_30d, avg_stock_retail_30d = EXCLUDED.avg_stock_retail_30d, avg_stock_gross_30d = EXCLUDED.avg_stock_gross_30d,
        received_qty_30d = EXCLUDED.received_qty_30d, received_cost_30d = EXCLUDED.received_cost_30d,
        lifetime_sales = EXCLUDED.lifetime_sales, lifetime_revenue = EXCLUDED.lifetime_revenue, lifetime_revenue_quality = EXCLUDED.lifetime_revenue_quality,
        first_7_days_sales = EXCLUDED.first_7_days_sales, first_7_days_revenue = EXCLUDED.first_7_days_revenue, first_30_days_sales = EXCLUDED.first_30_days_sales, first_30_days_revenue = EXCLUDED.first_30_days_revenue,
        first_60_days_sales = EXCLUDED.first_60_days_sales, first_60_days_revenue = EXCLUDED.first_60_days_revenue, first_90_days_sales = EXCLUDED.first_90_days_sales, first_90_days_revenue = EXCLUDED.first_90_days_revenue,
        asp_30d = EXCLUDED.asp_30d, acp_30d = EXCLUDED.acp_30d, avg_ros_30d = EXCLUDED.avg_ros_30d, avg_sales_per_day_30d = EXCLUDED.avg_sales_per_day_30d, avg_sales_per_month_30d = EXCLUDED.avg_sales_per_month_30d,
        margin_30d = EXCLUDED.margin_30d, markup_30d = EXCLUDED.markup_30d, gmroi_30d = EXCLUDED.gmroi_30d, stockturn_30d = EXCLUDED.stockturn_30d, return_rate_30d = EXCLUDED.return_rate_30d, discount_rate_30d = EXCLUDED.discount_rate_30d,
        stockout_rate_30d = EXCLUDED.stockout_rate_30d, markdown_30d = EXCLUDED.markdown_30d, markdown_rate_30d = EXCLUDED.markdown_rate_30d, sell_through_30d = EXCLUDED.sell_through_30d,
        -- avg_lead_time_days = EXCLUDED.avg_lead_time_days, -- Updated Periodically
        -- abc_class = EXCLUDED.abc_class, -- Updated Periodically
        sales_velocity_daily = EXCLUDED.sales_velocity_daily, config_lead_time = EXCLUDED.config_lead_time, config_days_of_stock = EXCLUDED.config_days_of_stock, config_safety_stock = EXCLUDED.config_safety_stock,
        planning_period_days = EXCLUDED.planning_period_days, lead_time_forecast_units = EXCLUDED.lead_time_forecast_units, days_of_stock_forecast_units = EXCLUDED.days_of_stock_forecast_units,
        planning_period_forecast_units = EXCLUDED.planning_period_forecast_units, lead_time_closing_stock = EXCLUDED.lead_time_closing_stock, days_of_stock_closing_stock = EXCLUDED.days_of_stock_closing_stock,
        replenishment_needed_raw = EXCLUDED.replenishment_needed_raw, replenishment_units = EXCLUDED.replenishment_units, replenishment_cost = EXCLUDED.replenishment_cost, replenishment_retail = EXCLUDED.replenishment_retail, replenishment_profit = EXCLUDED.replenishment_profit,
        to_order_units = EXCLUDED.to_order_units, forecast_lost_sales_units = EXCLUDED.forecast_lost_sales_units, forecast_lost_revenue = EXCLUDED.forecast_lost_revenue,
        stock_cover_in_days = EXCLUDED.stock_cover_in_days, po_cover_in_days = EXCLUDED.po_cover_in_days, sells_out_in_days = EXCLUDED.sells_out_in_days, replenish_date = EXCLUDED.replenish_date,
        overstocked_units = EXCLUDED.overstocked_units, overstocked_cost = EXCLUDED.overstocked_cost, overstocked_retail = EXCLUDED.overstocked_retail, is_old_stock = EXCLUDED.is_old_stock,
        yesterday_sales = EXCLUDED.yesterday_sales,
        status = EXCLUDED.status,
        sales_growth_30d_vs_prev = EXCLUDED.sales_growth_30d_vs_prev,
        revenue_growth_30d_vs_prev = EXCLUDED.revenue_growth_30d_vs_prev,
        sales_growth_yoy = EXCLUDED.sales_growth_yoy,
        revenue_growth_yoy = EXCLUDED.revenue_growth_yoy,
        sales_variance_30d = EXCLUDED.sales_variance_30d,
        sales_std_dev_30d = EXCLUDED.sales_std_dev_30d,
        sales_cv_30d = EXCLUDED.sales_cv_30d,
        demand_pattern = EXCLUDED.demand_pattern,
        fill_rate_30d = EXCLUDED.fill_rate_30d,
        stockout_incidents_30d = EXCLUDED.stockout_incidents_30d,
        service_level_30d = EXCLUDED.service_level_30d,
        lost_sales_incidents_30d = EXCLUDED.lost_sales_incidents_30d,
        seasonality_index = EXCLUDED.seasonality_index,
        seasonal_pattern = EXCLUDED.seasonal_pattern,
        peak_season = EXCLUDED.peak_season
        WHERE -- Only update if at least one key metric has changed
            product_metrics.current_stock IS DISTINCT FROM EXCLUDED.current_stock OR
            product_metrics.current_price IS DISTINCT FROM EXCLUDED.current_price OR
            product_metrics.current_cost_price IS DISTINCT FROM EXCLUDED.current_cost_price OR
            product_metrics.on_order_qty IS DISTINCT FROM EXCLUDED.on_order_qty OR
            product_metrics.sales_7d IS DISTINCT FROM EXCLUDED.sales_7d OR
            product_metrics.sales_30d IS DISTINCT FROM EXCLUDED.sales_30d OR
            product_metrics.revenue_30d IS DISTINCT FROM EXCLUDED.revenue_30d OR
            product_metrics.status IS DISTINCT FROM EXCLUDED.status OR
            product_metrics.replenishment_units IS DISTINCT FROM EXCLUDED.replenishment_units OR
            product_metrics.stock_cover_in_days IS DISTINCT FROM EXCLUDED.stock_cover_in_days OR
            product_metrics.yesterday_sales IS DISTINCT FROM EXCLUDED.yesterday_sales OR
            product_metrics.date_last_sold IS DISTINCT FROM EXCLUDED.date_last_sold OR
            product_metrics.earliest_expected_date IS DISTINCT FROM EXCLUDED.earliest_expected_date OR
            product_metrics.lifetime_sales IS DISTINCT FROM EXCLUDED.lifetime_sales OR
            product_metrics.lifetime_revenue_quality IS DISTINCT FROM EXCLUDED.lifetime_revenue_quality OR
            -- Derived metrics that can change even when source fields don't
            product_metrics.profit_30d IS DISTINCT FROM EXCLUDED.profit_30d OR
            product_metrics.cogs_30d IS DISTINCT FROM EXCLUDED.cogs_30d OR
            product_metrics.margin_30d IS DISTINCT FROM EXCLUDED.margin_30d OR
            product_metrics.stockout_days_30d IS DISTINCT FROM EXCLUDED.stockout_days_30d OR
            product_metrics.sell_through_30d IS DISTINCT FROM EXCLUDED.sell_through_30d OR
            -- Growth and variability metrics
            product_metrics.sales_growth_30d_vs_prev IS DISTINCT FROM EXCLUDED.sales_growth_30d_vs_prev OR
            product_metrics.revenue_growth_30d_vs_prev IS DISTINCT FROM EXCLUDED.revenue_growth_30d_vs_prev OR
            product_metrics.demand_pattern IS DISTINCT FROM EXCLUDED.demand_pattern OR
            product_metrics.seasonal_pattern IS DISTINCT FROM EXCLUDED.seasonal_pattern OR
            product_metrics.seasonality_index IS DISTINCT FROM EXCLUDED.seasonality_index OR
            product_metrics.service_level_30d IS DISTINCT FROM EXCLUDED.service_level_30d OR
            product_metrics.fill_rate_30d IS DISTINCT FROM EXCLUDED.fill_rate_30d OR
            -- Time-based safety net: always update if more than 1 day stale
            product_metrics.last_calculated < NOW() - INTERVAL '1 day'
        ;

    -- Update the status table with the timestamp from the START of this run
    UPDATE public.calculate_status
    SET last_calculation_timestamp = _start_time
    WHERE module_name = _module_name;

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

END $$;

-- Return metrics about the update operation
WITH update_stats AS (
    SELECT 
        COUNT(*) as total_products,
        COUNT(*) FILTER (WHERE last_calculated >= NOW() - INTERVAL '5 minutes') as rows_processed,
        COUNT(*) FILTER (WHERE status = 'Critical') as critical_count,
        COUNT(*) FILTER (WHERE status = 'Reorder Soon') as reorder_soon_count,
        COUNT(*) FILTER (WHERE status = 'Healthy') as healthy_count,
        COUNT(*) FILTER (WHERE status = 'Overstock') as overstock_count,
        COUNT(*) FILTER (WHERE status = 'At Risk') as at_risk_count,
        COUNT(*) FILTER (WHERE status = 'New') as new_count
    FROM public.product_metrics
)
SELECT 
    rows_processed,
    total_products,
    critical_count,
    reorder_soon_count,
    healthy_count,
    overstock_count,
    at_risk_count,
    new_count,
    ROUND((rows_processed::numeric / NULLIF(total_products, 0)) * 100, 2) as update_percentage
FROM update_stats;