/**
 * Phase 4 — margin, COGS, shrinkage analytics (client-side).
 * Depends on purchases + inventoryLedger + orders + products snapshots.
 */

import { classifyOrderChannel, getOrderCreatedAtMs, firebaseTimestampToMs } from './order-time-channel.js';
import { roundMoney2 } from './pos-finance.js';

export function skuCostKey(productId, variantId) {
    return `${String(productId)}|${variantId != null && variantId !== '' ? String(variantId) : ''}`;
}

function lineNetBuyCost(line) {
    const qty = Math.max(0, Number(line.qty) || 0);
    const unit = Number(line.unitBuyPrice) || 0;
    const disc = Math.max(0, roundMoney2(Number(line.lineDiscount) || 0));
    const gross = roundMoney2(qty * unit);
    return Math.max(0, roundMoney2(gross - disc));
}

/**
 * Merge purchase lines + ledger purchase events into qty/cost totals per SKU.
 * @param {Record<string, unknown>} purchases
 * @param {Record<string, unknown>} inventoryLedger
 */
export function aggregatePurchaseCostTotalsBySku(purchases, inventoryLedger) {
    /** @type {Map<string, { qty: number, costSum: number }>} */
    const m = new Map();
    const add = (pid, vid, qty, costSum) => {
        if (!pid || qty <= 0 || costSum <= 0) return;
        const k = skuCostKey(pid, vid);
        const cur = m.get(k) || { qty: 0, costSum: 0 };
        cur.qty = roundMoney2(cur.qty + qty);
        cur.costSum = roundMoney2(cur.costSum + costSum);
        m.set(k, cur);
    };

    let purchaseLineCount = 0;
    for (const p of Object.values(purchases || {})) {
        if (!p || typeof p !== 'object') continue;
        const items = Array.isArray(p.items) ? p.items : [];
        for (const line of items) {
            if (!line) continue;
            const pid = line.productId;
            const vid = line.variantId || null;
            const qty = Number(line.qty) || 0;
            if (qty <= 0) continue;
            purchaseLineCount++;
            const net = lineNetBuyCost(line);
            add(pid, vid, qty, net);
        }
    }

    // Avoid double-counting vs `purchases` rows: only use ledger purchases when there is no purchase history.
    if (purchaseLineCount === 0) {
        for (const ev of Object.values(inventoryLedger || {})) {
            if (!ev || typeof ev !== 'object') continue;
            if (String(ev.eventType || '') !== 'purchase') continue;
            const dq = Number(ev.qtyDelta) || 0;
            if (dq <= 0) continue;
            const uc = ev.unitCost != null ? Number(ev.unitCost) : null;
            if (uc == null || Number.isNaN(uc) || uc < 0) continue;
            const pid = ev.productId;
            const vid = ev.variantId || null;
            add(pid, vid, dq, roundMoney2(dq * uc));
        }
    }
    return m;
}

/**
 * @returns {{ avgMap: Map<string, { avgUnitCost: number, quality: string }>, totals: Map<string, { qty: number, costSum: number }> }}
 */
export function buildSkuAverageUnitCostMap(purchases, inventoryLedger) {
    const totals = aggregatePurchaseCostTotalsBySku(purchases, inventoryLedger);
    const avgMap = new Map();
    for (const [k, v] of totals) {
        if (v.qty > 0 && v.costSum > 0) {
            avgMap.set(k, { avgUnitCost: roundMoney2(v.costSum / v.qty), quality: 'moving_avg_v1' });
        }
    }
    return { avgMap, totals };
}

/**
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {string} productId
 * @param {string|null} variantId
 * @param {any} product
 */
export function resolveUnitCostForSku(avgMap, productId, variantId, product) {
    const k = skuCostKey(productId, variantId);
    if (avgMap.has(k)) {
        return { unitCost: avgMap.get(k).avgUnitCost, costMethod: 'moving_avg_v1', quality: 'moving_avg_v1' };
    }
    if (variantId && product?.variants?.[variantId]) {
        const sp = Number(product.variants[variantId].salePrice) || 0;
        if (sp > 0) {
            return { unitCost: roundMoney2(sp * 0.65), costMethod: 'heuristic_sale_065', quality: 'heuristic_sale_065' };
        }
    }
    const sp = Number(product?.salePrice) || 0;
    if (sp > 0) {
        return { unitCost: roundMoney2(sp * 0.65), costMethod: 'heuristic_sale_065', quality: 'heuristic_sale_065' };
    }
    return { unitCost: 0, costMethod: 'missing', quality: 'missing' };
}

