import { SignalDirection, type MarketContext, type RuleResult } from '../../strategies/rules/types.js';
import { RiskEngine } from '../../services/riskEngine.js';
import type { Candle } from '../../connectors/types.js';
import type {
    BacktestExecutionConfig,
    BacktestIntraCandlePolicy,
    BacktestOpenPositionAtEnd,
    BacktestTrade,
    BacktestTimeframe
} from '../types.js';
import { VirtualLedger } from './VirtualLedger.js';

interface VirtualPosition {
    id: string;
    symbol: string;
    side: SignalDirection.BUY | SignalDirection.SELL;
    size: number;
    entryPrice: number;
    entryNotional: number;
    entryFeeUsd: number;
    stopLoss: number;
    takeProfit: number;
    peakPrice: number;
    entryTimestamp: number;
    entryReason: string;
}

interface ProcessTickInput {
    symbol: string;
    timestamp: number;
    timeframe: BacktestTimeframe;
    candle: Candle;
    intraCandles?: Candle[];
    strategyResult: RuleResult | null;
    context: MarketContext | null;
    profileSettings?: any;
}

interface PendingEntryIntent {
    symbol: string;
    side: SignalDirection.BUY | SignalDirection.SELL;
    qty: number;
    orderType: 'market' | 'limit';
    limitPrice: number;
    stopLoss: number;
    takeProfit: number;
    queuedAt: number;
    entryReason: string;
}

interface PendingExitIntent {
    reason: 'SIGNAL_FLIP' | 'NEUTRAL_EXIT';
    qty: number;
    queuedAt: number;
}

const round = (value: number, precision: number = 8): number => {
    const factor = Math.pow(10, precision);
    return Math.round(value * factor) / factor;
};

const getRuleFailureLabel = (result: RuleResult | null): string | null => {
    if (!result || result.passed) return null;
    if (result.ruleName) return result.ruleName;
    return null;
};

export class VirtualExecutionEngine {
    private readonly riskEngine = new RiskEngine();
    private readonly positionsBySymbol = new Map<string, VirtualPosition>();
    private readonly pendingEntryBySymbol = new Map<string, PendingEntryIntent>();
    private readonly pendingExitBySymbol = new Map<string, PendingExitIntent>();
    private readonly trades: BacktestTrade[] = [];
    private readonly entryRejections: Record<string, number> = {};
    private readonly lossReasons: Record<string, number> = {};
    private sequence = 0;
    private tradeSliceSequence = 0;

    constructor(
        private readonly ledger: VirtualLedger,
        private readonly execution: BacktestExecutionConfig
    ) { }

    public getTrades(): BacktestTrade[] {
        return [...this.trades];
    }

    public getEntryRejections(): Record<string, number> {
        return { ...this.entryRejections };
    }

    public getLossReasons(): Record<string, number> {
        return { ...this.lossReasons };
    }

    public hasOpenPosition(symbol: string): boolean {
        return this.positionsBySymbol.has(symbol);
    }

    public getOpenPositions(): VirtualPosition[] {
        return Array.from(this.positionsBySymbol.values());
    }

    public getOpenPositionsAtEnd(markPriceBySymbol: Record<string, number>): BacktestOpenPositionAtEnd[] {
        return Array.from(this.positionsBySymbol.values())
            .sort((a, b) => a.symbol.localeCompare(b.symbol))
            .map((position) => {
                const markPrice = Number(markPriceBySymbol[position.symbol] || position.entryPrice);
                const grossUnrealizedPnlUsd = position.side === SignalDirection.BUY
                    ? (markPrice - position.entryPrice) * position.size
                    : (position.entryPrice - markPrice) * position.size;
                const unrealizedPnlUsd = grossUnrealizedPnlUsd - position.entryFeeUsd;
                const costBasis = position.entryNotional + position.entryFeeUsd;
                const unrealizedPnlPct = costBasis > 0
                    ? (unrealizedPnlUsd / costBasis) * 100
                    : 0;
                return {
                    status: 'OPEN_AT_END',
                    symbol: position.symbol,
                    side: position.side,
                    entryTimestamp: position.entryTimestamp,
                    size: round(position.size, 8),
                    entryPrice: round(position.entryPrice, 8),
                    markPrice: round(markPrice, 8),
                    unrealizedPnlUsd: round(unrealizedPnlUsd, 8),
                    unrealizedPnlPct: round(unrealizedPnlPct, 6),
                    reason: 'Replay window ended with position still open.'
                };
            });
    }

