import { IExchangeConnector } from '../connectors/types.js';
import { TradeExecutor } from './TradeExecutor.js';
import logger from '../utils/logger.js';
import { SymbolMapper } from '../utils/symbolMapper.js';
import { config } from '../config/index.js';
import { ApiServer } from './apiServer.js';
import { healthTracker } from './healthTracker.js';
import { observabilityService } from './observabilityService.js';
import { buildManagedBotSymbolTokenSet, isManagedBotSymbol } from '../utils/botSymbolScope.js';
import { supabaseService } from './SupabaseService.js';
import { extractOrderSubTag, subTagBelongsToProfile, subTagHintsTrade } from '../utils/alpacaSubTag.js';

export class TradeMonitor {
    private interval: NodeJS.Timeout | null = null;
    private isChecking = false;
    private positionMissingDetections = new Map<string, number>();
    private mismatchEventLastEmitted = new Map<string, number>();
    private exitEvidenceGapLastEmitted = new Map<string, number>();
    private consecutiveReconciliationFailures = 0;
    private readonly MAX_CONSECUTIVE_FAILURES = 3;

    constructor(
        private exchange: IExchangeConnector,
        private executionManager: TradeExecutor,
        private apiServer?: ApiServer
    ) { }

    public start(pollingIntervalMs: number = config.MONITOR_INTERVAL_MS) {
        if (this.interval) return;

        logger.info(`[TradeMonitor] Starting background monitoring (every ${pollingIntervalMs / 1000}s)...`);

        this.interval = setInterval(async () => {
            await this.checkOpenPositions();
        }, pollingIntervalMs);
    }