function parseSoldQty(item, product) {
    const q = Number(item.quantity);
    if (product?.isWeighted) return Number.isFinite(q) ? q : 0;
    return Number.isFinite(q) ? Math.max(0, Math.floor(q)) : 0;
}

function netSoldQty(item, product) {
    const sold = parseSoldQty(item, product);
    const ref = Number(item.refundedQty) || 0;
    const net = product?.isWeighted ? roundMoney2(sold - ref) : sold - ref;
    return Math.max(0, net);
}

/** Exported for Phase 5 top-product and drilldown helpers. */
export function netSoldQtyForLine(item, product) {
    return netSoldQty(item, product);
}

/**
 * @param {object[]} orders
 * @param {Record<string, unknown>} products
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {'all'|'pos'|'online'} channelFilter
 */
export function computeOrderDerivedMargin(orders, products, avgMap, channelFilter = 'all') {
    let revenue = 0;
    let cogs = 0;
    /** @type {Record<string, { revenue: number, cogs: number, name: string }>} */
    const bySku = {};
    const byCh = {
        pos: { revenue: 0, cogs: 0 },
        online: { revenue: 0, cogs: 0 },
        unknown: { revenue: 0, cogs: 0 }
    };

    for (const order of orders || []) {
        if (!order) continue;
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const bucket = ch === 'pos' || ch === 'online' ? ch : 'unknown';
        const items = Array.isArray(order.items) ? order.items : Object.values(order.items || {});
        for (const item of items) {
            if (!item || !item.id) continue;
            const pid = item.id;
            const vid = item.variantId || null;
            const p = products[pid];
            const netQ = netSoldQty(item, p);
            if (netQ <= 0) continue;
            const price = Number(item.price) || 0;
            const rev = roundMoney2(netQ * price);
            const { unitCost, quality } = resolveUnitCostForSku(avgMap, pid, vid, p);
            const cog = roundMoney2(netQ * unitCost);
            revenue = roundMoney2(revenue + rev);
            cogs = roundMoney2(cogs + cog);
            byCh[bucket].revenue = roundMoney2(byCh[bucket].revenue + rev);
            byCh[bucket].cogs = roundMoney2(byCh[bucket].cogs + cog);
            const sk = skuCostKey(pid, vid);
            if (!bySku[sk]) bySku[sk] = { revenue: 0, cogs: 0, name: String(item.name || pid), quality };
            bySku[sk].revenue = roundMoney2(bySku[sk].revenue + rev);
            bySku[sk].cogs = roundMoney2(bySku[sk].cogs + cog);
            if (bySku[sk].quality === 'missing' && quality !== 'missing') bySku[sk].quality = quality;
        }
    }

    const grossProfit = roundMoney2(revenue - cogs);
    const marginPct = revenue > 0 ? roundMoney2((grossProfit / revenue) * 100) : 0;
    return { revenue, cogs, grossProfit, marginPct, bySku, byChannel: byCh };
}

/**
 * Shrinkage / wastage estimated cost from ledger.
 */
export function computeShrinkageCostEstimate(inventoryLedger, products, avgMap) {
    let shrink = 0;
    for (const ev of Object.values(inventoryLedger || {})) {
        if (!ev || typeof ev !== 'object') continue;
        const et = String(ev.eventType || '');
        const dq = Number(ev.qtyDelta) || 0;
        if (et === 'wastage') {
            const q = Math.abs(dq);
            const uc = ev.unitCost != null && !Number.isNaN(Number(ev.unitCost))
                ? Number(ev.unitCost)
                : resolveUnitCostForSku(avgMap, ev.productId, ev.variantId || null, products[ev.productId]).unitCost;
            shrink = roundMoney2(shrink + roundMoney2(q * uc));
        } else if (et === 'adjustment' && dq < 0) {
            const q = Math.abs(dq);
            const uc = ev.unitCost != null && !Number.isNaN(Number(ev.unitCost))
                ? Number(ev.unitCost)
                : resolveUnitCostForSku(avgMap, ev.productId, ev.variantId || null, products[ev.productId]).unitCost;
            shrink = roundMoney2(shrink + roundMoney2(q * uc));
        }
    }
    return shrink;
}

/**
 * @param {object[]} orders
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {Record<string, unknown>} products
 * @param {'all'|'pos'|'online'} channelFilter
 * @param {number} days
 */
