import 'dotenv/config';
import { createClient } from '@supabase/supabase-js';

type CliOptions = {
    apply: boolean;
    includeInactive: boolean;
    includeQuarantinedHistory: boolean;
    profileIds: string[];
};

type ProfileRow = {
    id: string;
    name?: string | null;
    is_active?: boolean | null;
    allocated_capital?: number | string | null;
};

type LedgerRow = {
    profile_id: string;
    allocated_capital?: number | string | null;
    reserved_for_orders?: number | string | null;
    reserved_for_positions?: number | string | null;
    realized_pnl?: number | string | null;
    updated_at?: string | null;
};

type TradeHistoryRow = {
    pnl?: number | string | null;
    reason?: string | null;
};

type OrderRow = {
    symbol?: string | null;
    trade_id?: string | null;
    action?: string | null;
    side?: string | null;
    qty?: number | string | null;
    quantity?: number | string | null;
    price?: number | string | null;
    status?: string | null;
    timestamp?: number | string | null;
    created_at?: string | null;
};

type ProfileRepairReport = {
    profileId: string;
    profileName: string;
    isActive: boolean;
    allocatedCapital: number;
    ordersAnalyzed: number;
    historyRowsAnalyzed: number;
    openOrderRowsWithoutPrice: number;
    current: {
        allocatedCapital: number;
        reservedForOrders: number;
        reservedForPositions: number;
        realizedPnl: number;
    };
    target: {
        allocatedCapital: number;
        reservedForOrders: number;
        reservedForPositions: number;
        realizedPnl: number;
    };
    delta: {
        allocatedCapital: number;
        reservedForOrders: number;
        reservedForPositions: number;
        realizedPnl: number;
    };
    rawUtilizationBeforePct: number | null;
    rawUtilizationAfterPct: number | null;
    overAllocatedBefore: boolean;
    overAllocatedAfter: boolean;
    changed: boolean;
};

const PAGE_SIZE = 1000;
const EPSILON = 1e-8;

const OPEN_ORDER_STATUSES = new Set([
    'pending_new',
    'accepted',
    'pending',
    'new',
    'partially_filled',
    'partially-filled',
    'partiallyfilled',
    'partial_fill'
]);

const FILLED_STATUSES = new Set([
    'filled',
    'partially_filled',
    'partially-filled',
    'partiallyfilled',
    'partial_fill'
]);

const parseArgs = (argv: string[]): CliOptions => {
    const profileIds = new Set<string>();
    let apply = false;
    let includeInactive = false;
    let includeQuarantinedHistory = false;

    for (const raw of argv) {
        const arg = String(raw || '').trim();
        if (!arg) continue;
        if (arg === '--apply') {
            apply = true;
            continue;
        }
        if (arg === '--include-inactive') {
            includeInactive = true;
            continue;
        }
        if (arg === '--include-quarantined-history') {
            includeQuarantinedHistory = true;
            continue;
        }
        if (arg.startsWith('--profile=')) {
            const value = String(arg.slice('--profile='.length) || '').trim();
            if (value) profileIds.add(value);
        }
    }

    return {
        apply,
        includeInactive,
        includeQuarantinedHistory,
        profileIds: Array.from(profileIds)
    };
};

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

const round8 = (value: number): number => Number(value.toFixed(8));

const formatErrorPayload = (error: any): string => {
    if (!error) return 'unknown_error';
    if (error instanceof Error) {
        return JSON.stringify({
            message: error.message,
            stack: error.stack || null
        });
    }
    if (typeof error === 'object') {
        return JSON.stringify({
            message: String(error.message || ''),
            code: String(error.code || ''),
            details: String(error.details || ''),
            hint: String(error.hint || ''),
            raw: error
        });
    }
    return JSON.stringify({ message: String(error) });
};

const failIfError = (error: any, context: string): void => {
    if (!error) return;
    throw new Error(`${context}: ${formatErrorPayload(error)}`);
};

const normalizeStatus = (status: unknown): string => {
    const normalized = String(status || '').trim().toLowerCase();
    if (normalized === 'partially-filled') return 'partially_filled';
    if (normalized === 'partiallyfilled') return 'partially_filled';
    if (normalized === 'partial_fill') return 'partially_filled';
    return normalized;
};

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

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

