/**
 * Phase 5 — reconciliation helpers, CSV export, action queue, date-range utilities.
 */

import { getOrderCreatedAtMs, classifyOrderChannel } from './order-time-channel.js';
import { readOrderAmountSnapshot } from './pos-finance.js';
import { roundMoney2 } from './pos-finance.js';
import {
    buildSkuAverageUnitCostMap,
    buildSkuLedgerSaleSnapshotCostMap,
    computeOrderDerivedMarginFiltered,
    readMarginAnalyticsThresholds,
    netSoldQtyForLine,
    skuCostKey
} from './margin-analytics.js';

/** Start of local calendar day → UTC ms (approximation for date inputs). */
export function dateInputToStartMs(isoDateStr) {
    if (!isoDateStr || typeof isoDateStr !== 'string') return null;
    const d = new Date(`${isoDateStr}T00:00:00`);
    const t = d.getTime();
    return Number.isNaN(t) ? null : t;
}

export function dateInputToEndMs(isoDateStr) {
    if (!isoDateStr || typeof isoDateStr !== 'string') return null;
    const d = new Date(`${isoDateStr}T23:59:59.999`);
    const t = d.getTime();
    return Number.isNaN(t) ? null : t;
}

/** Default window when date inputs are empty or invalid (local “last N days” using UTC date keys). */
export const ANALYTICS_DEFAULT_RANGE_DAYS = 30;

/**
 * Canonicalize analytics date inputs: fill missing ends, swap inverted ranges, fall back to last 30 days.
 * @param {string} fromStrIn
 * @param {string} toStrIn
 * @param {number} [defaultDays]
 * @returns {{ fromStr: string, toStr: string, startMs: number, endMs: number }}
 */
export function normalizeAnalyticsDateRange(fromStrIn, toStrIn, defaultDays = ANALYTICS_DEFAULT_RANGE_DAYS) {
    const pad = (d) => d.toISOString().split('T')[0];
    const now = new Date();
    const endDefault = pad(now);
    const startBase = new Date(now.getTime());
    startBase.setDate(startBase.getDate() - Math.max(1, Math.min(366, Number(defaultDays) || ANALYTICS_DEFAULT_RANGE_DAYS)));
    const startDefault = pad(startBase);

    let fs = typeof fromStrIn === 'string' ? fromStrIn.trim() : '';
    let ts = typeof toStrIn === 'string' ? toStrIn.trim() : '';

    if (!fs && !ts) {
        fs = startDefault;
        ts = endDefault;
    } else if (fs && !ts) {
        ts = fs;
    } else if (!fs && ts) {
        const mid = new Date(`${ts}T12:00:00`);
        if (Number.isNaN(mid.getTime())) {
            fs = startDefault;
            ts = endDefault;
        } else {
            const sb = new Date(mid.getTime());
            sb.setDate(sb.getDate() - Math.max(1, Math.min(366, Number(defaultDays) || ANALYTICS_DEFAULT_RANGE_DAYS)));
            fs = pad(sb);
        }
    }

    let sMs = dateInputToStartMs(fs);
    let eMs = dateInputToEndMs(ts);
    if (sMs == null || eMs == null || Number.isNaN(sMs) || Number.isNaN(eMs)) {
        fs = startDefault;
        ts = endDefault;
        sMs = dateInputToStartMs(fs);
        eMs = dateInputToEndMs(ts);
    }
    if (sMs > eMs) {
        const t = fs;
        fs = ts;
        ts = t;
        sMs = dateInputToStartMs(fs);
        eMs = dateInputToEndMs(ts);
    }
    return { fromStr: fs, toStr: ts, startMs: sMs, endMs: eMs };
}

/**
 * @param {object[]} orders
 * @param {'all'|'pos'|'online'} channelFilter
 * @param {number|null} startMs
 * @param {number|null} endMs
 */
export function computeBillTotalsFiltered(orders, channelFilter, startMs, endMs) {
    let billSum = 0;
    let orderCount = 0;
    for (const o of orders || []) {
        if (!o) continue;
        const ms = getOrderCreatedAtMs(o);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        const ch = classifyOrderChannel(o);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        billSum = roundMoney2(billSum + (Number(o.totalBill) || 0));
        orderCount += 1;
    }
    return { billSum, orderCount, aov: orderCount > 0 ? roundMoney2(billSum / orderCount) : 0 };
}

export function sumVatFiltered(orders, channelFilter, startMs, endMs) {
    let s = 0;
    for (const o of orders || []) {
        if (!o) continue;
        const ms = getOrderCreatedAtMs(o);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        const ch = classifyOrderChannel(o);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        s = roundMoney2(s + readOrderAmountSnapshot(o).tax);
    }
    return s;
}

