import Alpaca from '@alpacahq/alpaca-trade-api';
import { config } from '../config/index.js';
import logger from '../utils/logger.js';
import { SymbolMapper } from '../utils/symbolMapper.js';
import { AccountSnapshot, IExchangeConnector, Candle, ExchangeCapabilities, ExchangeOrderCorrelation } from './types.js';

export class AlpacaConnector implements IExchangeConnector {
    private client: any;

    constructor(apiKey?: string, apiSecret?: string) {
        this.client = new (Alpaca as any)({
            keyId: apiKey || config.ALPACA_API_KEY,
            secretKey: apiSecret || config.ALPACA_API_SECRET,
            paper: config.PAPER_TRADING,
        });
    }

    public getCapabilities(): ExchangeCapabilities {
        return {
            fetchOpenOrders: true,
            fetchClosedOrders: true,
            shorting: config.ASSET_CLASS !== 'crypto', // Alpaca crypto shorting is limited
            margin: config.ASSET_CLASS !== 'crypto',
            leverage: config.ASSET_CLASS !== 'crypto',
            tradingWindow: true
        };
    }

    private mapTimeframe(tf: string): string {
        const map: { [key: string]: string } = {
            '1m': '1Min',
            '5m': '5Min',
            '15m': '15Min',
            '1h': '1Hour',
            '4h': '4Hour',
            '1d': '1Day',
            '1Min': '1Min',
            '1Hour': '1Hour',
            '1Day': '1Day'
        };
        return map[tf] || tf;
    }

    async fetchOHLCV(symbol: string, timeframe: string, limit: number = 100): Promise<Candle[]> {
        if (!this.client?.getBarsV2) {
            logger.warn(`[Alpaca] getBarsV2 capability missing. Skipping fetchOHLCV for ${symbol}`);
            return [];
        }
        try {
            const formattedSymbol = symbol.replace('/', '');
            let mappedTf = this.mapTimeframe(timeframe);
            let fetchLimit = limit;
            let needsAggregation = false;

            // Alpaca V2 Crypto does NOT support 4Hour. We must fetch 1Hour and aggregate.
            if (timeframe === '4h' || timeframe === '4Hour') {
                mappedTf = '1Hour';
                fetchLimit = limit * 4;
                needsAggregation = true;
                logger.info(`[Alpaca] 🔄 Aggregating 4h bars from 1h feed for ${symbol}`);
            }

            const options: any = {
                start: new Date(Date.now() - 3600000 * 24 * 7).toISOString(), // 7 day lookback to be safe
                timeframe: mappedTf,
                limit: fetchLimit,
            };

            if (config.ASSET_CLASS !== 'crypto') {
                options.feed = 'iex';
            }

            const barsGenerator = this.client.getBarsV2(formattedSymbol, options);
            const candles: Candle[] = [];
            for await (const bar of barsGenerator) {
                candles.push({
                    timestamp: new Date(bar.Timestamp).getTime(),
                    open: bar.OpenPrice,
                    high: bar.HighPrice,
                    low: bar.LowPrice,
                    close: bar.ClosePrice,
                    volume: bar.Volume,
                });
            }

            if (candles.length === 0) {
                logger.warn(`[Alpaca] No bars found for ${formattedSymbol}.`);
                return [];
            }

            const sorted = candles.sort((a, b) => a.timestamp - b.timestamp);

            if (needsAggregation) {
                return this.aggregateBars(sorted, 4);
            }

            return sorted;

        } catch (error: any) {
            logger.error(`[Alpaca] Error: ${error.message || error}`);
            throw error;
        }
    }

    private aggregateBars(candles: Candle[], factor: number): Candle[] {
        const aggregated: Candle[] = [];
        for (let i = 0; i < candles.length; i += factor) {
            const chunk = candles.slice(i, i + factor);
            if (chunk.length === 0) continue;

            aggregated.push({
                timestamp: chunk[0].timestamp,
                open: chunk[0].open,
                high: Math.max(...chunk.map(c => c.high)),
                low: Math.min(...chunk.map(c => c.low)),
                close: chunk[chunk.length - 1].close,
                volume: chunk.reduce((sum, c) => sum + c.volume, 0)
            });
        }
        return aggregated;
    }