    private incrementRejection(reason: string): void {
        this.entryRejections[reason] = (this.entryRejections[reason] || 0) + 1;
    }

    private incrementLossReason(reason: string): void {
        this.lossReasons[reason] = (this.lossReasons[reason] || 0) + 1;
    }

    private nextPositionId(symbol: string, timestamp: number): string {
        this.sequence += 1;
        return `${symbol}-${timestamp}-${String(this.sequence).padStart(5, '0')}`;
    }

    private nextTradeSliceId(positionId: string): string {
        this.tradeSliceSequence += 1;
        return `${positionId}-slice-${String(this.tradeSliceSequence).padStart(5, '0')}`;
    }

    private resolveOrderType(profileSettings?: any): 'market' | 'limit' {
        const profileType = String(profileSettings?.strategy_config?.execution?.orderType || '').trim().toLowerCase();
        if (profileType === 'limit') return 'limit';
        return this.execution.orderType;
    }

    private slippageMultiplier(side: SignalDirection.BUY | SignalDirection.SELL): number {
        const bps = this.execution.slippageBps / 10_000;
        if (side === SignalDirection.BUY) return 1 + bps;
        return 1 - bps;
    }

    private applySlippage(price: number, side: SignalDirection.BUY | SignalDirection.SELL): number {
        return round(price * this.slippageMultiplier(side), 8);
    }

    private feeRate(): number {
        return Math.max(0, Number(this.execution.feeBps || 0)) / 10_000;
    }

    private computeFeeUsd(notionalUsd: number): number {
        if (!(notionalUsd > 0)) return 0;
        return round(notionalUsd * this.feeRate(), 8);
    }

    private resolveEntryPriceAtClose(
        side: SignalDirection.BUY | SignalDirection.SELL,
        candle: Candle,
        orderType: 'market' | 'limit'
    ): { fillable: boolean; price: number } {
        if (orderType === 'market') {
            return { fillable: true, price: this.applySlippage(candle.close, side) };
        }

        const limitPrice = candle.close;
        const fillable = side === SignalDirection.BUY
            ? candle.low <= limitPrice
            : candle.high >= limitPrice;
        if (!fillable) {
            return { fillable: false, price: limitPrice };
        }
        return { fillable: true, price: limitPrice };
    }

    private resolveQueuedEntryPriceAtOpen(
        intent: PendingEntryIntent,
        candle: Candle
    ): { fillable: boolean; price: number } {
        if (intent.orderType === 'market') {
            return { fillable: true, price: this.applySlippage(candle.open, intent.side) };
        }
        const fillable = intent.side === SignalDirection.BUY
            ? candle.low <= intent.limitPrice
            : candle.high >= intent.limitPrice;
        return { fillable, price: intent.limitPrice };
    }

    private resolveSignalExitPrice(
        position: VirtualPosition,
        candle: Candle,
        at: 'open' | 'close'
    ): number {
        const closeSide = position.side === SignalDirection.BUY
            ? SignalDirection.SELL
            : SignalDirection.BUY;
        const reference = at === 'open' ? candle.open : candle.close;
        return this.applySlippage(reference, closeSide);
    }