export function sumRefundLedgerFiltered(orders, channelFilter, startMs, endMs) {
    let s = 0;
    for (const o of orders || []) {
        if (!o) continue;
        const ms = getOrderCreatedAtMs(o);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        const ch = classifyOrderChannel(o);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const led = o.paymentLedger;
        if (!Array.isArray(led)) continue;
        for (const e of led) {
            if (e && e.kind === 'refund' && Number(e.refundAmount)) s = roundMoney2(s + Number(e.refundAmount));
        }
    }
    return s;
}

/** Sum all refund entries on a single order (ignores date filter). */
export function sumRefundAmountOnOrder(order) {
    if (!order) return 0;
    const led = order.paymentLedger;
    if (!Array.isArray(led)) return 0;
    let s = 0;
    for (const e of led) {
        if (e && e.kind === 'refund' && Number(e.refundAmount)) s = roundMoney2(s + Number(e.refundAmount));
    }
    return s;
}

/** totalBill minus cumulative refund ledger on that order */
export function orderNetBillAmount(order) {
    return roundMoney2((Number(order?.totalBill) || 0) - sumRefundAmountOnOrder(order));
}

/**
 * Gross bill totals minus refunds attributed to orders in the same date/channel window.
 */
export function computeNetBillTotalsFiltered(orders, channelFilter, startMs, endMs) {
    const gross = computeBillTotalsFiltered(orders, channelFilter, startMs, endMs);
    const refundSum = sumRefundLedgerFiltered(orders, channelFilter, startMs, endMs);
    const netBillSum = roundMoney2(gross.billSum - refundSum);
    const netAov = gross.orderCount > 0 ? roundMoney2(netBillSum / gross.orderCount) : 0;
    return {
        ...gross,
        refundSum,
        netBillSum,
        netAov
    };
}

/**
 * Per-channel gross totals and refund sums (refunds on orders in that channel within the window).
 */
export function computeChannelNetTotalsFiltered(orders, startMs, endMs) {
    let posGross = 0;
    let posRef = 0;
    let posN = 0;
    let onGross = 0;
    let onRef = 0;
    let onN = 0;
    let unGross = 0;
    let unRef = 0;
    let unN = 0;
    for (const o of orders || []) {
        if (!o) continue;
        const ms = getOrderCreatedAtMs(o);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        const ch = classifyOrderChannel(o);
        const gross = Number(o.totalBill) || 0;
        const rf = sumRefundAmountOnOrder(o);
        if (ch === 'pos') {
            posGross = roundMoney2(posGross + gross);
            posRef = roundMoney2(posRef + rf);
            posN += 1;
        } else if (ch === 'online') {
            onGross = roundMoney2(onGross + gross);
            onRef = roundMoney2(onRef + rf);
            onN += 1;
        } else {
            unGross = roundMoney2(unGross + gross);
            unRef = roundMoney2(unRef + rf);
            unN += 1;
        }
    }
    return {
        posGross,
        posRefunds: posRef,
        posNet: roundMoney2(posGross - posRef),
        posOrders: posN,
        onlineGross: onGross,
        onlineRefunds: onRef,
        onlineNet: roundMoney2(onGross - onRef),
        onlineOrders: onN,
        unknownGross: unGross,
        unknownRefunds: unRef,
        unknownNet: roundMoney2(unGross - unRef),
        unknownOrders: unN
    };
}

export function computeChannelTotalsFiltered(orders, startMs, endMs) {
    let posSales = 0;
    let posOrders = 0;
    let onlineSales = 0;
    let onlineOrders = 0;
    let unknownSales = 0;
    let unknownOrders = 0;
    for (const o of orders || []) {
        if (!o) continue;
        const ms = getOrderCreatedAtMs(o);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        const amt = Number(o.totalBill) || 0;
        const ch = classifyOrderChannel(o);
        if (ch === 'pos') {
            posSales = roundMoney2(posSales + amt);
            posOrders += 1;
        } else if (ch === 'online') {
            onlineSales = roundMoney2(onlineSales + amt);
            onlineOrders += 1;
        } else {
            unknownSales = roundMoney2(unknownSales + amt);
            unknownOrders += 1;
        }
    }
    return { posSales, posOrders, onlineSales, onlineOrders, unknownSales, unknownOrders };
}

/**
 * Day-close reconciliation for a single local day (via start/end ms for that day).
 */