const inferAction = (row: OrderRow, knownEntrySide?: 'BUY' | 'SELL'): 'ENTRY' | 'EXIT' => {
    const explicit = normalizeAction(row.action);
    if (explicit) return explicit;

    const side = normalizeSide(row.side);
    if (knownEntrySide) {
        return side === knownEntrySide ? 'ENTRY' : 'EXIT';
    }
    return side === 'BUY' ? 'ENTRY' : 'EXIT';
};

const normalizeExecutionScopeSymbol = (symbol: unknown, provider: string): string => {
    const raw = String(symbol || '')
        .trim()
        .toUpperCase()
        .replace(/[\/_\-\s]/g, '');
    if (!raw) return '';
    if (provider === 'alpaca' && raw.endsWith('USDT')) {
        return `${raw.slice(0, -4)}USD`;
    }
    return raw;
};

const toTimestamp = (row: OrderRow, fallback: number): number => {
    const ts = Number(row.timestamp);
    if (Number.isFinite(ts) && ts > 0) {
        return ts > 1_000_000_000_000 ? ts : ts * 1000;
    }
    const created = Date.parse(String(row.created_at || ''));
    if (Number.isFinite(created) && created > 0) return created;
    return fallback;
};

const isQuarantinedHistoryReason = (reason: unknown): boolean => {
    const normalized = String(reason || '').trim().toUpperCase();
    return normalized.startsWith('[INVALID_')
        || normalized.startsWith('[DUPLICATE_')
        || normalized.startsWith('[RECONCILED_TO_');
};

const fetchPagedByProfile = async <T>(
    supabase: any,
    table: 'orders' | 'trade_history',
    columns: string,
    profileId: string
): Promise<T[]> => {
    const rows: T[] = [];
    let offset = 0;

    for (;;) {
        const { data, error } = await supabase
            .from(table)
            .select(columns)
            .eq('profile_id', profileId)
            .order('created_at', { ascending: true })
            .range(offset, offset + PAGE_SIZE - 1);

        failIfError(error, `fetchPagedByProfile:${table}:${profileId}`);
        const chunk = (data || []) as T[];
        if (!chunk.length) break;
        rows.push(...chunk);
        if (chunk.length < PAGE_SIZE) break;
        offset += PAGE_SIZE;
    }

    return rows;
};

const fetchOrdersForProfile = async (
    supabase: any,
    profileId: string
): Promise<OrderRow[]> => {
    const withQuantity = 'symbol,trade_id,action,side,qty,quantity,price,status,timestamp,created_at';
    const withoutQuantity = 'symbol,trade_id,action,side,qty,price,status,timestamp,created_at';

    try {
        return await fetchPagedByProfile<OrderRow>(supabase, 'orders', withQuantity, profileId);
    } catch (error: any) {
        const message = String(error?.message || '').toLowerCase();
        if (message.includes('column') && message.includes('quantity')) {
            return await fetchPagedByProfile<OrderRow>(supabase, 'orders', withoutQuantity, profileId);
        }
        throw error;
    }
};

const computeReservedForOpenEntryOrders = (orders: OrderRow[]): {
    reserved: number;
    rowsWithoutPrice: number;
} => {
    let reserved = 0;
    let rowsWithoutPrice = 0;

    for (const row of orders) {
        const status = normalizeStatus(row.status);
        if (!OPEN_ORDER_STATUSES.has(status)) continue;

        const action = inferAction(row);
        if (action !== 'ENTRY') continue;

        const qty = toNumber(row.qty);
        const quantity = qty > 0 ? qty : toNumber(row.quantity);
        if (!(quantity > 0)) continue;

        const price = toNumber(row.price);
        if (!(price > 0)) {
            rowsWithoutPrice += 1;
            continue;
        }

        reserved += quantity * price;
    }

    return {
        reserved: round8(reserved),
        rowsWithoutPrice
    };
};

