-- Description: Performs the first population OR full recalculation of the product_metrics table based on
--              historically backfilled daily_product_snapshots and current product/PO data.
--              Calculates all metrics considering the full available history up to 'yesterday'.
--              Run ONCE after backfill_historical_snapshots_final.sql completes successfully.
-- Dependencies: Core import tables (products, purchase_orders, receivings), daily_product_snapshots (historically populated),
--               configuration tables (settings_*), product_metrics table must exist.
-- Frequency: Run ONCE.
DO $$
DECLARE
    _module_name VARCHAR := 'product_metrics_population'; -- Generic name
    _start_time TIMESTAMPTZ := clock_timestamp();
    -- Calculate metrics AS OF the end of the last fully completed day
    _calculation_date DATE := CURRENT_DATE - INTERVAL '1 day';
BEGIN
    RAISE NOTICE 'Running % module. Calculating AS OF: %. Start Time: %', _module_name, _calculation_date, _start_time;

    -- Optional: Consider TRUNCATE if you want a completely fresh start,
    -- otherwise ON CONFLICT will update existing rows if this is rerun.
    -- TRUNCATE TABLE public.product_metrics;
    RAISE NOTICE 'Populating product_metrics table. This may take some time...';

    -- CTEs to gather necessary information AS OF _calculation_date
    WITH CurrentInfo AS (
        -- Fetches current product details, including costs/prices used for forecasting & fallbacks
        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,
            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, -- Use actual current stock for forecast base
            p.created_at, p.first_received, p.date_last_sold,
            p.moq,
            p.uom,
            p.total_sold as historical_total_sold -- Add historical total_sold from products table
        FROM public.products p
    ),
    OnOrderInfo AS (
        -- Calculates current on-order quantities and costs
        SELECT
            pid,
            SUM(ordered) AS on_order_qty,
            SUM(ordered * po_cost_price) AS on_order_cost,
            MIN(expected_date) AS earliest_expected_date
        FROM public.purchase_orders
        -- Use the most common statuses representing active, unfulfilled POs
        WHERE status IN ('created', 'ordered', 'preordered', 'electronically_sent', 'electronically_ready_send', 'receiving_started')
          AND status NOT IN ('canceled', 'done')
        GROUP BY pid
    ),
    HistoricalDates AS (
        -- Determines key historical dates from orders and receivings
         SELECT
            p.pid,
            MIN(o.date)::date AS date_first_sold,
            MAX(o.date)::date AS max_order_date, -- Used as fallback for date_last_sold
            MIN(r.received_date)::date AS date_first_received_calc,
            MAX(r.received_date)::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')
        LEFT JOIN public.receivings r ON p.pid = r.pid
        GROUP BY p.pid
    ),
    SnapshotAggregates AS (
        -- Aggregates metrics from historical snapshots up to the _calculation_date
        SELECT
            pid,
            -- Rolling periods relative to _calculation_date
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '6 days' AND _calculation_date THEN units_sold ELSE 0 END) AS sales_7d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '6 days' AND _calculation_date THEN net_revenue ELSE 0 END) AS revenue_7d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '13 days' AND _calculation_date THEN units_sold ELSE 0 END) AS sales_14d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '13 days' AND _calculation_date THEN net_revenue ELSE 0 END) AS revenue_14d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN units_sold ELSE 0 END) AS sales_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN net_revenue ELSE 0 END) AS revenue_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN cogs ELSE 0 END) AS cogs_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN profit ELSE 0 END) AS profit_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN units_returned ELSE 0 END) AS returns_units_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN returns_revenue ELSE 0 END) AS returns_revenue_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN discounts ELSE 0 END) AS discounts_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN gross_revenue ELSE 0 END) AS gross_revenue_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN gross_regular_revenue ELSE 0 END) AS gross_regular_revenue_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date AND stockout_flag THEN 1 ELSE 0 END) AS stockout_days_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '364 days' AND _calculation_date THEN units_sold ELSE 0 END) AS sales_365d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '364 days' AND _calculation_date THEN net_revenue ELSE 0 END) AS revenue_365d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN units_received ELSE 0 END) AS received_qty_30d,
            SUM(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN cost_received ELSE 0 END) AS received_cost_30d,

            -- Averages over the last 30 days ending _calculation_date
            AVG(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN eod_stock_quantity END) AS avg_stock_units_30d,
            AVG(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN eod_stock_cost END) AS avg_stock_cost_30d,
            AVG(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN eod_stock_retail END) AS avg_stock_retail_30d,
            AVG(CASE WHEN snapshot_date BETWEEN _calculation_date - INTERVAL '29 days' AND _calculation_date THEN eod_stock_gross END) AS avg_stock_gross_30d,

            -- Lifetime (Using historical total from products table)
            (SELECT total_sold FROM public.products WHERE public.products.pid = daily_product_snapshots.pid) AS lifetime_sales,
            COALESCE(
                -- Option 1: Use 30-day average price if available
                CASE WHEN SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '29 days' AND snapshot_date <= _calculation_date THEN units_sold ELSE 0 END) > 0 THEN
                    (SELECT total_sold FROM public.products WHERE public.products.pid = daily_product_snapshots.pid) * (
                        SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '29 days' AND snapshot_date <= _calculation_date THEN net_revenue ELSE 0 END) / 
                        NULLIF(SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '29 days' AND snapshot_date <= _calculation_date THEN units_sold ELSE 0 END), 0)
                    )
                ELSE NULL END,
                -- Option 2: Try 365-day average price if available
                CASE WHEN SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '364 days' AND snapshot_date <= _calculation_date THEN units_sold ELSE 0 END) > 0 THEN
                    (SELECT total_sold FROM public.products WHERE public.products.pid = daily_product_snapshots.pid) * (
                        SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '364 days' AND snapshot_date <= _calculation_date THEN net_revenue ELSE 0 END) / 
                        NULLIF(SUM(CASE WHEN snapshot_date >= _calculation_date - INTERVAL '364 days' AND snapshot_date <= _calculation_date THEN units_sold ELSE 0 END), 0)
                    )
                ELSE NULL END,
                -- Option 3: Use current price from products table
                (SELECT total_sold * price FROM public.products WHERE public.products.pid = daily_product_snapshots.pid),
                -- Option 4: Use regular price if current price might be zero
                (SELECT total_sold * regular_price FROM public.products WHERE public.products.pid = daily_product_snapshots.pid),
                -- Final fallback: Use accumulated revenue (less accurate for old products)
                SUM(net_revenue)
            ) AS lifetime_revenue,

            -- Yesterday (Sales for the specific _calculation_date)
            SUM(CASE WHEN snapshot_date = _calculation_date THEN units_sold ELSE 0 END) as yesterday_sales

        FROM public.daily_product_snapshots
        WHERE snapshot_date <= _calculation_date -- Ensure we only use data up to the calculation point
        GROUP BY pid
    ),
    FirstPeriodMetrics AS (
       -- Calculates sales/revenue for first X days after first sale date
       -- Uses HistoricalDates CTE to get the first sale date
        SELECT
            pid, date_first_sold,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '6 days' THEN units_sold ELSE 0 END) AS first_7_days_sales,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '6 days' THEN net_revenue ELSE 0 END) AS first_7_days_revenue,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '29 days' THEN units_sold ELSE 0 END) AS first_30_days_sales,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '29 days' THEN net_revenue ELSE 0 END) AS first_30_days_revenue,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '59 days' THEN units_sold ELSE 0 END) AS first_60_days_sales,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '59 days' THEN net_revenue ELSE 0 END) AS first_60_days_revenue,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND date_first_sold + INTERVAL '89 days' THEN units_sold ELSE 0 END) AS first_90_days_sales,
            SUM(CASE WHEN snapshot_date BETWEEN date_first_sold AND 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 -- Only consider snapshots after first sale
          AND snapshot_date <= _calculation_date -- Only up to the overall calculation date
        GROUP BY pid, date_first_sold
    ),
    Settings AS (
       -- Fetches effective configuration settings (Product > Vendor > Global)
       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, (SELECT setting_value::int FROM settings_global WHERE setting_key = 'default_safety_stock_units'), 0) AS effective_safety_stock,
        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
    ),
    AvgLeadTime AS (
        -- Calculate Average Lead Time by joining purchase_orders with receivings
        SELECT
            po.pid,
            AVG(GREATEST(1, 
                CASE 
                    WHEN r.received_date IS NOT NULL AND po.date IS NOT NULL 
                    THEN (r.received_date::date - po.date::date)
                    ELSE 1
                END
            ))::int AS avg_lead_time_days_calc
        FROM public.purchase_orders po
        JOIN public.receivings r ON r.pid = po.pid
        WHERE po.status = 'done' -- Completed POs
          AND r.received_date IS NOT NULL
          AND po.date IS NOT NULL
          AND r.received_date >= po.date
        GROUP BY po.pid
    ),
    RankedForABC AS (
        -- Ranks products based on the configured ABC metric (using historical data)
         SELECT
            p.pid,
            CASE COALESCE((SELECT setting_value FROM settings_global WHERE setting_key = 'abc_calculation_basis'), 'revenue_30d')
                WHEN 'sales_30d' THEN COALESCE(sa.sales_30d, 0)
                WHEN 'lifetime_revenue' THEN COALESCE(sa.lifetime_revenue, 0)::numeric
                ELSE COALESCE(sa.revenue_30d, 0) -- Default to revenue_30d
            END AS metric_value
        FROM public.products p -- Use products as the base
        JOIN SnapshotAggregates sa ON p.pid = sa.pid
        WHERE p.replenishable = TRUE -- Only rank replenishable products
          AND (CASE COALESCE((SELECT setting_value FROM settings_global WHERE setting_key = 'abc_calculation_basis'), 'revenue_30d')
                WHEN 'sales_30d' THEN COALESCE(sa.sales_30d, 0)
                WHEN 'lifetime_revenue' THEN COALESCE(sa.lifetime_revenue, 0)::numeric
                ELSE COALESCE(sa.revenue_30d, 0)
              END) > 0 -- Only include products with non-zero contribution
    ),
    CumulativeABC AS (
        -- Calculates cumulative metric values for ABC ranking
         SELECT
            pid, metric_value,
            SUM(metric_value) OVER (ORDER BY metric_value DESC NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_metric,
            SUM(metric_value) OVER () as total_metric
        FROM RankedForABC
    ),
    FinalABC AS (
        -- Assigns A, B, or C class based on thresholds
        SELECT
            pid,
            CASE
                WHEN cumulative_metric / NULLIF(total_metric, 0) <= COALESCE((SELECT setting_value::numeric FROM settings_global WHERE setting_key = 'abc_revenue_threshold_a'), 0.8) THEN 'A'::char(1)
                WHEN cumulative_metric / NULLIF(total_metric, 0) <= COALESCE((SELECT setting_value::numeric FROM settings_global WHERE setting_key = 'abc_revenue_threshold_b'), 0.95) THEN 'B'::char(1)
                ELSE 'C'::char(1)
            END AS abc_class_calc
         FROM CumulativeABC
    )
    -- Final INSERT/UPDATE statement using all the prepared CTEs
    INSERT INTO public.product_metrics (
        pid, last_calculated, sku, title, brand, vendor, image_url, is_visible, is_replenishable,
        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,
        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,
        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, abc_class,
        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
    )
    SELECT
        -- Select columns in order, joining all CTEs by pid
        ci.pid, _start_time, ci.sku, ci.title, ci.brand, ci.vendor, ci.image_url, ci.is_visible, ci.replenishable,
        ci.current_price, ci.current_regular_price, ci.current_cost_price,
        ci.current_stock, (ci.current_stock * COALESCE(ci.current_effective_cost, 0.00))::numeric(12,2), (ci.current_stock * COALESCE(ci.current_price, 0.00))::numeric(12,2), (ci.current_stock * COALESCE(ci.current_regular_price, 0.00))::numeric(12,2),
        COALESCE(ooi.on_order_qty, 0), COALESCE(ooi.on_order_cost, 0.00)::numeric(12,2), (COALESCE(ooi.on_order_qty, 0) * COALESCE(ci.current_price, 0.00))::numeric(12,2), ooi.earliest_expected_date,
        
        -- Fix type issue with date calculation - properly cast timestamps to dates before arithmetic
        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), 
        -- Fix timestamp + integer error by ensuring we work only with dates
        CASE 
            WHEN LEAST(ci.created_at::date, COALESCE(hd.date_first_sold, ci.created_at::date)) IS NOT NULL 
            THEN (_calculation_date::date - LEAST(ci.created_at::date, COALESCE(hd.date_first_sold, ci.created_at::date)))::int
            ELSE NULL
        END,
        
        COALESCE(sa.sales_7d, 0), COALESCE(sa.revenue_7d, 0), COALESCE(sa.sales_14d, 0), COALESCE(sa.revenue_14d, 0), COALESCE(sa.sales_30d, 0), COALESCE(sa.revenue_30d, 0), COALESCE(sa.cogs_30d, 0), COALESCE(sa.profit_30d, 0),
        COALESCE(sa.returns_units_30d, 0), COALESCE(sa.returns_revenue_30d, 0), COALESCE(sa.discounts_30d, 0), COALESCE(sa.gross_revenue_30d, 0), COALESCE(sa.gross_regular_revenue_30d, 0),
        COALESCE(sa.stockout_days_30d, 0), COALESCE(sa.sales_365d, 0), COALESCE(sa.revenue_365d, 0),
        sa.avg_stock_units_30d, sa.avg_stock_cost_30d, sa.avg_stock_retail_30d, sa.avg_stock_gross_30d, -- Averages can be NULL if no data
        COALESCE(sa.received_qty_30d, 0), COALESCE(sa.received_cost_30d, 0),
        COALESCE(sa.lifetime_sales, 0), COALESCE(sa.lifetime_revenue, 0),
        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,

        -- Calculated KPIs (using COALESCE on inputs where appropriate)
        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,
        COALESCE(sa.sales_30d, 0) / 30.0 AS avg_sales_per_day_30d,
        
        -- Fix for percentages - cast to numeric with appropriate precision
        ((sa.profit_30d / NULLIF(sa.revenue_30d, 0)) * 100)::numeric(8,2) AS margin_30d,
        ((sa.profit_30d / NULLIF(sa.cogs_30d, 0)) * 100)::numeric(8,2) AS markup_30d,
        sa.profit_30d / NULLIF(sa.avg_stock_cost_30d, 0) AS gmroi_30d,
        sa.sales_30d / NULLIF(sa.avg_stock_units_30d, 0) AS stockturn_30d,
        ((sa.returns_units_30d / NULLIF(COALESCE(sa.sales_30d, 0) + COALESCE(sa.returns_units_30d, 0), 0)) * 100)::numeric(8,2) AS return_rate_30d,
        ((sa.discounts_30d / NULLIF(sa.gross_revenue_30d, 0)) * 100)::numeric(8,2) AS discount_rate_30d,
        ((COALESCE(sa.stockout_days_30d, 0) / 30.0) * 100)::numeric(8,2) AS stockout_rate_30d,
        GREATEST(0, sa.gross_regular_revenue_30d - sa.gross_revenue_30d) AS markdown_30d, -- Ensure markdown isn't negative
        ((GREATEST(0, sa.gross_regular_revenue_30d - sa.gross_revenue_30d) / NULLIF(sa.gross_regular_revenue_30d, 0)) * 100)::numeric(8,2) AS markdown_rate_30d,
        -- Sell Through Rate: Sales / (Stock at end of period + Sales). This is one definition proxying for Sales / Beginning Stock.
        ((sa.sales_30d / NULLIF(
             (SELECT eod_stock_quantity FROM daily_product_snapshots WHERE snapshot_date = _calculation_date AND pid = ci.pid LIMIT 1) + COALESCE(sa.sales_30d, 0)
             , 0)) * 100)::numeric(8,2) AS sell_through_30d,

        -- Use calculated periodic metrics
        alt.avg_lead_time_days_calc,
        CASE
            WHEN ci.replenishable = FALSE THEN NULL -- Non-replenishable don't get a class
            ELSE COALESCE(fa.abc_class_calc, 'C') -- Default ranked replenishable but non-contributing to C
        END,

        -- Forecasting intermediate values (based on historical aggregates ending _calculation_date)
        (COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) AS sales_velocity_daily, -- Ensure divisor > 0
        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,
        -- Calculate raw forecast need components (using safe velocity)
        (COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time AS lead_time_forecast_units,
        (COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock AS days_of_stock_forecast_units,
        -- Planning period forecast units (sum of lead time and DOS units)
        CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
          + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock) AS planning_period_forecast_units,
        -- Closing stock calculations (using raw forecast components for accuracy before rounding)
        (ci.current_stock + COALESCE(ooi.on_order_qty, 0) - ((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)) AS lead_time_closing_stock,
        ((ci.current_stock + COALESCE(ooi.on_order_qty, 0) - ((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)))
          - ((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock) AS days_of_stock_closing_stock,
        -- Raw replenishment needed
        (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time) -- Use rounded forecast units
           + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
           + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0) AS replenishment_needed_raw,

        -- Final Forecasting Metrics
        -- Replenishment Units (calculated need, before MOQ)
        CEILING(GREATEST(0,
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
             + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
        ))::int AS replenishment_units,
        -- Replenishment Cost/Retail/Profit (based on replenishment_units)
        (CEILING(GREATEST(0, 
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
             + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
        ))::int) * COALESCE(ci.current_effective_cost, 0.00)::numeric(12,2) AS replenishment_cost,
        (CEILING(GREATEST(0, 
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
             + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
        ))::int) * COALESCE(ci.current_price, 0.00)::numeric(12,2) AS replenishment_retail,
        (CEILING(GREATEST(0, 
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
             + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
        ))::int) * (COALESCE(ci.current_price, 0.00) - COALESCE(ci.current_effective_cost, 0.00))::numeric(12,2) AS replenishment_profit,

        -- *** FIX: To Order Units (Apply MOQ rounding) ***
        CASE
            WHEN COALESCE(ci.moq, 0) <= 1 THEN -- Treat no/invalid MOQ or MOQ=1 as no rounding needed
                CEILING(GREATEST(0, 
                   (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
                     + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
                     + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
                ))::int
            ELSE -- Apply MOQ rounding: Round UP to nearest multiple of MOQ
                 (CEILING(GREATEST(0, 
                    (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
                      + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
                      + s.effective_safety_stock - ci.current_stock - COALESCE(ooi.on_order_qty, 0)
                 ) / NULLIF(ci.moq::numeric, 0)) * COALESCE(ci.moq, 1))::int
        END AS to_order_units,

        -- Forecast Lost Sales (Units occurring during lead time if current+on_order is insufficient)
        CEILING(GREATEST(0,
            ((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time) -- Demand during lead time
            - (ci.current_stock + COALESCE(ooi.on_order_qty, 0)) -- Supply available before order arrives
        ))::int AS forecast_lost_sales_units,
        -- Forecast Lost Revenue
        (CEILING(GREATEST(0, 
            ((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
            - (ci.current_stock + COALESCE(ooi.on_order_qty, 0))
        ))::int) * COALESCE(ci.current_price, 0.00)::numeric(12,2) AS forecast_lost_revenue,

        -- Stock Cover etc (using safe velocity)
        ci.current_stock / NULLIF((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)), 0) AS stock_cover_in_days,
        COALESCE(ooi.on_order_qty, 0) / NULLIF((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)), 0) AS po_cover_in_days,
        (ci.current_stock + COALESCE(ooi.on_order_qty, 0)) / NULLIF((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)), 0) AS sells_out_in_days,
         -- Replenish Date (Project forward from 'today', which is _calculation_date + 1 day)
        CASE
            WHEN (COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) > 0 -- Check for positive velocity
            THEN
                _calculation_date + INTERVAL '1 day' -- Today
                + FLOOR(GREATEST(0, ci.current_stock - s.effective_safety_stock) -- Stock above safety
                        / (COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) -- divided by velocity
                  )::integer * INTERVAL '1 day' -- Gives date safety stock is hit
                - s.effective_lead_time * INTERVAL '1 day' -- Subtract lead time
            ELSE NULL -- Cannot calculate if no sales velocity
        END AS replenish_date,
        -- Overstocked Units (Stock above safety + planning period demand)
        GREATEST(0, ci.current_stock - s.effective_safety_stock -
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time) -- Demand during lead time
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock)) -- Demand during DOS
        )::int AS overstocked_units,
        (GREATEST(0, ci.current_stock - s.effective_safety_stock -
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
        )::int) * COALESCE(ci.current_effective_cost, 0.00)::numeric(12,2) AS overstocked_cost,
        (GREATEST(0, ci.current_stock - s.effective_safety_stock -
           (CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_lead_time)
             + CEILING((COALESCE(sa.sales_30d, 0) / NULLIF(GREATEST(1.0, 30.0 - COALESCE(sa.stockout_days_30d, 0)), 0)) * s.effective_days_of_stock))
        )::int) * COALESCE(ci.current_price, 0.00)::numeric(12,2) AS overstocked_retail,
        -- Old Stock Flag
        (ci.created_at::date < (_calculation_date - INTERVAL '60 day')::date) AND
        (COALESCE(ci.date_last_sold, hd.max_order_date) IS NULL OR COALESCE(ci.date_last_sold, hd.max_order_date) < (_calculation_date - INTERVAL '60 day')::date) AND
        (hd.date_last_received_calc IS NULL OR hd.date_last_received_calc < (_calculation_date - INTERVAL '60 day')::date) AND
        COALESCE(ooi.on_order_qty, 0) = 0 AS is_old_stock,
        COALESCE(sa.yesterday_sales, 0) -- Sales for _calculation_date

    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 AvgLeadTime alt ON ci.pid = alt.pid -- Join calculated avg lead time
    LEFT JOIN FinalABC fa ON ci.pid = fa.pid -- Join calculated ABC class
    WHERE s.exclude_forecast IS FALSE OR s.exclude_forecast IS NULL

    ON CONFLICT (pid) DO UPDATE SET
        -- *** IMPORTANT: List ALL columns here, ensuring order matches INSERT list ***
        -- Update ALL columns to ensure entire row is refreshed
        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,
        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,
        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, 
        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, abc_class = EXCLUDED.abc_class,
        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, -- *** Update to use EXCLUDED ***
        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;
    RAISE NOTICE 'Finished % module. Duration: %', _module_name, clock_timestamp() - _start_time;
END $$;