export function computeDayCloseReconciliation(orders, products, purchases, ledger, dayStartMs, dayEndMs, channelFilter = 'all') {
    const { avgMap } = buildSkuAverageUnitCostMap(purchases, ledger);
    const snap = buildSkuLedgerSaleSnapshotCostMap(ledger, dayStartMs, dayEndMs);
    const ch = channelFilter === 'pos' || channelFilter === 'online' ? channelFilter : 'all';
    const m = computeOrderDerivedMarginFiltered(orders, products, avgMap, snap, ch, {
        startMs: dayStartMs,
        endMs: dayEndMs,
        categoryId: null,
        weightedOnly: false
    });
    const bill = computeBillTotalsFiltered(orders, ch, dayStartMs, dayEndMs);
    const vat = sumVatFiltered(orders, ch, dayStartMs, dayEndMs);
    const refunds = sumRefundLedgerFiltered(orders, ch, dayStartMs, dayEndMs);
    const variance = roundMoney2(bill.billSum - m.revenue);
    return {
        billTotal: bill.billSum,
        orderCount: bill.orderCount,
        lineNetRevenue: m.revenue,
        vatSum: vat,
        refundLedgerSum: refunds,
        cogs: m.cogs,
        grossProfit: m.grossProfit,
        marginPct: m.marginPct,
        varianceBillVsLine: variance
    };
}

/**
 * @param {unknown} siteSettings
 */
export function readPhase5Thresholds(siteSettings) {
    const base = readMarginAnalyticsThresholds(siteSettings);
    const s = siteSettings && typeof siteSettings === 'object' ? siteSettings : {};
    const m = s.marginAnalytics && typeof s.marginAnalytics === 'object' ? s.marginAnalytics : {};
    const p5 = m.phase5 && typeof m.phase5 === 'object' ? m.phase5 : {};
    return {
        ...base,
        reconVariancePct: Math.min(50, Math.max(0, Number(p5.reconVariancePct) || 2)),
        shrinkRepeatMinCost: Math.max(0, Number(p5.shrinkRepeatMinCost) || 500)
    };
}

/**
 * @param {object} ctx
 */
export function buildPrioritizedActionQueue(ctx) {
    const {
        belowCostRows = [],
        lowMarginRows = [],
        shrinkRows = [],
        reconVariancePct = 2,
        billTotal = 0,
        varianceBillVsLine = 0
    } = ctx;
    /** @type {{ id: string, severity: number, type: string, title: string, detail: string, links: { label: string, href: string }[] }[]} */
    const items = [];
    let i = 0;
    const add = (type, severity, title, detail, links) => {
        i += 1;
        items.push({ id: `${type}-${i}`, type, severity, title, detail, links: links || [] });
    };

    for (const r of belowCostRows.slice(0, 20)) {
        add(
            'below_cost',
            1,
            `Below-cost: ${r.name}`,
            `Margin ${Number(r.marginPct).toFixed(1)}% · Rev ${roundMoney2(r.revenue)} · COGS ${roundMoney2(r.cogs)}`,
            [
                { label: 'Purchase', href: 'purchase-add.html' },
                { label: 'Inventory', href: 'inventory.html' }
            ]
        );
    }
    for (const r of lowMarginRows.slice(0, 15)) {
        add(
            'low_margin',
            3,
            `Low margin: ${r.name}`,
            `${Number(r.marginPct).toFixed(1)}% · Rev ${roundMoney2(r.revenue)}`,
            [
                { label: 'Purchase', href: 'purchase-add.html' },
                { label: 'Inventory', href: 'inventory.html' }
            ]
        );
    }
    for (const r of shrinkRows.slice(0, 10)) {
        add(
            'shrinkage',
            4,
            `Shrinkage: ${r.name}`,
            `${r.eventType} · ${roundMoney2(r.cost)} ৳`,
            [{ label: 'Inventory', href: 'inventory.html' }]
        );
    }

    if (billTotal > 200 && Math.abs(varianceBillVsLine) > 0.01) {
        const pct = billTotal > 0 ? Math.abs((varianceBillVsLine / billTotal) * 100) : 0;
        if (pct >= reconVariancePct) {
            add(
                'recon_variance',
                2,
                'Bill vs line revenue variance',
                `${pct.toFixed(2)}% (${roundMoney2(varianceBillVsLine)} ৳) — see reconciliation panel`,
                []
            );
        }
    }

    return items.sort((a, b) => a.severity - b.severity);
}

export function actionQueueStorageKey(clientId) {
    return `phase5_action_queue:${String(clientId || 'default')}`;
}

export function loadActionQueueState(clientId) {
    try {
        const raw = localStorage.getItem(actionQueueStorageKey(clientId));
        if (!raw) return { notes: {}, owners: {} };
        return JSON.parse(raw);
    } catch {
        return { notes: {}, owners: {} };
    }
}