    private resolveIntraCandleConflictReason(
        position: VirtualPosition,
        candle: Candle,
        policy: BacktestIntraCandlePolicy
    ): 'STOP_LOSS' | 'TAKE_PROFIT' {
        if (policy === 'stop_loss_first') return 'STOP_LOSS';
        if (policy === 'take_profit_first') return 'TAKE_PROFIT';

        if (candle.close === candle.open) {
            const stopDistance = Math.abs(candle.open - position.stopLoss);
            const takeDistance = Math.abs(position.takeProfit - candle.open);
            if (stopDistance <= takeDistance) return 'STOP_LOSS';
            return 'TAKE_PROFIT';
        }

        const bullish = candle.close > candle.open;
        if (position.side === SignalDirection.BUY) {
            return bullish ? 'STOP_LOSS' : 'TAKE_PROFIT';
        }
        return bullish ? 'TAKE_PROFIT' : 'STOP_LOSS';
    }

    private detectStopTakeProfitTrigger(
        position: VirtualPosition,
        candle: Candle
    ): { reason: 'STOP_LOSS' | 'TAKE_PROFIT'; price: number } | null {
        const stopTriggered = position.side === SignalDirection.BUY
            ? (position.stopLoss > 0 && candle.low <= position.stopLoss)
            : (position.stopLoss > 0 && candle.high >= position.stopLoss);
        const takeProfitTriggered = position.side === SignalDirection.BUY
            ? (position.takeProfit > 0 && candle.high >= position.takeProfit)
            : (position.takeProfit > 0 && candle.low <= position.takeProfit);

        if (!stopTriggered && !takeProfitTriggered) {
            return null;
        }

        let reason: 'STOP_LOSS' | 'TAKE_PROFIT';
        if (stopTriggered && takeProfitTriggered) {
            reason = this.resolveIntraCandleConflictReason(position, candle, this.execution.intraCandlePolicy);
        } else {
            reason = stopTriggered ? 'STOP_LOSS' : 'TAKE_PROFIT';
        }
        const price = this.computeExitPrice(position, candle, reason);
        return { reason, price };
    }

    private openPosition(
        symbol: string,
        side: SignalDirection.BUY | SignalDirection.SELL,
        qty: number,
        entryPrice: number,
        stopLoss: number,
        takeProfit: number,
        timestamp: number,
        entryReason: string
    ): boolean {
        const normalizedQty = round(qty, 8);
        if (!(normalizedQty > 0) || !Number.isFinite(entryPrice) || entryPrice <= 0) {
            this.incrementRejection('PARTIAL_FILL_ZERO');
            return false;
        }

        const entryNotional = round(normalizedQty * entryPrice, 8);
        const entryFeeUsd = this.computeFeeUsd(entryNotional);
        const requiredCapital = round(entryNotional + entryFeeUsd, 8);
        if (!this.ledger.reserveCapital(requiredCapital)) {
            this.incrementRejection('INSUFFICIENT_CAPITAL');
            return false;
        }

        this.ledger.consumeReserved(requiredCapital);
        const positionId = this.nextPositionId(symbol, timestamp);
        this.positionsBySymbol.set(symbol, {
            id: positionId,
            symbol,
            side,
            size: normalizedQty,
            entryPrice: round(entryPrice, 8),
            entryNotional,
            entryFeeUsd,
            stopLoss: round(stopLoss, 8),
            takeProfit: round(takeProfit, 8),
            peakPrice: round(entryPrice, 8),
            entryTimestamp: timestamp,
            entryReason
        });
        return true;
    }

    private computeExitPrice(
        position: VirtualPosition,
        candle: Candle,
        exitReason: 'STOP_LOSS' | 'TAKE_PROFIT' | 'SIGNAL_FLIP' | 'NEUTRAL_EXIT' | 'FORCED_EOD'
    ): number {
        if (exitReason === 'STOP_LOSS') {
            return round(position.stopLoss > 0 ? position.stopLoss : candle.close, 8);
        }
        if (exitReason === 'TAKE_PROFIT') {
            return round(position.takeProfit > 0 ? position.takeProfit : candle.close, 8);
        }
        return round(candle.close, 8);
    }

