import type { FilledLifecycleOrderRow } from './SupabaseService.js';

export type CanonicalLifecycleState = 'OPEN' | 'PARTIAL_EXIT' | 'CLOSED' | 'ORPHAN_EXIT';
export type CanonicalSide = 'BUY' | 'SELL';

export interface CanonicalLifecycleProfileMeta {
    id: string;
    name: string;
    allocatedCapital: number;
    isActive: boolean;
    userId?: string;
}

export interface CanonicalLifecycleRow {
    id: string;
    profileId: string;
    profileName: string;
    tradeId: string;
    symbol: string;
    side: CanonicalSide;
    state: CanonicalLifecycleState;
    entryQty: number;
    exitQty: number;
    matchedQty: number;
    openQty: number;
    entryAvgPrice: number;
    exitAvgPrice: number;
    openEntryAvgPrice: number;
    openNotional: number;
    realizedPnl: number;
    realizedPnlPercent: number;
    unrealizedPnl: number;
    currentPrice: number;
    stopLoss?: number;
    takeProfit?: number;
    subTag?: string;
    hasSyntheticOrder: boolean;
    lastEventAt: number;
}

export interface CanonicalOpenPosition {
    id: string;
    profileId: string;
    profileName: string;
    tradeId: string;
    symbol: string;
    side: CanonicalSide;
    size: number;
    entryPrice: number;
    currentPrice: number;
    pnl: number;
    pnlPercent: number;
    stopLoss?: number;
    takeProfit?: number;
    subTag?: string;
    lastEventAt: number;
}

export interface CanonicalRealizedTrade {
    id: string;
    profileId: string;
    profileName: string;
    tradeId: string;
    symbol: string;
    side: CanonicalSide;
    size: number;
    entryPrice: number;
    exitPrice: number;
    pnl: number;
    pnlPercent: number;
    closedAt: number;
    state: CanonicalLifecycleState;
    subTag?: string;
}

export interface CanonicalLifecycleAggregate {
    profileId: string;
    profileName: string;
    allocatedCapital: number;
    isActive: boolean;
    openTrades: number;
    openNotional: number;
    realizedPnl: number;
    unrealizedPnl: number;
    netPnl: number;
    tradeCount: number;
    wins: number;
    winRate: number;
    lastClosedTradeAt: number;
}

export interface CanonicalLifecycleSnapshot {
    generatedAt: number;
    diagnostics: {
        orderRows: number;
        lifecycleRows: number;
        openPositions: number;
        realizedTrades: number;
        truncated: boolean;
        syntheticRealizedTradesExcluded?: number;
        syntheticRealizedPnlExcluded?: number;
    };
    profiles: CanonicalLifecycleProfileMeta[];
    lifecycleRows: CanonicalLifecycleRow[];
    openPositions: CanonicalOpenPosition[];
    realizedTrades: CanonicalRealizedTrade[];
    aggregates: {
        total: {
            openTrades: number;
            openNotional: number;
            realizedPnl: number;
            unrealizedPnl: number;
            netPnl: number;
            tradeCount: number;
            wins: number;
            winRate: number;
        };
        byProfile: Record<string, CanonicalLifecycleAggregate>;
    };
}

type BuildSnapshotInput = {
    orders: FilledLifecycleOrderRow[];
    profiles: CanonicalLifecycleProfileMeta[];
    symbolPrices: Record<string, number>;
    truncated?: boolean;
};

type LifecycleLot = {
    qty: number;
    price: number;
    stopLoss?: number;
    takeProfit?: number;
};

const EPSILON = 1e-8;
const SYNTHETIC_ORDER_PREFIXES = ['BFILL-', 'MANOVR-', 'RECON-BF', 'RECON-', 'SYNC-'];

const toNumber = (value: unknown): number => {
    const numeric = Number(value);
    return Number.isFinite(numeric) ? numeric : 0;
};

const normalizeStatus = (status?: string | null): string => {
    const normalized = String(status || '').trim().toLowerCase().replace(/-/g, '_');
    return normalized;
};

const normalizeSide = (side?: string | null): CanonicalSide => {
    const normalized = String(side || '').trim().toUpperCase();
    return normalized === 'SELL' || normalized === 'SHORT' ? 'SELL' : 'BUY';
};

const normalizeAction = (action?: string | null): 'ENTRY' | 'EXIT' | undefined => {
    const normalized = String(action || '').trim().toUpperCase();
    if (normalized === 'ENTRY' || normalized === 'EXIT') return normalized;
    return undefined;
};