export function saveActionQueueState(clientId, state) {
    localStorage.setItem(actionQueueStorageKey(clientId), JSON.stringify(state));
}

export function toCsvRow(cells) {
    return cells.map((c) => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',');
}

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 {Record<string, { revenue: number, cogs: number, name: string, quality?: string }>} bySku
 */
export function buildMarginSkuExportRows(bySku) {
    const rows = [['skuKey', 'name', 'revenue', 'cogs', 'grossProfit', 'marginPct', 'quality']];
    for (const [k, v] of Object.entries(bySku || {})) {
        if (!v) continue;
        const gp = roundMoney2(v.revenue - v.cogs);
        const mp = v.revenue > 0 ? roundMoney2((gp / v.revenue) * 100) : 0;
        rows.push([k, v.name, v.revenue, v.cogs, gp, mp, v.quality || '']);
    }
    return rows.map((r) => toCsvRow(r)).join('\n');
}

/**
 * Sales totalBill (or net bill when opts.useNetBill) per calendar day (UTC date key) within range.
 * @param {object[]} orders
 * @param {'all'|'pos'|'online'} channelFilter
 * @param {number|null} startMs
 * @param {number|null} endMs
 * @param {{ useNetBill?: boolean }} [opts]
 */
export function processSalesByDateInRange(orders, channelFilter, startMs, endMs, opts = {}) {
    const useNetBill = !!opts.useNetBill;
    const salesMap = new Map();
    if (startMs == null || endMs == null || Number.isNaN(Number(startMs)) || Number.isNaN(Number(endMs))) {
        return salesMap;
    }
    let sMs = Number(startMs);
    let eMs = Number(endMs);
    if (sMs > eMs) {
        const t = sMs;
        sMs = eMs;
        eMs = t;
    }
    const pad = (d) => d.toISOString().split('T')[0];
    const start = new Date(sMs);
    start.setHours(0, 0, 0, 0);
    const end = new Date(eMs);
    end.setHours(0, 0, 0, 0);
    let cursor = new Date(start);
    while (cursor <= end) {
        salesMap.set(pad(cursor), 0);
        cursor.setDate(cursor.getDate() + 1);
    }
    for (const order of orders || []) {
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const ms = getOrderCreatedAtMs(order);
        if (ms == null || ms < sMs || ms > eMs) continue;
        const dateString = pad(new Date(ms));
        const bill = useNetBill ? orderNetBillAmount(order) : Number(order.totalBill) || 0;
        salesMap.set(dateString, roundMoney2((salesMap.get(dateString) || 0) + bill));
    }
    return new Map([...salesMap.entries()].sort());
}

/**
 * Top products by net qty / line revenue with same filters as margin.
 */
export function processTopProductsFiltered(orders, products, channelFilter, filters = {}) {
    const { startMs = null, endMs = null, categoryId = null, weightedOnly = false } = filters;
    const productStats = {};
    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 (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        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 = netSoldQtyForLine(item, p);
            if (netQ <= 0) continue;
            if (!productStats[item.id]) {
                productStats[item.id] = { id: item.id, name: item.name, quantity: 0, revenue: 0 };
            }
            productStats[item.id].quantity = roundMoney2(productStats[item.id].quantity + netQ);
            productStats[item.id].revenue = roundMoney2(productStats[item.id].revenue + netQ * (Number(item.price) || 0));
        }
    }
    const statsArray = Object.values(productStats);
    const topSelling = [...statsArray].sort((a, b) => b.quantity - a.quantity).slice(0, 5);
    const topEarning = [...statsArray].sort((a, b) => b.revenue - a.revenue).slice(0, 5);
    return { topSelling, topEarning };
}

/**
 * Restock hints: fast movers with thin stock vs low stock regardless of velocity.
 * @param {object[]} orders
 * @param {Record<string, object>} products
 * @param {'all'|'pos'|'online'} channelFilter
 * @param {{ startMs?: number|null, endMs?: number|null, categoryId?: string|null, weightedOnly?: boolean }} filters
 * @returns {{ kind: 'fast_mover'|'low_stock', productId: string, name: string, unitsInWindow: number, stockQty: number|null, runwayDays: number|null, note: string }[]}
 */