    private closePositionSlice(
        position: VirtualPosition,
        qtyToClose: number,
        timestamp: number,
        rawExitPrice: number,
        exitReason: string
    ): void {
        if (!(qtyToClose > 0) || !Number.isFinite(rawExitPrice) || rawExitPrice <= 0) return;

        const previousSize = position.size;
        const normalizedQty = Math.min(previousSize, qtyToClose);
        const closeRatio = normalizedQty / previousSize;
        const entryNotionalSlice = round(position.entryNotional * closeRatio, 8);
        const entryFeeSlice = round(position.entryFeeUsd * closeRatio, 8);
        const exitPrice = round(rawExitPrice, 8);
        const exitNotional = round(exitPrice * normalizedQty, 8);
        const exitFeeUsd = this.computeFeeUsd(exitNotional);

        const grossPnlUsd = position.side === SignalDirection.BUY
            ? (exitPrice - position.entryPrice) * normalizedQty
            : (position.entryPrice - exitPrice) * normalizedQty;
        const pnlUsd = round(grossPnlUsd - entryFeeSlice - exitFeeUsd, 8);
        const pnlBase = entryNotionalSlice + entryFeeSlice;
        const pnlPct = pnlBase > 0
            ? (pnlUsd / pnlBase) * 100
            : 0;

        if (position.side === SignalDirection.BUY) {
            this.ledger.adjustCash(round(exitNotional - exitFeeUsd, 8));
        } else {
            this.ledger.adjustCash(round(entryNotionalSlice + grossPnlUsd - exitFeeUsd, 8));
        }
        this.ledger.recordRealizedPnl(pnlUsd);
        if (pnlUsd < 0) {
            this.incrementLossReason(exitReason);
        }

        const trade: BacktestTrade = {
            id: this.nextTradeSliceId(position.id),
            symbol: position.symbol,
            side: position.side,
            entryTimestamp: position.entryTimestamp,
            exitTimestamp: timestamp,
            durationMinutes: Math.max(0, Math.round((timestamp - position.entryTimestamp) / 60_000)),
            size: round(normalizedQty, 8),
            entryPrice: round(position.entryPrice, 8),
            exitPrice,
            pnlUsd: round(pnlUsd, 8),
            pnlPct: round(pnlPct, 6),
            entryReason: position.entryReason,
            exitReason
        };
        this.trades.push(trade);

        position.size = round(position.size - normalizedQty, 8);
        position.entryNotional = round(position.entryNotional - entryNotionalSlice, 8);
        position.entryFeeUsd = round(position.entryFeeUsd - entryFeeSlice, 8);
        if (position.size <= 1e-8 || position.entryNotional <= 1e-8) {
            this.positionsBySymbol.delete(position.symbol);
            this.pendingExitBySymbol.delete(position.symbol);
        } else {
            this.positionsBySymbol.set(position.symbol, position);
        }
    }

    private maybeExitForStops(
        position: VirtualPosition,
        candle: Candle,
        timestamp: number,
        intraCandles?: Candle[]
    ): boolean {
        const triggerCandles = Array.isArray(intraCandles) && intraCandles.length > 0
            ? [...intraCandles].sort((a, b) => a.timestamp - b.timestamp)
            : [candle];

        for (const triggerCandle of triggerCandles) {
            const trigger = this.detectStopTakeProfitTrigger(position, triggerCandle);
            if (!trigger) continue;
            const closeQty = round(position.size * this.execution.partialFillPct, 8);
            const normalizedCloseQty = closeQty > 0 ? closeQty : position.size;
            const effectiveTimestamp = Number.isFinite(triggerCandle.timestamp)
                ? Math.max(timestamp, triggerCandle.timestamp)
                : timestamp;
            this.closePositionSlice(position, normalizedCloseQty, effectiveTimestamp, trigger.price, trigger.reason);
            return true;
        }
        return false;
    }

