import { config } from '../../config/index.js';
import type { Candle } from '../../connectors/types.js';
import { ProStrategyEngine } from '../../strategies/ProStrategyEngine.js';
import { SignalDirection, type RuleResult } from '../../strategies/rules/types.js';
import { ReplayExchangeConnector } from '../exchange/ReplayExchangeConnector.js';
import { VirtualLedger } from './VirtualLedger.js';
import { VirtualExecutionEngine } from './VirtualExecutionEngine.js';
import { runWithFrozenDate } from './timeFreeze.js';
import { computeSummary } from '../metrics/computeSummary.js';
import { computeWarmupRequirements, createWarmupReport } from './warmup.js';
import type {
    BacktestDataSourceType,
    BacktestExecutionConfig,
    BacktestIntraCandlePolicy,
    BacktestReplayWindow,
    BacktestRequest,
    BacktestResult,
    BacktestTimeframe,
    EquityPoint,
    HistoricalDataset
} from '../types.js';

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

const TIMEFRAME_TO_MS: Record<BacktestTimeframe, number> = {
    '1m': 60 * 1000,
    '15m': 15 * 60 * 1000,
    '1h': 60 * 60 * 1000,
    '4h': 4 * 60 * 60 * 1000
};

const normalizeIntraCandlePolicy = (value: unknown): BacktestIntraCandlePolicy => {
    const normalized = String(value || '').trim().toLowerCase();
    if (normalized === 'stop_loss_first' || normalized === 'stop-first' || normalized === 'stop_first') {
        return 'stop_loss_first';
    }
    if (normalized === 'take_profit_first' || normalized === 'take-first' || normalized === 'take_first') {
        return 'take_profit_first';
    }
    return 'ohlc_path';
};

const normalizeTriggerTimeframe = (value: unknown): 'off' | '1m' => {
    const normalized = String(value || '').trim().toLowerCase();
    if (normalized === 'off' || normalized === 'none' || normalized === 'disabled') return 'off';
    if (normalized === '1m' || normalized === '1min') return '1m';
    return '1m';
};

const buildExecutionConfig = (request: BacktestRequest, profileSettings?: any): BacktestExecutionConfig => {
    const initialCapital = Number(request.execution?.initialCapitalUsd ?? profileSettings?.allocated_capital ?? config.TOTAL_CAPITAL);
    const orderTypeRaw = String(request.execution?.orderType || profileSettings?.strategy_config?.execution?.orderType || 'market').trim().toLowerCase();
    const orderType = orderTypeRaw === 'limit' ? 'limit' : 'market';
    const slippageBps = Number(request.execution?.slippageBps ?? 5);
    const feeBps = Number(request.execution?.feeBps ?? 10);
    const partialFillPct = Number(request.execution?.partialFillPct ?? 1);
    const fillOnNextBar = request.execution?.fillOnNextBar !== false;
    const intraCandlePolicy = normalizeIntraCandlePolicy(request.execution?.intraCandlePolicy);
    const triggerTimeframe = normalizeTriggerTimeframe(request.execution?.triggerTimeframe);
    const allowNegativeCash = request.execution?.allowNegativeCash === true;
    const enforceWarmup = request.execution?.enforceWarmup !== false;
    const forceCloseAtWindowEnd = request.execution?.forceCloseAtWindowEnd === true;

    if (!Number.isFinite(initialCapital) || initialCapital <= 0) {
        throw new Error(`Invalid execution.initialCapitalUsd: ${initialCapital}`);
    }

    return {
        initialCapitalUsd: initialCapital,
        orderType,
        slippageBps: Number.isFinite(slippageBps) ? Math.max(0, slippageBps) : 0,
        feeBps: Number.isFinite(feeBps) ? Math.max(0, feeBps) : 0,
        partialFillPct: Number.isFinite(partialFillPct) ? Math.max(0.01, Math.min(1, partialFillPct)) : 1,
        fillOnNextBar,
        intraCandlePolicy,
        triggerTimeframe,
        allowNegativeCash,
        enforceWarmup,
        forceCloseAtWindowEnd
    };
};