export function buildRestockHintRows(orders, products, channelFilter, filters = {}) {
    const { startMs = null, endMs = null } = filters;
    const { topSelling } = processTopProductsFiltered(orders, products, channelFilter, {
        ...filters,
        startMs,
        endMs
    });
    const windowMs =
        startMs != null && endMs != null && !Number.isNaN(Number(startMs)) && !Number.isNaN(Number(endMs))
            ? Math.max(86400000, Number(endMs) - Number(startMs))
            : 86400000 * 30;
    const windowDays = Math.max(1, windowMs / 86400000);

    /** @type {Map<string, { productId: string, name: string, unitsInWindow: number, stockQty: number|null, unlimited: boolean }>} */
    const byId = new Map();
    for (const row of topSelling.slice(0, 40)) {
        const p = products[row.id];
        if (!p) continue;
        let stockQty = null;
        let unlimited = false;
        if (p.hasVariants && p.variants && typeof p.variants === 'object') {
            let sum = 0;
            let any = false;
            for (const v of Object.values(p.variants)) {
                if (!v) continue;
                const s = v.stock;
                if (s === null || s === undefined || s === '') {
                    unlimited = true;
                    break;
                }
                const n = p.isWeighted ? parseFloat(s) : parseInt(s, 10);
                if (!Number.isNaN(n)) {
                    sum += n;
                    any = true;
                }
            }
            if (!unlimited && any) stockQty = roundMoney2(sum);
        } else {
            const s = p.stock;
            if (s === null || s === undefined || s === '') unlimited = true;
            else {
                const n = p.isWeighted ? parseFloat(s) : parseInt(s, 10);
                stockQty = Number.isNaN(n) ? 0 : n;
            }
        }
        byId.set(row.id, {
            productId: row.id,
            name: String(row.name || row.id),
            unitsInWindow: row.quantity,
            stockQty: unlimited ? null : stockQty,
            unlimited
        });
    }

    const out = [];
    for (const v of byId.values()) {
        if (v.unlimited) continue;
        const perDay = v.unitsInWindow / windowDays;
        const stock = v.stockQty != null ? v.stockQty : 0;
        const runway = perDay > 0.001 ? roundMoney2(stock / perDay) : null;
        if (stock <= 0) {
            out.push({
                kind: 'low_stock',
                productId: v.productId,
                name: v.name,
                unitsInWindow: v.unitsInWindow,
                stockQty: stock,
                runwayDays: runway,
                note: 'Out of stock — reorder'
            });
        } else if (perDay >= 1 && runway != null && runway < 7) {
            out.push({
                kind: 'fast_mover',
                productId: v.productId,
                name: v.name,
                unitsInWindow: v.unitsInWindow,
                stockQty: stock,
                runwayDays: runway,
                note: `Fast mover — ~${runway.toFixed(1)} d runway at current pace`
            });
        } else if (stock > 0 && stock < 5 && perDay < 0.2) {
            out.push({
                kind: 'low_stock',
                productId: v.productId,
                name: v.name,
                unitsInWindow: v.unitsInWindow,
                stockQty: stock,
                runwayDays: runway,
                note: 'Low on-hand — slow mover'
            });
        }
    }
    const rank = { low_stock: 0, fast_mover: 1 };
    return out.sort((a, b) => {
        const ra = rank[a.kind] ?? 9;
        const rb = rank[b.kind] ?? 9;
        if (ra !== rb) return ra - rb;
        return (a.runwayDays ?? 999) - (b.runwayDays ?? 999);
    }).slice(0, 24);
}

/**
 * Evidence: orders contributing to a SKU key in the current filters.
 * @param {Record<string, object>} ordersDataRecord `clients/.../orders` map (keys = order ids)
 * @returns {{ orderKey: string, channel: string, lineRev: number, netQty: number }[]}
 */
export function collectSkuOrderEvidence(ordersDataRecord, products, skuKey, channelFilter, filters = {}) {
    const { startMs = null, endMs = null, categoryId = null, weightedOnly = false } = filters;
    const out = [];
    for (const [orderKey, order] of Object.entries(ordersDataRecord || {})) {
        if (!order || typeof order !== 'object') continue;
        const ch = classifyOrderChannel(order);
        if (channelFilter === 'pos' && ch !== 'pos') continue;
        if (channelFilter === 'online' && ch !== 'online') continue;
        const ms = getOrderCreatedAtMs(order);
        if (startMs != null && (ms == null || ms < startMs)) continue;
        if (endMs != null && (ms == null || ms > endMs)) continue;
        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 k = skuCostKey(item.id, item.variantId || null);
            if (k !== skuKey) continue;
            const netQ = netSoldQtyForLine(item, p);
            if (netQ <= 0) continue;
            const lineRev = roundMoney2(netQ * (Number(item.price) || 0));
            out.push({ orderKey: String(orderKey), channel: ch, lineRev, netQty: netQ });
        }
    }
    return out.sort((a, b) => b.lineRev - a.lineRev).slice(0, 25);
}