    private executePendingExit(symbol: string, timestamp: number, candle: Candle): void {
        const pending = this.pendingExitBySymbol.get(symbol);
        if (!pending) return;

        const position = this.positionsBySymbol.get(symbol);
        if (!position) {
            this.pendingExitBySymbol.delete(symbol);
            return;
        }

        const normalizedQty = round(Math.min(position.size, pending.qty), 8);
        if (!(normalizedQty > 0)) {
            this.pendingExitBySymbol.delete(symbol);
            return;
        }

        const exitPrice = this.resolveSignalExitPrice(position, candle, 'open');
        this.closePositionSlice(position, normalizedQty, timestamp, exitPrice, pending.reason);
        this.pendingExitBySymbol.delete(symbol);
    }

    private executePendingEntry(symbol: string, timestamp: number, candle: Candle): void {
        const pending = this.pendingEntryBySymbol.get(symbol);
        if (!pending) return;

        if (this.positionsBySymbol.has(symbol)) {
            this.pendingEntryBySymbol.delete(symbol);
            return;
        }

        const fill = this.resolveQueuedEntryPriceAtOpen(pending, candle);
        if (!fill.fillable) {
            this.incrementRejection('LIMIT_NOT_FILLED');
            this.pendingEntryBySymbol.delete(symbol);
            return;
        }

        this.openPosition(
            symbol,
            pending.side,
            pending.qty,
            fill.price,
            pending.stopLoss,
            pending.takeProfit,
            timestamp,
            pending.entryReason
        );
        this.pendingEntryBySymbol.delete(symbol);
    }