const computeOpenPositionNotional = (orders: OrderRow[], provider: string): number => {
    type TradeLedger = {
        side: 'BUY' | 'SELL';
        entryQty: number;
        entryNotional: number;
        entryLastPrice: number;
        exitQty: number;
    };

    const byExecutionSymbol = new Map<string, Array<{ row: OrderRow; ts: number; idx: number }>>();
    let rowIndex = 0;

    for (const row of orders) {
        const status = normalizeStatus(row.status);
        if (!FILLED_STATUSES.has(status)) continue;

        const qty = toNumber(row.qty);
        const quantity = qty > 0 ? qty : toNumber(row.quantity);
        if (!(quantity > 0)) continue;

        const symbolKey = normalizeExecutionScopeSymbol(row.symbol, provider);
        if (!symbolKey) continue;

        const bucket = byExecutionSymbol.get(symbolKey) || [];
        bucket.push({
            row,
            ts: toTimestamp(row, rowIndex),
            idx: rowIndex
        });
        byExecutionSymbol.set(symbolKey, bucket);
        rowIndex += 1;
    }

    let reservedNotional = 0;

    for (const [symbolKey, bucket] of Array.from(byExecutionSymbol.entries())) {
        const ordered = [...bucket].sort((a, b) => (a.ts - b.ts) || (a.idx - b.idx));
        const ledgerByTrade = new Map<string, TradeLedger>();
        const entrySideByTrade = new Map<string, 'BUY' | 'SELL'>();
        const openQueueBySide: Record<'BUY' | 'SELL', string[]> = { BUY: [], SELL: [] };
        let syntheticCounter = 0;

        const buildSyntheticTradeId = (side: 'BUY' | 'SELL', ts: number): string => {
            syntheticCounter += 1;
            const tsToken = Number.isFinite(ts) && ts > 0 ? Math.trunc(ts) : syntheticCounter;
            return `__legacy__-${symbolKey}-${side}-${tsToken}-${String(syntheticCounter).padStart(4, '0')}`;
        };

        for (const wrapped of ordered) {
            const row = wrapped.row;
            const qtyRaw = toNumber(row.qty);
            const qty = qtyRaw > 0 ? qtyRaw : toNumber(row.quantity);
            if (!(qty > 0)) continue;

            const rowSide = normalizeSide(row.side);
            const oppositeSide: 'BUY' | 'SELL' = rowSide === 'BUY' ? 'SELL' : 'BUY';
            const explicitAction = normalizeAction(row.action);
            let action = explicitAction;
            let tradeId = String(row.trade_id || '').trim();

            if (!action && !tradeId) {
                action = openQueueBySide[oppositeSide].length > 0 ? 'EXIT' : 'ENTRY';
            }

            if (!tradeId) {
                if (action === 'EXIT' && openQueueBySide[oppositeSide].length > 0) {
                    tradeId = openQueueBySide[oppositeSide][0];
                } else {
                    tradeId = buildSyntheticTradeId(action === 'EXIT' ? oppositeSide : rowSide, wrapped.ts);
                }
            }

            if (!action) {
                action = inferAction(row, entrySideByTrade.get(tradeId));
            }

            let tradeLedger = ledgerByTrade.get(tradeId);
            if (!tradeLedger) {
                tradeLedger = {
                    side: rowSide,
                    entryQty: 0,
                    entryNotional: 0,
                    entryLastPrice: 0,
                    exitQty: 0
                };
                ledgerByTrade.set(tradeId, tradeLedger);
            }

            if (action === 'ENTRY') {
                if (tradeLedger.entryQty <= EPSILON) {
                    tradeLedger.side = rowSide;
                }
                tradeLedger.entryQty += qty;
                entrySideByTrade.set(tradeId, tradeLedger.side);
                if (!openQueueBySide[tradeLedger.side].includes(tradeId)) {
                    openQueueBySide[tradeLedger.side].push(tradeId);
                }

                const price = toNumber(row.price);
                if (price > 0) {
                    tradeLedger.entryNotional += price * qty;
                    tradeLedger.entryLastPrice = price;
                }
            } else {
                tradeLedger.exitQty += qty;
                const queue = openQueueBySide[oppositeSide];
                const idx = queue.findIndex((queuedTradeId) => queuedTradeId === tradeId);
                if (idx >= 0) {
                    queue.splice(idx, 1);
                } else if (queue.length > 0) {
                    queue.shift();
                }
            }
        }

        for (const tradeLedger of Array.from(ledgerByTrade.values())) {
            const remainingQty = tradeLedger.entryQty - tradeLedger.exitQty;
            if (!(remainingQty > EPSILON)) continue;

            const weightedEntryPrice = tradeLedger.entryQty > EPSILON && tradeLedger.entryNotional > EPSILON
                ? tradeLedger.entryNotional / tradeLedger.entryQty
                : tradeLedger.entryLastPrice;
            if (!(weightedEntryPrice > 0)) continue;

            reservedNotional += remainingQty * weightedEntryPrice;
        }
    }

    return round8(reservedNotional);
};