export function computeMarginByDateLastDays(orders, avgMap, products, channelFilter, days = 30) {
    const today = new Date();
    const start = new Date(today);
    start.setDate(start.getDate() - days);
    /** @type {Map<string, { revenue: number, cogs: number }>} */
    const map = new Map();
    const pad = (d) => d.toISOString().split('T')[0];

    let d = new Date(start);
    while (d <= today) {
        map.set(pad(d), { revenue: 0, cogs: 0 });
        d.setDate(d.getDate() + 1);
    }

    for (const order of orders || []) {
        if (!order) continue;
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const ms = getOrderCreatedAtMs(order);
        if (ms == null) continue;
        const od = new Date(ms);
        if (od < start || od > today) continue;
        const key = pad(od);
        if (!map.has(key)) map.set(key, { revenue: 0, cogs: 0 });
        const items = Array.isArray(order.items) ? order.items : Object.values(order.items || {});
        for (const item of items) {
            if (!item || !item.id) continue;
            const p = products[item.id];
            const netQ = netSoldQty(item, p);
            if (netQ <= 0) continue;
            const rev = roundMoney2(netQ * (Number(item.price) || 0));
            const { unitCost } = resolveUnitCostForSku(avgMap, item.id, item.variantId || null, p);
            const cog = roundMoney2(netQ * unitCost);
            const cell = map.get(key);
            cell.revenue = roundMoney2(cell.revenue + rev);
            cell.cogs = roundMoney2(cell.cogs + cog);
        }
    }
    return map;
}

/**
 * @param {Record<string, { revenue: number, cogs: number, name: string, quality?: string }>} bySku
 * @param {{ lowMarginPct: number, minRevenue: number }} opts
 */
export function pickLowMarginSkus(bySku, opts = {}) {
    const lowPct = opts.lowMarginPct != null ? Number(opts.lowMarginPct) : 15;
    const minRev = opts.minRevenue != null ? Number(opts.minRevenue) : 200;
    const rows = Object.entries(bySku || {})
        .map(([k, v]) => {
            const marginPct = v.revenue > 0 ? roundMoney2(((v.revenue - v.cogs) / v.revenue) * 100) : 0;
            return { key: k, name: v.name, revenue: v.revenue, cogs: v.cogs, marginPct, quality: v.quality };
        })
        .filter((r) => r.revenue >= minRev && r.marginPct < lowPct)
        .sort((a, b) => a.marginPct - b.marginPct);
    return rows.slice(0, 15);
}

/**
 * @param {Record<string, unknown>} inventoryLedger
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {Record<string, unknown>} products
 */
function getLedgerEventMs(ev) {
    if (!ev || typeof ev !== 'object') return null;
    return firebaseTimestampToMs(ev.createdAt);
}

/**
 * Weighted-average unitCost from sale ledger rows in a time window (Phase 5 precedence layer 1).
 * @param {Record<string, unknown>} inventoryLedger
 * @param {number|null} startMs inclusive
 * @param {number|null} endMs inclusive
 * @returns {Map<string, { qtyAbs: number, costSum: number }>}
 */
export function buildSkuLedgerSaleSnapshotCostMap(inventoryLedger, startMs, endMs) {
    /** @type {Map<string, { qtyAbs: number, costSum: number }>} */
    const m = new Map();
    for (const ev of Object.values(inventoryLedger || {})) {
        if (!ev || typeof ev !== 'object') continue;
        if (String(ev.eventType || '') !== 'sale') continue;
        const t = getLedgerEventMs(ev);
        if (startMs != null && (t == null || t < startMs)) continue;
        if (endMs != null && (t == null || t > endMs)) continue;
        const uc = Number(ev.unitCost);
        if (!Number.isFinite(uc) || uc <= 0) continue;
        const dq = Number(ev.qtyDelta) || 0;
        const q = Math.abs(dq);
        if (q <= 0) continue;
        const k = skuCostKey(ev.productId, ev.variantId || null);
        const cur = m.get(k) || { qtyAbs: 0, costSum: 0 };
        cur.qtyAbs = roundMoney2(cur.qtyAbs + q);
        cur.costSum = roundMoney2(cur.costSum + roundMoney2(q * uc));
        m.set(k, cur);
    }
    return m;
}

/**
 * Phase 5 COGS precedence: ledger snapshot window → moving avg → heuristic → missing.
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {Map<string, { qtyAbs: number, costSum: number }>|null|undefined} saleSnapshotMap
 */
export function resolveUnitCostPhase5(avgMap, saleSnapshotMap, productId, variantId, product) {
    const k = skuCostKey(productId, variantId);
    if (saleSnapshotMap && saleSnapshotMap.has(k)) {
        const s = saleSnapshotMap.get(k);
        if (s && s.qtyAbs > 0 && s.costSum > 0) {
            return {
                unitCost: roundMoney2(s.costSum / s.qtyAbs),
                costMethod: 'ledger_sale_snapshot_avg',
                quality: 'ledger_snapshot_avg'
            };
        }
    }
    return resolveUnitCostForSku(avgMap, productId, variantId, product);
}