const buildReplayTimeline = (
    dataset: HistoricalDataset,
    symbols: string[],
    timeframe: BacktestTimeframe,
    fromTimestamp: number,
    toTimestamp: number
): number[] => {
    const timeline = new Set<number>();
    for (const symbol of symbols) {
        const candles = dataset[symbol]?.[timeframe] || [];
        for (const candle of candles) {
            if (candle.timestamp < fromTimestamp || candle.timestamp > toTimestamp) continue;
            timeline.add(candle.timestamp);
        }
    }
    return Array.from(timeline.values()).sort((a, b) => a - b);
};

const findCandleAt = (candles: Candle[], timestamp: number): Candle | null => {
    if (!candles.length) return null;
    let left = 0;
    let right = candles.length - 1;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const current = candles[mid];
        if (current.timestamp === timestamp) return current;
        if (current.timestamp < timestamp) left = mid + 1;
        else right = mid - 1;
    }
    return null;
};

const markBlockedRule = (result: RuleResult | null, bucket: Record<string, number>): void => {
    if (!result || result.passed) return;
    const key = String(result.ruleName || 'UnknownRule');
    bucket[key] = (bucket[key] || 0) + 1;
};

const formatReplayWindow = (window: BacktestReplayWindow): string =>
    `${window.fromIso.slice(0, 10)} -> ${window.toIso.slice(0, 10)} (${window.timezone})`;

const hasWarmupBeforeWindow = (
    dataset: HistoricalDataset,
    symbols: string[],
    fromTimestamp: number
): boolean => symbols.every((symbol) => (
    (dataset[symbol]?.['15m'] || []).some((candle) => candle.timestamp < fromTimestamp)
    && (dataset[symbol]?.['1h'] || []).some((candle) => candle.timestamp < fromTimestamp)
    && (dataset[symbol]?.['4h'] || []).some((candle) => candle.timestamp < fromTimestamp)
));

const buildEquityPoint = (
    timestamp: number,
    ledger: VirtualLedger,
    unrealizedPnl: number,
    peakEquity: number
): { point: EquityPoint; nextPeak: number } => {
    const equityUsd = round(ledger.getInitialCapitalUsd() + ledger.getRealizedPnlUsd() + unrealizedPnl, 8);
    const nextPeak = Math.max(peakEquity, equityUsd);
    const drawdownPct = nextPeak > 0 ? ((nextPeak - equityUsd) / nextPeak) * 100 : 0;
    return {
        point: {
            timestamp,
            equityUsd,
            drawdownPct: round(drawdownPct, 8),
            cashUsd: round(ledger.getCashUsd(), 8),
            reservedUsd: round(ledger.getReservedUsd(), 8)
        },
        nextPeak
    };
};

export interface BacktestRunnerInput {
    request: BacktestRequest;
    dataset: HistoricalDataset;
    replayWindow: BacktestReplayWindow;
    dataSourceType: BacktestDataSourceType;
    profileSettings?: any;
}