    private sanitizeClientOrderToken(value: unknown, fallback: string, maxLength: number): string {
        const cleaned = String(value || '')
            .trim()
            .replace(/[^A-Za-z0-9_-]/g, '');
        const token = cleaned || fallback;
        if (token.length <= maxLength) return token;
        return token.slice(token.length - maxLength);
    }

    private buildCorrelationClientOrderId(correlation?: ExchangeOrderCorrelation): string | undefined {
        const tradeId = String(correlation?.tradeId || '').trim();
        if (!tradeId) return undefined;

        const profileToken = this.sanitizeClientOrderToken(correlation?.profileId || 'global', 'global', 12);
        const tradeToken = this.sanitizeClientOrderToken(tradeId, 'trade', 24);
        const intentToken = this.sanitizeClientOrderToken(correlation?.intent || 'unknown', 'unknown', 8).toLowerCase();
        const candidate = `bytelyst-${profileToken}-${tradeToken}-${intentToken}`;
        return candidate.length <= 48 ? candidate : candidate.slice(0, 48);
    }

    async placeOrder(
        symbol: string,
        side: 'buy' | 'sell',
        qty: number,
        type: 'market' | 'limit',
        price?: number,
        stopLoss?: number,
        takeProfit?: number,
        clientOrderId?: string,
        correlation?: ExchangeOrderCorrelation
    ): Promise<any> {
        try {
            const formattedSymbol = symbol.replace('/', '');
            const orderOptions: any = {
                symbol: formattedSymbol,
                qty,
                side,
                type,
                time_in_force: 'gtc'
            };

            if (type === 'limit' && price) {
                orderOptions.limit_price = price;
            }

            // Bracket Order (One-Cancels-Other for Profit/Stop)
            // NOTE: Alpaca does not support advanced orders for Crypto.
            // Our bot handles local SL/TP monitoring in TradeMonitor.ts, so we can skip exchange-side brackets for crypto.
            const isCrypto = config.ASSET_CLASS === 'crypto' || formattedSymbol.endsWith('USD') || formattedSymbol.endsWith('USDT');

            if ((stopLoss || takeProfit) && !isCrypto) {
                orderOptions.order_class = 'bracket';
                if (takeProfit) {
                    orderOptions.take_profit = { limit_price: takeProfit };
                }
                if (stopLoss) {
                    orderOptions.stop_loss = { stop_price: stopLoss };
                }
            }

            logger.info(`[Alpaca] Placing ${side} order for ${qty} ${formattedSymbol} (Raw: ${symbol})...`);
            const resolvedClientOrderId = clientOrderId || this.buildCorrelationClientOrderId(correlation);
            if (resolvedClientOrderId) {
                orderOptions.client_order_id = resolvedClientOrderId;
            }
            if (correlation?.subTag) {
                // Broker API omnibus field. Keep snake_case variant for compatibility.
                orderOptions.subtag = correlation.subTag;
            }

            if (!this.client?.createOrder) {
                throw new Error("Alpaca client createOrder capability missing");
            }
            const order = await this.client.createOrder(orderOptions);
            if (resolvedClientOrderId && !order?.client_order_id) {
                order.client_order_id = resolvedClientOrderId;
            }
            if (correlation?.subTag) {
                order.subtag = order?.subtag || correlation.subTag;
                order.sub_tag = order?.sub_tag || correlation.subTag;
            }
            return order;
        } catch (error: any) {
            const errorData = error.response?.data ? JSON.stringify(error.response.data) : (error.message || error);
            logger.error(`[Alpaca] Order Error: ${errorData}`);
            throw new Error(errorData);
        }
    }

    async getOrder(orderId: string): Promise<any> {
        if (!this.client?.getOrder) return null;
        try {
            const order = await this.client.getOrder(orderId);
            return order;
        } catch (error: any) {
            logger.error(`[Alpaca] GetOrder Error for ${orderId}: ${error.message || error}`);
            return null;
        }
    }