function lineMatchesProductFilters(product, categoryId, weightedOnly) {
    if (!product) return !categoryId && !weightedOnly;
    if (categoryId != null && String(categoryId) !== '' && String(product.category || '') !== String(categoryId)) return false;
    if (weightedOnly && !product.isWeighted) return false;
    return true;
}

/**
 * @param {object[]} orders
 * @param {Record<string, unknown>} products
 * @param {Map<string, { avgUnitCost: number, quality: string }>} avgMap
 * @param {Map<string, { qtyAbs: number, costSum: number }>|null|undefined} saleSnapshotMap
 * @param {'all'|'pos'|'online'} channelFilter
 * @param {{ startMs?: number|null, endMs?: number|null, categoryId?: string|null, weightedOnly?: boolean }} filters
 */
export function computeOrderDerivedMarginFiltered(
    orders,
    products,
    avgMap,
    saleSnapshotMap,
    channelFilter = 'all',
    filters = {}
) {
    const { startMs = null, endMs = null, categoryId = null, weightedOnly = false } = filters;
    let revenue = 0;
    let cogs = 0;
    /** @type {Record<string, { revenue: number, cogs: number, name: string, quality?: string }>} */
    const bySku = {};
    const byCh = {
        pos: { revenue: 0, cogs: 0 },
        online: { revenue: 0, cogs: 0 },
        unknown: { revenue: 0, cogs: 0 }
    };

    for (const order of orders || []) {
        if (!order) continue;
        const oms = getOrderCreatedAtMs(order);
        if (startMs != null && (oms == null || oms < startMs)) continue;
        if (endMs != null && (oms == null || oms > endMs)) continue;
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const bucket = ch === 'pos' || ch === 'online' ? ch : 'unknown';
        const items = Array.isArray(order.items) ? order.items : Object.values(order.items || {});
        for (const item of items) {
            if (!item || !item.id) continue;
            const pid = item.id;
            const vid = item.variantId || null;
            const p = products[pid];
            if (!lineMatchesProductFilters(p, categoryId, weightedOnly)) continue;
            const netQ = netSoldQty(item, p);
            if (netQ <= 0) continue;
            const price = Number(item.price) || 0;
            const rev = roundMoney2(netQ * price);
            const { unitCost, quality } = resolveUnitCostPhase5(avgMap, saleSnapshotMap, pid, vid, p);
            const cog = roundMoney2(netQ * unitCost);
            revenue = roundMoney2(revenue + rev);
            cogs = roundMoney2(cogs + cog);
            byCh[bucket].revenue = roundMoney2(byCh[bucket].revenue + rev);
            byCh[bucket].cogs = roundMoney2(byCh[bucket].cogs + cog);
            const sk = skuCostKey(pid, vid);
            if (!bySku[sk]) bySku[sk] = { revenue: 0, cogs: 0, name: String(item.name || pid), quality };
            bySku[sk].revenue = roundMoney2(bySku[sk].revenue + rev);
            bySku[sk].cogs = roundMoney2(bySku[sk].cogs + cog);
            if (bySku[sk].quality === 'missing' && quality !== 'missing') bySku[sk].quality = quality;
        }
    }

    const grossProfit = roundMoney2(revenue - cogs);
    const marginPct = revenue > 0 ? roundMoney2((grossProfit / revenue) * 100) : 0;
    return { revenue, cogs, grossProfit, marginPct, bySku, byChannel: byCh };
}

/**
 * Shrinkage cost limited to ledger rows whose createdAt falls in [startMs,endMs].
 */
export function computeShrinkageCostEstimateFiltered(inventoryLedger, products, avgMap, startMs, endMs) {
    let shrink = 0;
    for (const ev of Object.values(inventoryLedger || {})) {
        if (!ev || typeof ev !== 'object') continue;
        const t = getLedgerEventMs(ev);
        if (startMs != null && (t == null || t < startMs)) continue;
        if (endMs != null && (t == null || t > endMs)) continue;
        const et = String(ev.eventType || '');
        const dq = Number(ev.qtyDelta) || 0;
        if (et === 'wastage') {
            const q = Math.abs(dq);
            const uc = ev.unitCost != null && !Number.isNaN(Number(ev.unitCost))
                ? Number(ev.unitCost)
                : resolveUnitCostForSku(avgMap, ev.productId, ev.variantId || null, products[ev.productId]).unitCost;
            shrink = roundMoney2(shrink + roundMoney2(q * uc));
        } else if (et === 'adjustment' && dq < 0) {
            const q = Math.abs(dq);
            const uc = ev.unitCost != null && !Number.isNaN(Number(ev.unitCost))
                ? Number(ev.unitCost)
                : resolveUnitCostForSku(avgMap, ev.productId, ev.variantId || null, products[ev.productId]).unitCost;
            shrink = roundMoney2(shrink + roundMoney2(q * uc));
        }
    }
    return shrink;
}