    public stop() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
            logger.info('[TradeMonitor] Background monitoring stopped.');
        }
    }

    /**
     * FIX-04: Clear per-symbol detection state when a symbol is removed from a profile
     * during hot-reload. Without this, stale miss-counts can trigger ghost position
     * finalization for symbols the profile no longer monitors.
     */
    public clearSymbolDetector(symbol: string): void {
        this.positionMissingDetections.delete(symbol);
        let clearedThrottleKeys = 0;
        const keySuffix = `::${symbol}`;
        for (const key of Array.from(this.mismatchEventLastEmitted.keys())) {
            if (key === symbol || key.endsWith(keySuffix)) {
                this.mismatchEventLastEmitted.delete(key);
                clearedThrottleKeys += 1;
            }
        }
        for (const key of Array.from(this.exitEvidenceGapLastEmitted.keys())) {
            if (key === symbol || key.endsWith(keySuffix)) {
                this.exitEvidenceGapLastEmitted.delete(key);
                clearedThrottleKeys += 1;
            }
        }
        logger.info(`[TradeMonitor] Cleared detection state for removed symbol: ${symbol} (throttle keys=${clearedThrottleKeys})`);
    }

    private async checkOpenPositions() {
        if (this.isChecking) {
            logger.debug('[TradeMonitor] Previous check still running, skipping this tick.');
            return;
        }
        const loopStart = Date.now();
        this.isChecking = true;

        try {
            // Keep local pending limit orders from lingering forever.
            await this.checkPendingOrderTimeouts();

            const managedSymbolTokens = buildManagedBotSymbolTokenSet();
            const activeSymbols = this.executionManager.getActiveSymbols().filter((symbol) =>
                isManagedBotSymbol(symbol, managedSymbolTokens)
            );

            for (const symbol of activeSymbols) {
                try {
                    const localPositions = this.executionManager.getActivePositions(symbol);
                    if (localPositions.length === 0) continue;

                    const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER);
                    const position = await this.instrumentExchange('get_position', () => this.exchange.getPosition(tradeSymbol));

                    // Detection for Exchange ↔ DB parity mismatch with per-profile throttling.
                    if (!position && localPositions.length > 0) {
                        const profileId = String(localPositions[0]?.profileId || this.executionManager.getProfileId() || '').trim();
                        if (this.shouldEmitExchangeMismatch(profileId, symbol)) {
                            this.apiServer?.broadcast('PARITY_WARNING', {
                                symbol,
                                level: 'exchange_flat_db_open',
                                message: `State mismatch: ${symbol} is open in DB but flat on exchange.`
                            });
                            observabilityService.emitEvent({
                                type: 'EXCHANGE_STATE_MISMATCH',
                                severity: 'WARN',
                                message: `State mismatch: ${symbol} is open in DB but flat on exchange.`,
                                profileId: localPositions[0]?.profileId,
                                symbol
                            });
                        }
                    }

                    // If position is gone on exchange, mark trade complete locally.
                    if (!position) {
                        const hasTradingWindow = this.executionManager.verifyCapability('tradingWindow', 'Trading Window Checks');
                        const windowAwareExchange = this.exchange as any;
                        const tradingWindowOpen = (hasTradingWindow && typeof windowAwareExchange.isTradingWindowOpen === 'function')
                            ? await windowAwareExchange.isTradingWindowOpen(tradeSymbol)
                            : true;
                        const misses = (this.positionMissingDetections.get(symbol) || 0) + 1;
                        this.positionMissingDetections.set(symbol, misses);

                        // Off-session position checks are less reliable for finalization; require more confirmations.
                        const requiredMisses = tradingWindowOpen === false ? 3 : 2;
                        if (tradingWindowOpen === null || misses < requiredMisses) {
                            logger.warn(`[TradeMonitor] Position missing for ${symbol} (${misses}/${requiredMisses}) | tradingWindow=${tradingWindowOpen === null ? 'unknown' : tradingWindowOpen ? 'open' : 'closed'}. Waiting for confirmation.`);
                            continue;
                        }

                        // Final confirmation before state mutation.
                        const confirmPosition = await this.instrumentExchange('get_position', () => this.exchange.getPosition(tradeSymbol));
                        if (confirmPosition) {
                            this.positionMissingDetections.delete(symbol);
                            logger.info(`[TradeMonitor] Position reappeared for ${symbol} after missing checks. Keeping trade active.`);
                            continue;
                        }

                        const fallbackPrice = this.apiServer?.getState()?.symbols?.[symbol]?.price
                            || localPositions[0]?.entryPrice
                            || 0;
                        let baselineExitPrice = fallbackPrice;
                        let baselineExitPriceSource = 'fallback';
                        const closedOrderRows: any[] = [];
                        const usedClosedOrderIds = new Set<string>();

                        // FIX-02: Prefer actual exchange fill price over candle close estimate.
                        const canFetchClosed = this.executionManager.verifyCapability('fetchClosedOrders', 'Fetch Closed Orders');
                        if (canFetchClosed && typeof this.exchange.fetchClosedOrders === 'function') {
                            try {
                                const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
                                const closedOrders = await this.instrumentExchange('fetch_closed_orders', () =>
                                    this.exchange.fetchClosedOrders!([tradeSymbol], {
                                        after: since,
                                        limit: Math.max(50, localPositions.length * 8),
                                        maxPages: 3
                                    })
                                );
                                if (Array.isArray(closedOrders)) closedOrderRows.push(...closedOrders);
                                const latestFill = this.selectLatestClosedFill(closedOrderRows);
                                if (latestFill) {
                                    baselineExitPrice = latestFill.price;
                                    baselineExitPriceSource = latestFill.source;
                                }
                            } catch (e) {
                                logger.warn(`[TradeMonitor] Failed to fetch closed orders for ${symbol} fill price: ${e}`);
                            }
                        }

                        // In strict evidence mode, never estimate finalization prices from candles.
                        const strictEvidenceRequired = Boolean(config.REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE);
                        if (!strictEvidenceRequired && baselineExitPriceSource !== 'exchange_fill') {
                            try {
                                const candles = await this.instrumentExchange('fetch_ohlcv', () =>
                                    this.exchange.fetchOHLCV(tradeSymbol, '1Min', 1)
                                );
                                if (candles && candles.length > 0) {
                                    baselineExitPrice = candles[0].close;
                                    baselineExitPriceSource = 'candle_close_estimate';
                                    logger.warn(`[TradeMonitor] Using candle close as exit price for ${symbol} — this is an ESTIMATE, not the actual fill price.`);
                                }
                            } catch {
                                logger.warn(`[TradeMonitor] Could not fetch exit price for ${symbol}; using fallback ${fallbackPrice}`);
                            }
                        } else if (strictEvidenceRequired && baselineExitPriceSource !== 'exchange_fill') {
                            logger.warn(`[TradeMonitor] Strict evidence mode active: skipping candle-based close estimate for ${symbol}. Waiting for exchange fill evidence.`);
                        }

                        logger.info(`[TradeMonitor] Position for ${symbol} (mapped: ${tradeSymbol}) confirmed closed on exchange. Baseline exit price: ${baselineExitPrice} (source: ${baselineExitPriceSource}).`);
                        this.positionMissingDetections.delete(symbol);
                        for (const localPos of localPositions) {
                            const evidenceFill = this.findClosedFillForPosition(localPos, closedOrderRows, usedClosedOrderIds);
                            if (!evidenceFill && strictEvidenceRequired) {
                                const profileId = String(localPos.profileId || this.executionManager.getProfileId() || '').trim();
                                if (this.shouldEmitExitEvidenceGap(profileId, symbol)) {
                                    observabilityService.emitEvent({
                                        type: 'EXIT_FILL_COHERENCE_VIOLATION',
                                        severity: 'ERROR',
                                        message: `Exchange is flat for ${symbol}, but no matching closed-fill evidence was found for DB-open lifecycle. Manual reconciliation required.`,
                                        profileId: localPos.profileId,
                                        symbol
                                    });
                                }
                                const markManualReview = (this.executionManager as any)?.markExitManualReview;
                                if (typeof markManualReview === 'function') {
                                    markManualReview.call(
                                        this.executionManager,
                                        symbol,
                                        'missing_exchange_fill_evidence',
                                        'exchange_flat_no_closed_fill_match',
                                        localPos.tradeId
                                    );
                                }
                                logger.warn(`[TradeMonitor] Skipping finalize for ${symbol} trade=${String(localPos.tradeId || 'unknown')}: missing exchange fill evidence.`);
                                continue;
                            }
                            const exitPrice = evidenceFill?.price || baselineExitPrice;
                            const exitPriceSource = evidenceFill?.source || baselineExitPriceSource;
                            if (evidenceFill?.orderId) usedClosedOrderIds.add(evidenceFill.orderId);
                            logger.info(`[TradeMonitor] Finalizing ${symbol} trade=${String(localPos.tradeId || 'unknown')} using exit price ${exitPrice} (source: ${exitPriceSource}).`);
                            await this.executionManager.markTradeComplete(symbol, exitPrice, 'Exchange Position Closed', localPos.tradeId);
                        }
                        continue;
                    }
                    this.positionMissingDetections.delete(symbol);

                    // Local SL and trailing-profit guard.
                    const candles = await this.instrumentExchange('fetch_ohlcv', () => this.exchange.fetchOHLCV(tradeSymbol, '1Min', 1));
                    if (!candles || candles.length === 0) continue;

                    const currentPrice = candles[0].close;

                    for (const localPos of localPositions) {
                        const stillActive = this.executionManager.getActivePosition(symbol, localPos.tradeId);
                        if (!stillActive) continue;

                        // FIX-01 part 2: Warn when stopLoss=0 — the SL guard below will be a no-op.
                        if (stillActive.stopLoss <= 0) {
                            logger.warn(`[TradeMonitor] ⚠️ Position ${symbol} (trade=${stillActive.tradeId}) has stopLoss=0. Stop-loss guard is INACTIVE for this position.`);
                        }

                        if (stillActive.side === 'BUY' && stillActive.stopLoss > 0 && currentPrice <= stillActive.stopLoss) {
                            await this.executionManager.executeExit(symbol, currentPrice, 'Safety Stop Loss Hit', stillActive.tradeId);
                            continue;
                        }
                        if (stillActive.side === 'SELL' && stillActive.stopLoss > 0 && currentPrice >= stillActive.stopLoss) {
                            await this.executionManager.executeExit(symbol, currentPrice, 'Safety Stop Loss Hit', stillActive.tradeId);
                            continue;
                        }

                        const targetPrice = Number(stillActive.takeProfit || 0);
                        const hasTakeProfit = Number.isFinite(targetPrice) && targetPrice > 0;
                        const isTargetHit = hasTakeProfit
                            ? (stillActive.side === 'BUY' ? currentPrice >= targetPrice : currentPrice <= targetPrice)
                            : false;

                        if (isTargetHit || stillActive.profitGuardActive) {
                            if (!stillActive.profitGuardActive) {
                                logger.info(`[TradeMonitor] Target hit for ${symbol}. Activating trailing profit guard.`);
                                stillActive.profitGuardActive = true;
                                stillActive.peakPrice = currentPrice;
                            }

                            const trailingStopPct = config.TRAILING_STOP_PERCENT;
                            if (stillActive.side === 'BUY') {
                                stillActive.peakPrice = Math.max(stillActive.peakPrice, currentPrice);
                                if (currentPrice <= stillActive.peakPrice * (1 - trailingStopPct)) {
                                    await this.executionManager.executeExit(symbol, currentPrice, `Trailing Profit Guard (${(trailingStopPct * 100).toFixed(1)}% Pullback)`, stillActive.tradeId);
                                }
                            } else {
                                stillActive.peakPrice = Math.min(stillActive.peakPrice, currentPrice);
                                if (currentPrice >= stillActive.peakPrice * (1 + trailingStopPct)) {
                                    await this.executionManager.executeExit(symbol, currentPrice, `Trailing Profit Guard (${(trailingStopPct * 100).toFixed(1)}% Pullback)`, stillActive.tradeId);
                                }
                            }
                        }
                    }
                } catch (e) {
                    logger.error(`[TradeMonitor] Error checking position for ${symbol}: ${e}`);
                }
            }

            this.consecutiveReconciliationFailures = 0;
            if (this.apiServer) {
                const allPos = this.executionManager.getAllActivePositions();
                this.apiServer.updatePositions(allPos, this.executionManager.getProfileId() || 'global');
            }
        } catch (e) {
            this.consecutiveReconciliationFailures++;
            logger.error(`[TradeMonitor] Reconciliation failed (${this.consecutiveReconciliationFailures}/${this.MAX_CONSECUTIVE_FAILURES}): ${e}`);

            if (this.consecutiveReconciliationFailures >= this.MAX_CONSECUTIVE_FAILURES) {
                if (this.apiServer) {
                    this.apiServer.broadcast('RECONCILIATION_DEGRADED', {
                        consecutiveFailures: this.consecutiveReconciliationFailures,
                        lastError: String(e)
                    });
                }
                observabilityService.emitEvent({
                    type: 'RECONCILIATION_DEGRADED',
                    severity: 'ERROR',
                    message: `Position reconciliation failing repeatedly (${this.consecutiveReconciliationFailures}/${this.MAX_CONSECUTIVE_FAILURES})`
                });
            }
        } finally {
            const duration = Date.now() - loopStart;
            observabilityService.recordMonitorLoop(duration);
            healthTracker.recordMonitorLoop();
            this.apiServer?.publishHealthSnapshot({ broadcast: true });
            this.isChecking = false;
        }
    }

    /**
     * Cancel stale local limit orders. Exchange order-status reconciliation is owned
     * by OrderStatusSyncService to avoid duplicate status writers.
     */
    private async checkPendingOrderTimeouts() {
        const pendingOrders = this.executionManager.getPendingOrders();
        const timeout = config.LIMIT_ORDER_TIMEOUT_MS;
        const now = Date.now();

        for (const [orderId, pending] of pendingOrders.entries()) {
            if (pending.type !== 'limit') continue;
            const elapsed = now - pending.placedAt;

            if (elapsed <= timeout) continue;

            logger.warn(`[TradeMonitor] Limit order ${orderId} for ${pending.symbol} timed out after ${Math.round(elapsed / 60000)}m. Cancelling...`);

            const cancelOrder = this.exchange.cancelOrder;
            if (cancelOrder) {
                const cancelled = await this.instrumentExchange('cancel_order', () => cancelOrder.call(this.exchange, orderId));
                if (cancelled) {
                    pendingOrders.delete(orderId);
                    logger.info(`[TradeMonitor] Limit order ${orderId} cancelled due to timeout.`);
                    try {
                        await supabaseService.updateOrderStatus(orderId, 'canceled');
                    } catch (e) {
                        logger.warn(`[TradeMonitor] Failed to update DB status for timed-out order ${orderId}: ${e}`);
                    }
                }
            } else {
                logger.warn('[TradeMonitor] Exchange does not support cancelOrder. Removing from local tracking.');
                pendingOrders.delete(orderId);
                try {
                    await supabaseService.updateOrderStatus(orderId, 'canceled');
                } catch (e) {
                    logger.warn(`[TradeMonitor] Failed to update DB status for timed-out order ${orderId}: ${e}`);
                }
            }
        }
    }

    private async instrumentExchange<T>(operation: string, fn: () => Promise<T>): Promise<T> {
        const start = Date.now();
        try {
            return await fn();
        } finally {
            observabilityService.observeExchangeLatency(operation, Date.now() - start);
        }
    }

    private shouldEmitExchangeMismatch(profileId: string, symbol: string): boolean {
        const throttleMs = Math.max(1, Number(config.EXCHANGE_STATE_MISMATCH_THROTTLE_MS || 300000));
        const key = `${profileId || 'unknown'}::${symbol}`;
        const now = Date.now();
        const lastTs = this.mismatchEventLastEmitted.get(key) || 0;
        if (now - lastTs < throttleMs) {
            return false;
        }
        this.mismatchEventLastEmitted.set(key, now);
        return true;
    }

    private shouldEmitExitEvidenceGap(profileId: string, symbol: string): boolean {
        const throttleMs = Math.max(1, Number(config.EXCHANGE_STATE_MISMATCH_THROTTLE_MS || 300000));
        const key = `${profileId || 'unknown'}::${symbol}`;
        const now = Date.now();
        const lastTs = this.exitEvidenceGapLastEmitted.get(key) || 0;
        if (now - lastTs < throttleMs) {
            return false;
        }
        this.exitEvidenceGapLastEmitted.set(key, now);
        return true;
    }

    private getOrderTimestampMs(order: any): number {
        const candidates = [
            order?.filled_at,
            order?.updated_at,
            order?.submitted_at,
            order?.created_at,
            order?.timestamp
        ];
        for (const candidate of candidates) {
            if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
                return candidate > 1_000_000_000_000 ? candidate : candidate * 1000;
            }
            const parsed = Date.parse(String(candidate || '').trim());
            if (Number.isFinite(parsed) && parsed > 0) return parsed;
        }
        return 0;
    }

    private getOrderFillPrice(order: any): number {
        const candidates = [order?.filled_avg_price, order?.avg_price, order?.price];
        for (const candidate of candidates) {
            const parsed = Number(candidate);
            if (Number.isFinite(parsed) && parsed > 0) return parsed;
        }
        return 0;
    }

    private isFilledLikeOrder(order: any): boolean {
        const status = String(order?.status || '').trim().toLowerCase().replace(/-/g, '_');
        return status === 'filled' || status === 'partially_filled';
    }

    private normalizeOrderSide(sideRaw: unknown): 'BUY' | 'SELL' | '' {
        const side = String(sideRaw || '').trim().toUpperCase();
        if (side === 'BUY' || side === 'LONG') return 'BUY';
        if (side === 'SELL' || side === 'SHORT') return 'SELL';
        return '';
    }

    private getOrderIdentity(order: any): string {
        return String(order?.id || order?.order_id || order?.client_order_id || '').trim();
    }

    private selectLatestClosedFill(orders: any[]): { price: number; source: string; orderId: string } | null {
        const ranked = (orders || [])
            .map((order) => ({
                isFilledLike: this.isFilledLikeOrder(order),
                orderId: this.getOrderIdentity(order),
                price: this.getOrderFillPrice(order),
                ts: this.getOrderTimestampMs(order)
            }))
            .filter((row) => row.isFilledLike && row.price > 0);
        if (ranked.length === 0) return null;
        ranked.sort((a, b) => (b.ts - a.ts) || b.orderId.localeCompare(a.orderId));
        return {
            price: ranked[0].price,
            source: 'exchange_fill',
            orderId: ranked[0].orderId
        };
    }

    private findClosedFillForPosition(
        localPos: { tradeId?: string; profileId?: string; side?: string },
        orders: any[],
        usedOrderIds: Set<string>
    ): { price: number; source: string; orderId: string } | null {
        const tradeId = String(localPos.tradeId || '').trim();
        const profileId = String(localPos.profileId || '').trim();
        const expectedExitSide = this.normalizeOrderSide(localPos.side === 'BUY' ? 'SELL' : localPos.side === 'SELL' ? 'BUY' : '');

        const candidates = (orders || [])
            .map((order) => {
                const orderId = this.getOrderIdentity(order);
                const price = this.getOrderFillPrice(order);
                const ts = this.getOrderTimestampMs(order);
                const isFilledLike = this.isFilledLikeOrder(order);
                const orderSide = this.normalizeOrderSide(order?.side);
                const subTag = extractOrderSubTag(order);
                const orderTradeId = String(order?.trade_id || order?.tradeId || '').trim();
                const clientOrderId = String(order?.client_order_id || order?.clientOrderId || '').trim();
                return { order, orderId, price, ts, isFilledLike, orderSide, subTag, orderTradeId, clientOrderId };
            })
            .filter((row) => {
                if (!row.orderId || usedOrderIds.has(row.orderId)) return false;
                if (!row.isFilledLike) return false;
                if (!(row.price > 0)) return false;
                if (expectedExitSide && row.orderSide && row.orderSide !== expectedExitSide) return false;
                return true;
            })
            .sort((a, b) => (b.ts - a.ts) || b.orderId.localeCompare(a.orderId));

        const pick = (
            predicate: (row: {
                order: any;
                orderId: string;
                price: number;
                ts: number;
                isFilledLike: boolean;
                orderSide: 'BUY' | 'SELL' | '';
                subTag: string;
                orderTradeId: string;
                clientOrderId: string;
            }) => boolean,
            source: string
        ) => {
            const selected = candidates.find(predicate);
            if (!selected) return null;
            return {
                price: selected.price,
                source,
                orderId: selected.orderId
            };
        };

        if (tradeId) {
            const byTradeId = pick((row) => row.orderTradeId === tradeId, 'exchange_fill:trade_id');
            if (byTradeId) return byTradeId;

            const bySubTagTrade = pick((row) => row.subTag.length > 0 && subTagHintsTrade(row.subTag, tradeId), 'exchange_fill:sub_tag_trade');
            if (bySubTagTrade) return bySubTagTrade;

            const byClientTrade = pick((row) => row.clientOrderId.includes(tradeId), 'exchange_fill:client_order_trade');
            if (byClientTrade) return byClientTrade;
        }

        if (profileId) {
            const byProfileSubTag = pick((row) => row.subTag.length > 0 && subTagBelongsToProfile(row.subTag, profileId), 'exchange_fill:sub_tag_profile');
            if (byProfileSubTag) return byProfileSubTag;
        }

        return pick(() => true, 'exchange_fill:latest');
    }
}