    async cancelOrder(orderId: string): Promise<boolean> {
        if (!this.client?.cancelOrder) return false;
        try {
            await this.client.cancelOrder(orderId);
            logger.info(`[Alpaca] Order ${orderId} cancelled successfully.`);
            return true;
        } catch (error: any) {
            logger.error(`[Alpaca] CancelOrder Error for ${orderId}: ${error.message || error}`);
            return false;
        }
    }

    async fetchAccountSnapshot(): Promise<AccountSnapshot | null> {
        if (!this.client?.getAccount) return null;
        try {
            const account = await this.client.getAccount();
            const parseNumber = (value: unknown) => {
                const parsed = typeof value === 'string' ? Number(value) : Number(value ?? 0);
                return Number.isFinite(parsed) ? parsed : 0;
            };
            return {
                buying_power: parseNumber(account.buying_power || account.buyingPower),
                cash: parseNumber(account.cash),
                currency: String(account.currency || 'USD'),
                timestamp: Date.now()
            };
        } catch (error: any) {
            logger.warn(`[Alpaca] Account snapshot failed: ${error.message || error}`);
            return null;
        }
    }

    private normalizeDataSymbol(symbol: string): string {
        const upper = String(symbol || '').toUpperCase();
        if (!upper) return upper;
        if (upper.includes('/')) return upper;
        if (upper.endsWith('USDT')) {
            return `${upper.slice(0, -4)}/USDT`;
        }
        if (upper.endsWith('USD')) {
            return `${upper.slice(0, -3)}/USD`;
        }
        return upper;
    }

    private buildNormalizedSymbolTargets(symbols?: string[]): Set<string> {
        const targets = new Set<string>();
        if (!symbols || symbols.length === 0) return targets;

        for (const raw of symbols) {
            const input = String(raw || '').trim().toUpperCase();
            if (!input) continue;

            const normalizedInput = this.normalizeDataSymbol(input);
            const tradeVariant = SymbolMapper.toTradeSymbol(normalizedInput, 'alpaca').toUpperCase();
            const dataVariant = SymbolMapper.toDataSymbol(normalizedInput, 'alpaca').toUpperCase();
            const normalizedTradeVariant = this.normalizeDataSymbol(tradeVariant);
            const normalizedDataVariant = this.normalizeDataSymbol(dataVariant);

            targets.add(input);
            targets.add(normalizedInput);
            targets.add(tradeVariant);
            targets.add(dataVariant);
            targets.add(normalizedTradeVariant);
            targets.add(normalizedDataVariant);

            if (normalizedInput.endsWith('/USDT')) {
                targets.add(normalizedInput.replace('/USDT', '/USD'));
            }
            if (normalizedInput.endsWith('/USD')) {
                targets.add(normalizedInput.replace('/USD', '/USDT'));
            }
        }

        return targets;
    }

    async fetchOpenOrders(symbols?: string[]): Promise<any[]> {
        if (!this.client?.getOrders) return [];
        try {
            const opts: any = {
                status: 'open',
                direction: 'desc',
                limit: 200
            };
            const orders: any[] = await this.client.getOrders(opts);
            if (!symbols || symbols.length === 0) {
                return orders;
            }
            const normalizedTargets = this.buildNormalizedSymbolTargets(symbols);
            return orders.filter((order) => {
                const normalized = this.normalizeDataSymbol(order.symbol);
                return normalizedTargets.has(normalized);
            });
        } catch (error: any) {
            logger.error(`[Alpaca] Fetch open orders failed: ${error.message || error}`);
            return [];
        }
    }