/**
 * Daily net line revenue + COGS between startMs and endMs (inclusive days in local iterator matching Phase 4 style).
 */
export function computeMarginByDateInRange(
    orders,
    avgMap,
    products,
    saleSnapshotMap,
    channelFilter,
    startMs,
    endMs,
    lineFilters = {}
) {
    const { categoryId = null, weightedOnly = false } = lineFilters;
    const start = new Date(startMs);
    const end = new Date(endMs);
    /** @type {Map<string, { revenue: number, cogs: number }>} */
    const map = new Map();
    const pad = (d) => d.toISOString().split('T')[0];
    const endDay = new Date(end);
    endDay.setHours(23, 59, 59, 999);
    const stop = new Date(endDay);
    stop.setHours(0, 0, 0, 0);
    let cursor = new Date(start);
    cursor.setHours(0, 0, 0, 0);
    while (cursor <= stop) {
        map.set(pad(cursor), { revenue: 0, cogs: 0 });
        cursor.setDate(cursor.getDate() + 1);
    }

    for (const order of orders || []) {
        if (!order) continue;
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const ms = getOrderCreatedAtMs(order);
        if (ms == null || ms < startMs || ms > endMs) continue;
        const od = new Date(ms);
        const key = pad(od);
        if (!map.has(key)) map.set(key, { revenue: 0, cogs: 0 });
        const items = Array.isArray(order.items) ? order.items : Object.values(order.items || {});
        for (const item of items) {
            if (!item || !item.id) continue;
            const p = products[item.id];
            if (!lineMatchesProductFilters(p, categoryId, weightedOnly)) continue;
            const netQ = netSoldQty(item, p);
            if (netQ <= 0) continue;
            const rev = roundMoney2(netQ * (Number(item.price) || 0));
            const { unitCost } = resolveUnitCostPhase5(avgMap, saleSnapshotMap, item.id, item.variantId || null, p);
            const cog = roundMoney2(netQ * unitCost);
            const cell = map.get(key);
            if (!cell) continue;
            cell.revenue = roundMoney2(cell.revenue + rev);
            cell.cogs = roundMoney2(cell.cogs + cog);
        }
    }
    return map;
}

export function pickHighShrinkageLedgerLines(inventoryLedger, avgMap, products, limit = 12, startMs = null, endMs = null) {
    const rows = [];
    for (const [id, ev] of Object.entries(inventoryLedger || {})) {
        if (!ev || typeof ev !== 'object') continue;
        const t = getLedgerEventMs(ev);
        if (startMs != null && (t == null || t < startMs)) continue;
        if (endMs != null && (t == null || t > endMs)) continue;
        const et = String(ev.eventType || '');
        const dq = Number(ev.qtyDelta) || 0;
        if (et !== 'wastage' && !(et === 'adjustment' && dq < 0)) continue;
        const q = Math.abs(dq);
        const uc = ev.unitCost != null && !Number.isNaN(Number(ev.unitCost))
            ? Number(ev.unitCost)
            : resolveUnitCostForSku(avgMap, ev.productId, ev.variantId || null, products[ev.productId]).unitCost;
        const cost = roundMoney2(q * uc);
        if (cost <= 0) continue;
        const pname = products[ev.productId]?.name || String(ev.productId);
        rows.push({
            id,
            name: pname,
            eventType: et,
            cost,
            qty: q,
            note: String(ev.note || '').slice(0, 80)
        });
    }
    return rows.sort((a, b) => b.cost - a.cost).slice(0, limit);
}

/**
 * @param {unknown} siteSettings
 */
export function readMarginAnalyticsThresholds(siteSettings) {
    const s = siteSettings && typeof siteSettings === 'object' ? siteSettings : {};
    const m = s.marginAnalytics && typeof s.marginAnalytics === 'object' ? s.marginAnalytics : {};
    return {
        lowMarginPct: Math.min(99, Math.max(0, Number(m.lowMarginPct) || 15)),
        minRevenueForLowMargin: Math.max(0, Number(m.minRevenueForLowMargin) || 200),
        shrinkageCostAlert: Math.max(0, Number(m.shrinkageCostAlert) || 5000)
    };
}