export const runBacktestReplay = async ({
    request,
    dataset,
    replayWindow,
    dataSourceType,
    profileSettings
}: BacktestRunnerInput): Promise<BacktestResult> => {
    const symbols = request.symbols
        .map((value) => String(value || '').trim().toUpperCase())
        .filter(Boolean)
        .sort((a, b) => a.localeCompare(b));
    if (!symbols.length) {
        throw new Error('Backtest requires at least one symbol.');
    }

    const execution = buildExecutionConfig(request, profileSettings);
    const strategyConfig = request.strategyConfig || profileSettings?.strategy_config || {};
    const profileRules = Array.isArray(strategyConfig?.rules) ? strategyConfig.rules : undefined;
    const minRulePassRatio = Number(strategyConfig?.execution?.minRulePassRatio ?? 1.0);

    const connector = new ReplayExchangeConnector(dataset);
    const engine = new ProStrategyEngine(connector);
    const ledger = new VirtualLedger(execution.initialCapitalUsd, execution.allowNegativeCash);
    const executor = new VirtualExecutionEngine(ledger, execution);

    const replayTimeline = buildReplayTimeline(
        dataset,
        symbols,
        request.timeframe,
        replayWindow.fromTimestamp,
        replayWindow.toTimestamp
    );
    if (!replayTimeline.length) {
        throw new Error('No replay timestamps found for selected symbols/timeframe/window.');
    }

    const bySymbolCandle: Record<string, Candle[]> = {};
    const bySymbolTriggerCandle: Record<string, Candle[]> = {};
    const latestPriceBySymbol: Record<string, number> = {};
    for (const symbol of symbols) {
        bySymbolCandle[symbol] = dataset[symbol]?.[request.timeframe] || [];
        bySymbolTriggerCandle[symbol] = execution.triggerTimeframe === 'off'
            ? []
            : (dataset[symbol]?.[execution.triggerTimeframe] || []);
        const firstReplayTimestamp = replayTimeline[0];
        const prior = bySymbolCandle[symbol].filter((candle) => candle.timestamp <= firstReplayTimestamp);
        latestPriceBySymbol[symbol] = prior.length
            ? prior[prior.length - 1].close
            : (bySymbolCandle[symbol][0]?.close || 0);
    }
    const intraCandleWindowMs = TIMEFRAME_TO_MS[request.timeframe];
    const useIntraCandleTriggerResolution = execution.triggerTimeframe !== 'off'
        && execution.triggerTimeframe !== request.timeframe;
    const triggerCursorBySymbol: Record<string, number> = Object.fromEntries(
        symbols.map((symbol) => [symbol, 0])
    );

    const warmupRequirements = computeWarmupRequirements(strategyConfig);
    const warmupStart = symbols.reduce((min, symbol) => {
        const ts = dataset[symbol]?.['15m']?.[0]?.timestamp;
        if (!Number.isFinite(ts)) return min;
        return Math.min(min, Number(ts));
    }, replayWindow.fromTimestamp);
    let warmupEnd: number | null = null;
    let ignoredSignals = 0;

    const blockedRuleCounts: Record<string, number> = {};
    const equityTimeline: EquityPoint[] = [];
    let peakEquity = execution.initialCapitalUsd;

    for (const timestamp of replayTimeline) {
        connector.setCursorTimestamp(timestamp);
        const warmupSatisfied = symbols.every((symbol) =>
            connector.getCandleCountUpTo(symbol, '15m', timestamp) >= warmupRequirements['15m']
            && connector.getCandleCountUpTo(symbol, '1h', timestamp) >= warmupRequirements['1h']
            && connector.getCandleCountUpTo(symbol, '4h', timestamp) >= warmupRequirements['4h']
        );
        if (warmupSatisfied && warmupEnd === null) {
            warmupEnd = timestamp;
        }
        const warmupActive = execution.enforceWarmup && !warmupSatisfied;

        for (const symbol of symbols) {
            const candle = findCandleAt(bySymbolCandle[symbol], timestamp);
            if (!candle) continue;
            latestPriceBySymbol[symbol] = candle.close;

            const { context, result } = await runWithFrozenDate(timestamp, async () => {
                const builtContext = await engine.buildMarketContext(symbol);
                if (!builtContext) {
                    return { context: null, result: null as RuleResult | null };
                }
                const evaluated = await engine.evaluateContext(builtContext, profileRules, minRulePassRatio);
                return { context: builtContext, result: evaluated };
            });

            markBlockedRule(result, blockedRuleCounts);
            if (warmupActive) {
                if (result?.passed && result.signal && result.signal !== SignalDirection.NONE) {
                    ignoredSignals += 1;
                }
                continue;
            }

            let intraCandles: Candle[] | undefined;
            if (useIntraCandleTriggerResolution) {
                const triggerCandles = bySymbolTriggerCandle[symbol] || [];
                if (triggerCandles.length > 0) {
                    const windowStart = candle.timestamp;
                    const windowEnd = windowStart + intraCandleWindowMs;
                    let cursor = triggerCursorBySymbol[symbol] || 0;
                    while (cursor < triggerCandles.length && triggerCandles[cursor].timestamp < windowStart) {
                        cursor += 1;
                    }
                    triggerCursorBySymbol[symbol] = cursor;
                    const scoped: Candle[] = [];
                    for (let i = cursor; i < triggerCandles.length; i++) {
                        const point = triggerCandles[i];
                        if (point.timestamp >= windowEnd) break;
                        if (point.timestamp >= windowStart) scoped.push(point);
                    }
                    intraCandles = scoped.length > 0 ? scoped : undefined;
                }
            }

            await executor.processTick({
                symbol,
                timestamp,
                timeframe: request.timeframe,
                candle,
                intraCandles,
                strategyResult: result,
                context,
                profileSettings
            });
        }

        const unrealized = executor.computeUnrealizedPnl(latestPriceBySymbol);
        const { point, nextPeak } = buildEquityPoint(timestamp, ledger, unrealized, peakEquity);
        peakEquity = nextPeak;
        equityTimeline.push(point);
    }

    const lastTimestamp = replayTimeline[replayTimeline.length - 1];
    if (execution.forceCloseAtWindowEnd) {
        for (const symbol of symbols) {
            const closeCandle = findCandleAt(bySymbolCandle[symbol], lastTimestamp) || bySymbolCandle[symbol][bySymbolCandle[symbol].length - 1];
            if (!closeCandle) continue;
            executor.forceCloseAtEnd(symbol, lastTimestamp, closeCandle);
            latestPriceBySymbol[symbol] = closeCandle.close;
        }
    }

    const openPositionsAtEnd = execution.forceCloseAtWindowEnd
        ? []
        : executor.getOpenPositionsAtEnd(latestPriceBySymbol);
    const finalUnrealized = executor.computeUnrealizedPnl(latestPriceBySymbol);
    const { point: finalPoint, nextPeak: finalPeak } = buildEquityPoint(lastTimestamp, ledger, finalUnrealized, peakEquity);
    peakEquity = finalPeak;
    if (
        equityTimeline.length === 0
        || equityTimeline[equityTimeline.length - 1].equityUsd !== finalPoint.equityUsd
        || equityTimeline[equityTimeline.length - 1].cashUsd !== finalPoint.cashUsd
    ) {
        equityTimeline.push(finalPoint);
    }

    const trades = executor.getTrades();
    const summary = computeSummary(trades, equityTimeline, request.timeframe);
    const warmup = createWarmupReport(
        warmupStart,
        warmupEnd ?? lastTimestamp,
        warmupRequirements,
        ignoredSignals
    );

    return {
        mode: 'backtest',
        trades,
        summary,
        timeline: equityTimeline,
        window: {
            fromTimestamp: replayWindow.fromTimestamp,
            toTimestamp: replayWindow.toTimestamp,
            fromIso: replayWindow.fromIso,
            toIso: replayWindow.toIso,
            timezone: replayWindow.timezone,
            includesWarmupCandles: hasWarmupBeforeWindow(dataset, symbols, replayWindow.fromTimestamp),
            endOfWindowPolicy: execution.forceCloseAtWindowEnd ? 'FORCE_CLOSE' : 'OPEN_AT_END'
        },
        warmup,
        openPositionsAtEnd,
        assumptions: {
            fillModel: execution.fillOnNextBar
                ? 'market@next_bar_open (limit@next_bar_touch)'
                : 'market@candle_close (limit@candle_touch)',
            slippageModel: `deterministic ${execution.slippageBps} bps per fill`,
            feeModel: `deterministic ${execution.feeBps} bps per fill`,
            latencyModel: execution.fillOnNextBar
                ? 'signal@bar_close -> fill@next_bar_open'
                : 'signal@bar_close -> fill@bar_close',
            intraCandleModel: execution.intraCandlePolicy,
            triggerResolution: execution.triggerTimeframe === 'off'
                ? 'base timeframe only'
                : `${execution.triggerTimeframe} trigger candles`,
            warmupEnforced: execution.enforceWarmup,
            deterministicReplay: true,
            replayWindow: formatReplayWindow(replayWindow),
            endOfWindowPolicy: execution.forceCloseAtWindowEnd ? 'FORCE_CLOSE' : 'OPEN_AT_END',
            disclaimer: `Backtests are deterministic simulations and do not guarantee live outcomes. Source: ${dataSourceType}.`
        },
        diagnostics: {
            blockedRuleCounts,
            entryRejections: executor.getEntryRejections(),
            lossReasons: executor.getLossReasons(),
            connectorCalls: connector.getCallCounts()
        }
    };
};