    async fetchClosedOrders(
        symbols?: string[],
        options?: {
            after?: Date;
            limit?: number;
            maxPages?: number;
        }
    ): Promise<any[]> {
        if (!this.client?.getOrders) return [];
        try {
            const limit = Math.max(1, Math.min(500, Math.floor(Number(options?.limit || 500))));
            const maxPages = Math.max(1, Math.min(100, Math.floor(Number(options?.maxPages || 1))));
            const normalizedTargets = this.buildNormalizedSymbolTargets(symbols);
            const afterMs = options?.after instanceof Date && Number.isFinite(options.after.getTime())
                ? options.after.getTime()
                : 0;

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

            const includeOrder = (order: any): boolean => {
                if (!symbols || symbols.length === 0) return true;
                const normalized = this.normalizeDataSymbol(order?.symbol);
                return normalizedTargets.has(normalized);
            };

            const deduped = new Map<string, any>();
            let until: Date | undefined;
            let page = 0;
            let reachedPotentialTruncation = false;
            let previousCursorMs = Number.POSITIVE_INFINITY;

            while (page < maxPages) {
                const opts: any = {
                    status: 'closed',
                    direction: 'desc',
                    limit
                };
                if (options?.after instanceof Date && Number.isFinite(options.after.getTime())) {
                    opts.after = options.after;
                }
                if (until instanceof Date && Number.isFinite(until.getTime())) {
                    opts.until = until;
                }

                const batch: any[] = await this.client.getOrders(opts);
                if (!Array.isArray(batch) || batch.length === 0) break;
                page += 1;

                for (const order of batch) {
                    if (!includeOrder(order)) continue;
                    const orderKey = String(order?.id || order?.order_id || order?.client_order_id || '').trim();
                    if (!orderKey) continue;
                    if (!deduped.has(orderKey)) {
                        deduped.set(orderKey, order);
                    }
                }

                let oldestBatchTs = Number.POSITIVE_INFINITY;
                for (const order of batch) {
                    const ts = parseOrderTimestampMs(order);
                    if (ts > 0 && ts < oldestBatchTs) {
                        oldestBatchTs = ts;
                    }
                }

                if (!Number.isFinite(oldestBatchTs) || oldestBatchTs <= 0) break;
                if (afterMs > 0 && oldestBatchTs <= afterMs) break;
                if (batch.length < limit) break;

                const nextCursorMs = oldestBatchTs - 1;
                if (!(nextCursorMs > 0) || nextCursorMs >= previousCursorMs) break;
                previousCursorMs = nextCursorMs;
                until = new Date(nextCursorMs);
            }

            reachedPotentialTruncation = page >= maxPages;
            if (reachedPotentialTruncation) {
                logger.warn(`[Alpaca] fetchClosedOrders reached maxPages=${maxPages}. Results may be truncated for lookback window.`);
            }

            return Array.from(deduped.values());
        } catch (error: any) {
            logger.error(`[Alpaca] Fetch closed orders failed: ${error.message || error}`);
            return [];
        }
    }

    async getPosition(symbol: string): Promise<any> {
        if (!this.client?.getPosition) return null;
        try {
            // Alpaca Positions endpoint usually expects symbols without slashes (e.g. BTCUSD)
            // even if Market Data V2 uses BTC/USD.
            const formattedSymbol = symbol.replace('/', '');
            logger.info(`[Alpaca] Fetching position for ${formattedSymbol} (Raw: ${symbol})...`);
            const position = await this.client.getPosition(formattedSymbol);
            return position;
        } catch (error: any) {
            // Alpaca throws 404 if no position exists
            if (error.message?.includes('404')) {
                // logger.info(`[Alpaca] No position found for ${symbol}`);
                return null;
            }
            const message = error.message || String(error);
            logger.error(`[Alpaca] GetPosition Error for ${symbol}: ${message}`);
            throw new Error(message);
        }
    }

    async isTradingWindowOpen(): Promise<boolean | null> {
        if (config.ASSET_CLASS === 'crypto') {
            return true;
        }

        if (!this.client?.getClock) return null;
        try {
            const clock = await this.client.getClock();
            if (typeof clock?.is_open === 'boolean') {
                return clock.is_open;
            }
            return null;
        } catch (error: any) {
            logger.warn(`[Alpaca] Could not resolve market clock: ${error.message || error}`);
            return null;
        }
    }
}