const parseTimestampCandidate = (value: unknown): number => {
    if (typeof value === 'number') {
        if (!Number.isFinite(value) || value <= 0) return 0;
        return value > 1_000_000_000_000 ? value : value * 1000;
    }
    if (typeof value === 'string') {
        const trimmed = value.trim();
        if (!trimmed) return 0;
        if (/^\d+(\.\d+)?$/.test(trimmed)) {
            return parseTimestampCandidate(Number(trimmed));
        }
        const parsed = Date.parse(trimmed);
        if (Number.isFinite(parsed) && parsed > 0) return parsed;
    }
    return 0;
};

const normalizeTimestamp = (row: FilledLifecycleOrderRow): number => {
    // Backfill/synthetic rows can carry historical `filled_at` or stale `timestamp`
    // that predates the ENTRY for the same trade. Use the latest persisted event time
    // to preserve lifecycle causality inside each trade.
    const fromTimestamp = parseTimestampCandidate(row.timestamp);
    const fromCreatedAt = parseTimestampCandidate(row.created_at);
    const fromFilledAt = parseTimestampCandidate(row.filled_at);
    return Math.max(fromTimestamp, fromCreatedAt, fromFilledAt, 0);
};

const normalizeSymbolKey = (symbol: string): string => {
    return String(symbol || '')
        .toUpperCase()
        .replace(/[-_/]/g, '')
        .replace(/USDT/g, 'USD')
        .trim();
};

const normalizeOrderQty = (row: FilledLifecycleOrderRow): number => {
    const qty = toNumber(row.qty);
    if (qty > EPSILON) return qty;
    return toNumber(row.quantity);
};

const normalizeOrderPrice = (row: FilledLifecycleOrderRow): number => {
    return toNumber(row.price);
};

const isSyntheticOrderId = (orderIdRaw: unknown): boolean => {
    const normalized = String(orderIdRaw || '').trim().toUpperCase();
    if (!normalized) return false;
    return SYNTHETIC_ORDER_PREFIXES.some((prefix) => normalized.startsWith(prefix));
};

const isBotLifecycleOrder = (row: FilledLifecycleOrderRow): boolean => {
    const source = String(row.source || '').trim().toUpperCase();
    if (source === 'MANUAL') return false;
    const status = normalizeStatus(row.status);
    return status === 'filled' || status === 'partially_filled';
};

const buildSymbolPriceMap = (symbolPrices: Record<string, number>): Map<string, number> => {
    const map = new Map<string, number>();
    for (const [rawSymbol, rawPrice] of Object.entries(symbolPrices || {})) {
        const price = toNumber(rawPrice);
        if (price <= EPSILON) continue;
        const key = normalizeSymbolKey(rawSymbol);
        if (!key) continue;
        map.set(key, price);
    }
    return map;
};

const lookupCurrentPrice = (symbol: string, priceMap: Map<string, number>): number => {
    const key = normalizeSymbolKey(symbol);
    if (!key) return 0;
    return toNumber(priceMap.get(key) || 0);
};