const run = async (): Promise<void> => {
    const options = parseArgs(process.argv.slice(2));

    const supabaseUrl = String(process.env.SUPABASE_URL || '').trim();
    const supabaseKey = String(
        process.env.SUPABASE_KEY
        || process.env.SUPABASE_SERVICE_ROLE_KEY
        || process.env.SUPABASE_ANON_KEY
        || ''
    ).trim();
    if (!supabaseUrl || !supabaseKey) {
        throw new Error('Missing Supabase credentials. Expected SUPABASE_URL + SUPABASE_KEY/SUPABASE_SERVICE_ROLE_KEY.');
    }

    const provider = String(process.env.EXECUTION_PROVIDER || process.env.PROVIDER || 'alpaca').trim().toLowerCase() || 'alpaca';
    const supabase = createClient(supabaseUrl, supabaseKey);

    let profileQuery = supabase
        .from('trade_profiles')
        .select('id,name,is_active,allocated_capital')
        .order('created_at', { ascending: true });

    if (!options.includeInactive) {
        profileQuery = profileQuery.eq('is_active', true);
    }
    if (options.profileIds.length > 0) {
        profileQuery = profileQuery.in('id', options.profileIds);
    }

    const { data: profileData, error: profileError } = await profileQuery;
    failIfError(profileError, 'load_profiles');

    const profiles = (profileData || []) as ProfileRow[];
    const profileIds = profiles.map((profile) => String(profile.id || '').trim()).filter(Boolean);

    if (profileIds.length === 0) {
        console.log(JSON.stringify({
            mode: options.apply ? 'apply' : 'dry-run',
            provider,
            profilesProcessed: 0,
            message: 'No matching profiles found.'
        }, null, 2));
        return;
    }

    const { data: ledgerData, error: ledgerError } = await supabase
        .from('capital_ledgers')
        .select('profile_id,allocated_capital,reserved_for_orders,reserved_for_positions,realized_pnl,updated_at')
        .in('profile_id', profileIds);
    failIfError(ledgerError, 'load_capital_ledgers');

    const ledgerByProfile = new Map<string, LedgerRow>();
    for (const row of (ledgerData || []) as LedgerRow[]) {
        const profileId = String(row.profile_id || '').trim();
        if (!profileId) continue;
        ledgerByProfile.set(profileId, row);
    }

    const reports: ProfileRepairReport[] = [];

    for (const profile of profiles) {
        const profileId = String(profile.id || '').trim();
        if (!profileId) continue;

        const [historyRows, orders] = await Promise.all([
            fetchPagedByProfile<TradeHistoryRow>(supabase, 'trade_history', 'pnl,reason', profileId),
            fetchOrdersForProfile(supabase, profileId)
        ]);

        const realizedFromHistory = historyRows.reduce((sum, row) => {
            if (!options.includeQuarantinedHistory && isQuarantinedHistoryReason(row.reason)) {
                return sum;
            }
            return sum + toNumber(row.pnl);
        }, 0);

        const openOrders = computeReservedForOpenEntryOrders(orders);
        const reservedPositions = computeOpenPositionNotional(orders, provider);

        const allocatedCapital = toNumber(profile.allocated_capital);
        const currentLedger = ledgerByProfile.get(profileId);

        const currentAllocated = toNumber(currentLedger?.allocated_capital);
        const currentReservedOrders = toNumber(currentLedger?.reserved_for_orders);
        const currentReservedPositions = toNumber(currentLedger?.reserved_for_positions);
        const currentRealized = toNumber(currentLedger?.realized_pnl);

        const targetAllocated = round8(allocatedCapital);
        const targetReservedOrders = round8(openOrders.reserved);
        const targetReservedPositions = round8(reservedPositions);
        const targetRealized = round8(realizedFromHistory);

        const deltaAllocated = round8(targetAllocated - currentAllocated);
        const deltaReservedOrders = round8(targetReservedOrders - currentReservedOrders);
        const deltaReservedPositions = round8(targetReservedPositions - currentReservedPositions);
        const deltaRealized = round8(targetRealized - currentRealized);

        const changed = !currentLedger
            || Math.abs(deltaAllocated) > 0.01
            || Math.abs(deltaReservedOrders) > 0.01
            || Math.abs(deltaReservedPositions) > 0.01
            || Math.abs(deltaRealized) > 0.01;

        const utilizationBefore = currentAllocated > 0
            ? ((currentReservedOrders + currentReservedPositions) / currentAllocated) * 100
            : null;
        const utilizationAfter = targetAllocated > 0
            ? ((targetReservedOrders + targetReservedPositions) / targetAllocated) * 100
            : null;

        const overAllocatedBefore = currentAllocated > 0
            ? currentReservedOrders + currentReservedPositions > currentAllocated + EPSILON
            : false;
        const overAllocatedAfter = targetAllocated > 0
            ? targetReservedOrders + targetReservedPositions > targetAllocated + EPSILON
            : false;

        reports.push({
            profileId,
            profileName: String(profile.name || ''),
            isActive: Boolean(profile.is_active),
            allocatedCapital: targetAllocated,
            ordersAnalyzed: orders.length,
            historyRowsAnalyzed: historyRows.length,
            openOrderRowsWithoutPrice: openOrders.rowsWithoutPrice,
            current: {
                allocatedCapital: round8(currentAllocated),
                reservedForOrders: round8(currentReservedOrders),
                reservedForPositions: round8(currentReservedPositions),
                realizedPnl: round8(currentRealized)
            },
            target: {
                allocatedCapital: targetAllocated,
                reservedForOrders: targetReservedOrders,
                reservedForPositions: targetReservedPositions,
                realizedPnl: targetRealized
            },
            delta: {
                allocatedCapital: deltaAllocated,
                reservedForOrders: deltaReservedOrders,
                reservedForPositions: deltaReservedPositions,
                realizedPnl: deltaRealized
            },
            rawUtilizationBeforePct: utilizationBefore === null ? null : round8(utilizationBefore),
            rawUtilizationAfterPct: utilizationAfter === null ? null : round8(utilizationAfter),
            overAllocatedBefore,
            overAllocatedAfter,
            changed
        });
    }

    const changedReports = reports.filter((report) => report.changed);
    const payload = changedReports.map((report) => ({
        profile_id: report.profileId,
        allocated_capital: report.target.allocatedCapital,
        reserved_for_orders: report.target.reservedForOrders,
        reserved_for_positions: report.target.reservedForPositions,
        realized_pnl: report.target.realizedPnl,
        updated_at: new Date().toISOString()
    }));

    let appliedRows = 0;
    if (options.apply && payload.length > 0) {
        const { error: upsertError } = await supabase
            .from('capital_ledgers')
            .upsert(payload, { onConflict: 'profile_id' });
        failIfError(upsertError, 'upsert_capital_ledgers');
        appliedRows = payload.length;
    }

    const summary = {
        mode: options.apply ? 'apply' : 'dry-run',
        provider,
        includeInactive: options.includeInactive,
        includeQuarantinedHistory: options.includeQuarantinedHistory,
        profilesProcessed: reports.length,
        profilesChanged: changedReports.length,
        appliedRows,
        overAllocatedBeforeCount: reports.filter((report) => report.overAllocatedBefore).length,
        overAllocatedAfterCount: reports.filter((report) => report.overAllocatedAfter).length,
        totals: {
            realizedDeltaApplied: round8(changedReports.reduce((sum, report) => sum + report.delta.realizedPnl, 0)),
            reservedOrdersDeltaApplied: round8(changedReports.reduce((sum, report) => sum + report.delta.reservedForOrders, 0)),
            reservedPositionsDeltaApplied: round8(changedReports.reduce((sum, report) => sum + report.delta.reservedForPositions, 0))
        },
        reports: reports.sort((left, right) => {
            const leftScore = Math.abs(left.delta.realizedPnl) + Math.abs(left.delta.reservedForPositions);
            const rightScore = Math.abs(right.delta.realizedPnl) + Math.abs(right.delta.reservedForPositions);
            return rightScore - leftScore;
        })
    };

    console.log(JSON.stringify(summary, null, 2));
};

run().catch((error) => {
    console.error(JSON.stringify({
        error: formatErrorPayload(error)
    }, null, 2));
    process.exit(1);
});