    public async processTick(input: ProcessTickInput): Promise<void> {
        const { symbol, timestamp, candle, intraCandles, strategyResult, context, profileSettings } = input;

        if (this.execution.fillOnNextBar) {
            this.executePendingExit(symbol, timestamp, candle);
            this.executePendingEntry(symbol, timestamp, candle);
        }

        const position = this.positionsBySymbol.get(symbol);

        if (position) {
            if (this.maybeExitForStops(position, candle, timestamp, intraCandles)) {
                return;
            }
        }

        const refreshedPosition = this.positionsBySymbol.get(symbol);
        if (refreshedPosition && strategyResult) {
            const signal = strategyResult.signal || SignalDirection.NONE;
            const isOpposite = (refreshedPosition.side === SignalDirection.BUY && signal === SignalDirection.SELL)
                || (refreshedPosition.side === SignalDirection.SELL && signal === SignalDirection.BUY);

            if (isOpposite) {
                const closeQty = round(refreshedPosition.size * this.execution.partialFillPct, 8);
                const normalizedCloseQty = closeQty > 0 ? closeQty : refreshedPosition.size;
                if (this.execution.fillOnNextBar) {
                    this.pendingExitBySymbol.set(symbol, {
                        reason: 'SIGNAL_FLIP',
                        qty: normalizedCloseQty,
                        queuedAt: timestamp
                    });
                } else {
                    const exitPrice = this.resolveSignalExitPrice(refreshedPosition, candle, 'close');
                    this.closePositionSlice(refreshedPosition, normalizedCloseQty, timestamp, exitPrice, 'SIGNAL_FLIP');
                }
                return;
            }

            const neutralSignal = signal === SignalDirection.NONE;
            if (neutralSignal) {
                const pnlPct = ((candle.close - refreshedPosition.entryPrice) / refreshedPosition.entryPrice) * 100
                    * (refreshedPosition.side === SignalDirection.BUY ? 1 : -1);
                const neutralExitThreshold = Number(profileSettings?.strategy_config?.execution?.profitExitPercent ?? 1.0);
                if (pnlPct >= neutralExitThreshold) {
                    const closeQty = round(refreshedPosition.size * this.execution.partialFillPct, 8);
                    const normalizedCloseQty = closeQty > 0 ? closeQty : refreshedPosition.size;
                    if (this.execution.fillOnNextBar) {
                        this.pendingExitBySymbol.set(symbol, {
                            reason: 'NEUTRAL_EXIT',
                            qty: normalizedCloseQty,
                            queuedAt: timestamp
                        });
                    } else {
                        const exitPrice = this.resolveSignalExitPrice(refreshedPosition, candle, 'close');
                        this.closePositionSlice(refreshedPosition, normalizedCloseQty, timestamp, exitPrice, 'NEUTRAL_EXIT');
                    }
                    return;
                }
            }
        }

        if (this.positionsBySymbol.has(symbol)) return;
        if (this.execution.fillOnNextBar && this.pendingEntryBySymbol.has(symbol)) return;
        if (!strategyResult || !context) return;
        if (!strategyResult.passed || !strategyResult.signal || strategyResult.signal === SignalDirection.NONE) {
            const failedRule = getRuleFailureLabel(strategyResult);
            if (failedRule) {
                this.incrementRejection(`RULE_BLOCKED_${failedRule}`);
            }
            return;
        }

        const side = strategyResult.signal as SignalDirection.BUY | SignalDirection.SELL;
        const availableCapital = this.ledger.getAvailableCashUsd();
        const riskProfile = await this.riskEngine.calculateRiskProfile(
            symbol,
            side,
            context,
            profileSettings,
            availableCapital
        );
        if (!riskProfile) {
            this.incrementRejection('RISK_ENGINE_REJECTED');
            return;
        }

        const orderType = this.resolveOrderType(profileSettings);
        const partialFillPct = Math.max(0.01, Math.min(1, this.execution.partialFillPct));
        const filledQty = round(riskProfile.positionSize * partialFillPct, 8);
        if (!(filledQty > 0)) {
            this.incrementRejection('PARTIAL_FILL_ZERO');
            return;
        }

        if (this.execution.fillOnNextBar) {
            this.pendingEntryBySymbol.set(symbol, {
                symbol,
                side,
                qty: filledQty,
                orderType,
                limitPrice: round(candle.close, 8),
                stopLoss: round(riskProfile.stopLoss, 8),
                takeProfit: round(riskProfile.takeProfit, 8),
                queuedAt: timestamp,
                entryReason: strategyResult.reason || 'Signal entry'
            });
            return;
        }

        const fill = this.resolveEntryPriceAtClose(side, candle, orderType);
        if (!fill.fillable) {
            this.incrementRejection('LIMIT_NOT_FILLED');
            return;
        }

        this.openPosition(
            symbol,
            side,
            filledQty,
            fill.price,
            riskProfile.stopLoss,
            riskProfile.takeProfit,
            timestamp,
            strategyResult.reason || 'Signal entry'
        );
    }

    public forceCloseAtEnd(symbol: string, timestamp: number, candle: Candle): void {
        const position = this.positionsBySymbol.get(symbol);
        if (!position) return;
        const exitPrice = this.resolveSignalExitPrice(position, candle, 'close');
        this.closePositionSlice(position, position.size, timestamp, exitPrice, 'FORCED_WINDOW_END');
        this.pendingExitBySymbol.delete(symbol);
        this.pendingEntryBySymbol.delete(symbol);
    }

    public computeUnrealizedPnl(priceBySymbol: Record<string, number>): number {
        let total = 0;
        for (const position of this.positionsBySymbol.values()) {
            const mark = Number(priceBySymbol[position.symbol] || position.entryPrice);
            const grossPnl = position.side === SignalDirection.BUY
                ? (mark - position.entryPrice) * position.size
                : (position.entryPrice - mark) * position.size;
            total += grossPnl - position.entryFeeUsd;
        }
        return total;
    }
}