export class CanonicalLifecycleService {
    buildSnapshot(input: BuildSnapshotInput): CanonicalLifecycleSnapshot {
        const profileById = new Map<string, CanonicalLifecycleProfileMeta>();
        for (const profile of input.profiles || []) {
            const profileId = String(profile.id || '').trim();
            if (!profileId) continue;
            profileById.set(profileId, profile);
        }

        const grouped = new Map<string, FilledLifecycleOrderRow[]>();
        for (const row of input.orders || []) {
            if (!isBotLifecycleOrder(row)) continue;

            const tradeId = String(row.trade_id || '').trim();
            const profileId = String(row.profile_id || '').trim();
            if (!tradeId || !profileId) continue;

            const qty = normalizeOrderQty(row);
            if (qty <= EPSILON) continue;

            const key = `${profileId}|${tradeId}`;
            const list = grouped.get(key) || [];
            list.push(row);
            grouped.set(key, list);
        }

        const priceMap = buildSymbolPriceMap(input.symbolPrices || {});
        const lifecycleRows: CanonicalLifecycleRow[] = [];
        const openPositions: CanonicalOpenPosition[] = [];
        const realizedTrades: CanonicalRealizedTrade[] = [];
        let syntheticRealizedTradesExcluded = 0;
        let syntheticRealizedPnlExcluded = 0;

        for (const [key, rows] of grouped.entries()) {
            const sorted = [...rows].sort((left, right) => {
                const leftTs = normalizeTimestamp(left);
                const rightTs = normalizeTimestamp(right);
                if (leftTs !== rightTs) return leftTs - rightTs;
                const leftOrderId = String(left.order_id || left.id || '');
                const rightOrderId = String(right.order_id || right.id || '');
                return leftOrderId.localeCompare(rightOrderId);
            });

            const [profileId, tradeId] = key.split('|');
            const profileMeta = profileById.get(profileId);
            const profileName = String(profileMeta?.name || profileId || 'Unknown Profile');

            let symbol = '';
            let entrySide: CanonicalSide | null = null;
            let entryQty = 0;
            let exitQty = 0;
            let entryNotional = 0;
            let exitNotional = 0;
            let matchedQty = 0;
            let matchedEntryNotional = 0;
            let matchedExitNotional = 0;
            let realizedPnl = 0;
            let lastEventAt = 0;
            let subTag = '';
            let hasSyntheticOrder = false;
            let fallbackStopLoss = 0;
            let fallbackTakeProfit = 0;
            const lots: LifecycleLot[] = [];

            for (const row of sorted) {
                const qty = normalizeOrderQty(row);
                if (qty <= EPSILON) continue;

                const side = normalizeSide(row.side);
                const price = normalizeOrderPrice(row);
                const action = normalizeAction(row.action)
                    || (side === 'SELL' ? 'EXIT' : 'ENTRY');
                const rowTs = normalizeTimestamp(row);
                lastEventAt = Math.max(lastEventAt, rowTs);

                if (!symbol) {
                    symbol = String(row.symbol || '').trim();
                }
                if (!entrySide && action === 'ENTRY') {
                    entrySide = side;
                }
                if (!subTag) {
                    const candidateTag = String(row.sub_tag || '').trim();
                    if (candidateTag) subTag = candidateTag;
                }
                if (!hasSyntheticOrder && isSyntheticOrderId(row.order_id || row.id)) {
                    hasSyntheticOrder = true;
                }

                const stopLoss = toNumber(row.stop_loss);
                const takeProfit = toNumber(row.take_profit);
                if (stopLoss > EPSILON) fallbackStopLoss = stopLoss;
                if (takeProfit > EPSILON) fallbackTakeProfit = takeProfit;

                if (action === 'ENTRY') {
                    const sideForEntry: CanonicalSide = entrySide ?? side;
                    entrySide = sideForEntry;
                    if (side !== sideForEntry) continue;

                    entryQty += qty;
                    if (price > EPSILON) entryNotional += qty * price;
                    lots.push({
                        qty,
                        price,
                        stopLoss: stopLoss > EPSILON ? stopLoss : undefined,
                        takeProfit: takeProfit > EPSILON ? takeProfit : undefined
                    });
                    continue;
                }

                const expectedExitSide: CanonicalSide = (entrySide || 'BUY') === 'BUY' ? 'SELL' : 'BUY';
                if (side !== expectedExitSide) continue;

                exitQty += qty;
                if (price > EPSILON) exitNotional += qty * price;

                let remaining = qty;
                while (remaining > EPSILON && lots.length > 0) {
                    const lot = lots[0];
                    const matched = Math.min(remaining, lot.qty);
                    if (matched <= EPSILON) break;

                    lot.qty -= matched;
                    remaining -= matched;
                    matchedQty += matched;

                    if (lot.price > EPSILON && price > EPSILON) {
                        matchedEntryNotional += matched * lot.price;
                        matchedExitNotional += matched * price;
                        realizedPnl += (entrySide || 'BUY') === 'BUY'
                            ? (price - lot.price) * matched
                            : (lot.price - price) * matched;
                    }

                    if (lot.qty <= EPSILON) {
                        lots.shift();
                    }
                }
            }

            const openQty = lots.reduce((sum, lot) => sum + Math.max(0, lot.qty), 0);
            const openNotional = lots.reduce((sum, lot) => (
                lot.price > EPSILON ? sum + (lot.qty * lot.price) : sum
            ), 0);
            const entryAvgPrice = matchedQty > EPSILON && matchedEntryNotional > EPSILON
                ? matchedEntryNotional / matchedQty
                : (entryQty > EPSILON && entryNotional > EPSILON ? entryNotional / entryQty : 0);
            const exitAvgPrice = matchedQty > EPSILON && matchedExitNotional > EPSILON
                ? matchedExitNotional / matchedQty
                : (exitQty > EPSILON && exitNotional > EPSILON ? exitNotional / exitQty : 0);
            const openEntryAvgPrice = openQty > EPSILON && openNotional > EPSILON
                ? openNotional / openQty
                : 0;

            const side: CanonicalSide = entrySide || 'BUY';
            const direction = side === 'SELL' ? -1 : 1;
            const currentPrice = lookupCurrentPrice(symbol, priceMap);
            const unrealizedPnl = openQty > EPSILON && openEntryAvgPrice > EPSILON && currentPrice > EPSILON
                ? (currentPrice - openEntryAvgPrice) * openQty * direction
                : 0;

            let state: CanonicalLifecycleState = 'CLOSED';
            if (entryQty <= EPSILON && exitQty > EPSILON) {
                state = 'ORPHAN_EXIT';
            } else if (openQty > EPSILON && matchedQty > EPSILON) {
                state = 'PARTIAL_EXIT';
            } else if (openQty > EPSILON) {
                state = 'OPEN';
            } else {
                state = 'CLOSED';
            }

            const realizedPnlPercent = entryAvgPrice > EPSILON
                ? (((exitAvgPrice - entryAvgPrice) / entryAvgPrice) * 100 * direction)
                : 0;

            let stopLoss = fallbackStopLoss > EPSILON ? fallbackStopLoss : undefined;
            let takeProfit = fallbackTakeProfit > EPSILON ? fallbackTakeProfit : undefined;
            for (const lot of lots) {
                if (!stopLoss && lot.stopLoss && lot.stopLoss > EPSILON) stopLoss = lot.stopLoss;
                if (!takeProfit && lot.takeProfit && lot.takeProfit > EPSILON) takeProfit = lot.takeProfit;
                if (stopLoss && takeProfit) break;
            }

            const lifecycleRow: CanonicalLifecycleRow = {
                id: `life:${profileId}:${tradeId}`,
                profileId,
                profileName,
                tradeId,
                symbol,
                side,
                state,
                entryQty: Number(entryQty.toFixed(8)),
                exitQty: Number(exitQty.toFixed(8)),
                matchedQty: Number(matchedQty.toFixed(8)),
                openQty: Number(openQty.toFixed(8)),
                entryAvgPrice: Number(entryAvgPrice.toFixed(8)),
                exitAvgPrice: Number(exitAvgPrice.toFixed(8)),
                openEntryAvgPrice: Number(openEntryAvgPrice.toFixed(8)),
                openNotional: Number((openQty * openEntryAvgPrice).toFixed(8)),
                realizedPnl: Number(realizedPnl.toFixed(8)),
                realizedPnlPercent: Number(realizedPnlPercent.toFixed(4)),
                unrealizedPnl: Number(unrealizedPnl.toFixed(8)),
                currentPrice: Number(currentPrice.toFixed(8)),
                stopLoss,
                takeProfit,
                subTag: subTag || undefined,
                hasSyntheticOrder,
                lastEventAt
            };
            lifecycleRows.push(lifecycleRow);

            if (openQty > EPSILON) {
                const pnlPercent = openEntryAvgPrice > EPSILON
                    ? (((currentPrice - openEntryAvgPrice) * direction) / openEntryAvgPrice) * 100
                    : 0;
                openPositions.push({
                    id: `open:${profileId}:${tradeId}`,
                    profileId,
                    profileName,
                    tradeId,
                    symbol,
                    side,
                    size: Number(openQty.toFixed(8)),
                    entryPrice: Number(openEntryAvgPrice.toFixed(8)),
                    currentPrice: Number(currentPrice.toFixed(8)),
                    pnl: Number(unrealizedPnl.toFixed(8)),
                    pnlPercent: Number(pnlPercent.toFixed(4)),
                    stopLoss,
                    takeProfit,
                    subTag: subTag || undefined,
                    lastEventAt
                });
            }

            if (matchedQty > EPSILON && !hasSyntheticOrder) {
                realizedTrades.push({
                    id: `realized:${profileId}:${tradeId}`,
                    profileId,
                    profileName,
                    tradeId,
                    symbol,
                    side,
                    size: Number(matchedQty.toFixed(8)),
                    entryPrice: Number(entryAvgPrice.toFixed(8)),
                    exitPrice: Number(exitAvgPrice.toFixed(8)),
                    pnl: Number(realizedPnl.toFixed(8)),
                    pnlPercent: Number(realizedPnlPercent.toFixed(4)),
                    closedAt: lastEventAt,
                    state,
                    subTag: subTag || undefined
                });
            }
        }

        lifecycleRows.sort((a, b) => b.lastEventAt - a.lastEventAt || a.id.localeCompare(b.id));
        openPositions.sort((a, b) => b.lastEventAt - a.lastEventAt || a.id.localeCompare(b.id));
        realizedTrades.sort((a, b) => b.closedAt - a.closedAt || a.id.localeCompare(b.id));

        const byProfile: Record<string, CanonicalLifecycleAggregate> = {};
        for (const profile of input.profiles || []) {
            const profileId = String(profile.id || '').trim();
            if (!profileId) continue;
            byProfile[profileId] = {
                profileId,
                profileName: profile.name,
                allocatedCapital: Number(profile.allocatedCapital || 0),
                isActive: Boolean(profile.isActive),
                openTrades: 0,
                openNotional: 0,
                realizedPnl: 0,
                unrealizedPnl: 0,
                netPnl: 0,
                tradeCount: 0,
                wins: 0,
                winRate: 0,
                lastClosedTradeAt: 0
            };
        }

        for (const row of lifecycleRows) {
            const profileId = row.profileId;
            if (!byProfile[profileId]) {
                byProfile[profileId] = {
                    profileId,
                    profileName: row.profileName,
                    allocatedCapital: 0,
                    isActive: false,
                    openTrades: 0,
                    openNotional: 0,
                    realizedPnl: 0,
                    unrealizedPnl: 0,
                    netPnl: 0,
                    tradeCount: 0,
                    wins: 0,
                    winRate: 0,
                    lastClosedTradeAt: 0
                };
            }
            const aggregate = byProfile[profileId];
            aggregate.openNotional += row.openNotional;
            aggregate.unrealizedPnl += row.unrealizedPnl;
            if (!row.hasSyntheticOrder) {
                aggregate.realizedPnl += row.realizedPnl;
            } else if (row.matchedQty > EPSILON) {
                syntheticRealizedTradesExcluded += 1;
                syntheticRealizedPnlExcluded += row.realizedPnl;
            }
            if (row.openQty > EPSILON) aggregate.openTrades += 1;
            if (row.matchedQty > EPSILON && !row.hasSyntheticOrder) {
                aggregate.tradeCount += 1;
                if (row.realizedPnl > 0) aggregate.wins += 1;
                aggregate.lastClosedTradeAt = Math.max(aggregate.lastClosedTradeAt, row.lastEventAt);
            }
        }

        let totalOpenTrades = 0;
        let totalOpenNotional = 0;
        let totalRealizedPnl = 0;
        let totalUnrealizedPnl = 0;
        let totalTrades = 0;
        let totalWins = 0;

        for (const aggregate of Object.values(byProfile)) {
            aggregate.openNotional = Number(aggregate.openNotional.toFixed(8));
            aggregate.realizedPnl = Number(aggregate.realizedPnl.toFixed(8));
            aggregate.unrealizedPnl = Number(aggregate.unrealizedPnl.toFixed(8));
            aggregate.netPnl = Number((aggregate.realizedPnl + aggregate.unrealizedPnl).toFixed(8));
            aggregate.winRate = aggregate.tradeCount > 0
                ? Number(((aggregate.wins / aggregate.tradeCount) * 100).toFixed(4))
                : 0;

            totalOpenTrades += aggregate.openTrades;
            totalOpenNotional += aggregate.openNotional;
            totalRealizedPnl += aggregate.realizedPnl;
            totalUnrealizedPnl += aggregate.unrealizedPnl;
            totalTrades += aggregate.tradeCount;
            totalWins += aggregate.wins;
        }

        const totalNetPnl = totalRealizedPnl + totalUnrealizedPnl;

        return {
            generatedAt: Date.now(),
            diagnostics: {
                orderRows: input.orders.length,
                lifecycleRows: lifecycleRows.length,
                openPositions: openPositions.length,
                realizedTrades: realizedTrades.length,
                truncated: Boolean(input.truncated),
                syntheticRealizedTradesExcluded,
                syntheticRealizedPnlExcluded: Number(syntheticRealizedPnlExcluded.toFixed(8))
            },
            profiles: input.profiles,
            lifecycleRows,
            openPositions,
            realizedTrades,
            aggregates: {
                total: {
                    openTrades: totalOpenTrades,
                    openNotional: Number(totalOpenNotional.toFixed(8)),
                    realizedPnl: Number(totalRealizedPnl.toFixed(8)),
                    unrealizedPnl: Number(totalUnrealizedPnl.toFixed(8)),
                    netPnl: Number(totalNetPnl.toFixed(8)),
                    tradeCount: totalTrades,
                    wins: totalWins,
                    winRate: totalTrades > 0
                        ? Number(((totalWins / totalTrades) * 100).toFixed(4))
                        : 0
                },
                byProfile
            }
        };
    }
}

export const canonicalLifecycleService = new CanonicalLifecycleService();
