import ccxt from 'ccxt';
import type { BacktestRawCandle, HistoricalDataset } from '../types.js';
import { buildDatasetFromRows, normalizeRawCandles } from './normalize.js';

const ONE_MINUTE_MS = 60 * 1000;
const FIFTEEN_MINUTES_MS = 15 * 60 * 1000;
const DEFAULT_LOOKBACK_CANDLES = 2_000;
const DEFAULT_LIMIT = 720;

const symbolCache = new Map<string, BacktestRawCandle[]>();
const BASE_SYMBOL_ALIASES: Record<string, string[]> = {
    BTC: ['XBT'],
    XBT: ['BTC']
};

const clampPositiveInt = (value: unknown, fallback: number): number => {
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
    return Math.max(1, Math.floor(parsed));
};

const toIso = (timestamp: number): string => new Date(timestamp).toISOString();

const splitSymbol = (symbol: string): { base: string; quote: string } | null => {
    const [base, quote] = String(symbol || '').trim().toUpperCase().split('/');
    if (!base || !quote) return null;
    return { base, quote };
};

const buildSymbolCandidates = (requested: string): string[] => {
    const normalized = String(requested || '').trim().toUpperCase();
    const out = new Set<string>();
    out.add(normalized);

    const parts = splitSymbol(normalized);
    if (!parts) return Array.from(out);

    const baseAliases = BASE_SYMBOL_ALIASES[parts.base] || [];
    for (const alias of baseAliases) {
        out.add(`${alias}/${parts.quote}`);
    }
    return Array.from(out);
};

const resolveKrakenSymbol = (markets: Record<string, unknown>, requested: string): string => {
    const candidates = buildSymbolCandidates(requested);
    for (const candidate of candidates) {
        if (candidate in markets) {
            return candidate;
        }
    }

    const requestedParts = splitSymbol(requested);
    const suffix = requestedParts ? `/${requestedParts.quote}` : '';
    const related = Object.keys(markets)
        .filter((symbol) => suffix && symbol.endsWith(suffix))
        .slice(0, 8);

    const relatedHint = related.length ? ` Similar Kraken symbols: ${related.join(', ')}.` : '';
    throw new Error(`Kraken market symbol not found for ${requested}.${relatedHint}`);
};

const toBacktestRows = (
    symbol: string,
    timeframe: '1m' | '15m',
    rows: number[][]
): BacktestRawCandle[] => rows
    .filter((row) => Array.isArray(row) && row.length >= 6)
    .map((row) => ({
        symbol,
        timeframe,
        timestamp: row[0],
        open: row[1],
        high: row[2],
        low: row[3],
        close: row[4],
        volume: row[5]
    }));

const fetchSymbolRows = async (
    exchange: InstanceType<typeof ccxt.kraken>,
    requestedSymbol: string,
    exchangeSymbol: string,
    timeframe: '1m' | '15m',
    fromTs: number,
    toTs: number,
    lookbackCandles: number,
    limitPerRequest: number,
    disableCache: boolean
): Promise<BacktestRawCandle[]> => {
    const timeframeMs = timeframe === '1m' ? ONE_MINUTE_MS : FIFTEEN_MINUTES_MS;
    const startTs = Math.max(0, fromTs - (lookbackCandles * timeframeMs));
    const cacheKey = `${requestedSymbol}|${exchangeSymbol}|${timeframe}|${startTs}|${toTs}|${limitPerRequest}`;
    if (!disableCache) {
        const cached = symbolCache.get(cacheKey);
        if (cached) {
            return [...cached];
        }
    }

    const out: number[][] = [];
    let since = startTs;
    let page = 0;
    const maxPages = 2_000;

    while (since <= toTs && page < maxPages) {
        page += 1;
        const batch = await exchange.fetchOHLCV(exchangeSymbol, timeframe, since, limitPerRequest);
        if (!Array.isArray(batch) || batch.length === 0) break;

        let lastTimestamp = Number(batch[batch.length - 1]?.[0] ?? NaN);
        if (!Number.isFinite(lastTimestamp)) break;

        for (const row of batch) {
            if (!Array.isArray(row) || row.length < 6) continue;
            const ts = Number(row[0]);
            if (!Number.isFinite(ts)) continue;
            if (ts < startTs) continue;
            if (ts > toTs) break;
            out.push(row as number[]);
        }

        if (lastTimestamp >= toTs) break;
        const nextSince = lastTimestamp + timeframeMs;
        if (nextSince <= since) {
            since += timeframeMs;
        } else {
            since = nextSince;
        }
    }

    const rows = toBacktestRows(requestedSymbol, timeframe, out);
    if (!disableCache) {
        symbolCache.set(cacheKey, rows);
    }
    return rows;
};

const buildNoCandleError = async (
    exchange: InstanceType<typeof ccxt.kraken>,
    requestedSymbol: string,
    exchangeSymbol: string,
    timeframe: '1m' | '15m',
    fromTs: number,
    toTs: number
): Promise<Error> => {
    const base = `Kraken returned no ${timeframe} OHLC candles for ${requestedSymbol} (${exchangeSymbol}) in selected window ${toIso(fromTs)} -> ${toIso(toTs)}.`;
    try {
        const latest = await exchange.fetchOHLCV(exchangeSymbol, timeframe, undefined, 2);
        if (Array.isArray(latest) && latest.length > 0) {
            const firstTs = Number(latest[0]?.[0]);
            const lastTs = Number(latest[latest.length - 1]?.[0]);
            if (Number.isFinite(firstTs) && Number.isFinite(lastTs)) {
                if (firstTs > toTs) {
                    return new Error(
                        `${base} Kraken currently returns candles from ${toIso(firstTs)} onward for this pair. Choose a later replay window or use CSV/JSON historical data for older ranges.`
                    );
                }
                if (lastTs < fromTs) {
                    return new Error(
                        `${base} Latest Kraken candle is ${toIso(lastTs)}, which is earlier than your replay start. Choose an earlier window end.`
                    );
                }
                return new Error(
                    `${base} Kraken has data near ${toIso(firstTs)} -> ${toIso(lastTs)} for this pair. Verify symbol and window alignment (UTC), or use CSV/JSON data.`
                );
            }
        }
    } catch {
        // Fall through to generic guidance below.
    }
    return new Error(`${base} Verify symbol format and UTC window, or use CSV/JSON historical data.`);
};

export interface KrakenLoaderInput {
    symbols: string[];
    fromTs: number;
    toTs: number;
    lookbackCandles?: number;
    limitPerRequest?: number;
    includeOneMinute?: boolean;
    disableCache?: boolean;
}

export const loadDatasetFromKraken = async ({
    symbols,
    fromTs,
    toTs,
    lookbackCandles,
    limitPerRequest,
    includeOneMinute = false,
    disableCache = false
}: KrakenLoaderInput): Promise<HistoricalDataset> => {
    const uniqueSymbols = Array.from(new Set(
        (symbols || [])
            .map((symbol) => String(symbol || '').trim().toUpperCase())
            .filter(Boolean)
    )).sort((a, b) => a.localeCompare(b));
    if (!uniqueSymbols.length) {
        throw new Error('Kraken loader requires at least one symbol.');
    }
    if (!Number.isFinite(fromTs) || !Number.isFinite(toTs) || toTs < fromTs) {
        throw new Error('Invalid replay window for Kraken loader.');
    }

    const lookback = clampPositiveInt(lookbackCandles, DEFAULT_LOOKBACK_CANDLES);
    const oneMinuteLookback = Math.max(lookback * 15, 500);
    const limit = clampPositiveInt(limitPerRequest, DEFAULT_LIMIT);

    const exchange = new ccxt.kraken({
        enableRateLimit: true
    });

    try {
        await exchange.loadMarkets();
        const markets = (exchange.markets || {}) as Record<string, unknown>;

        const rows: BacktestRawCandle[] = [];
        for (const symbol of uniqueSymbols) {
            const exchangeSymbol = resolveKrakenSymbol(markets, symbol);
            const fifteenMinuteRows = await fetchSymbolRows(
                exchange,
                symbol,
                exchangeSymbol,
                '15m',
                fromTs,
                toTs,
                lookback,
                limit,
                disableCache
            );
            if (!fifteenMinuteRows.length) {
                throw await buildNoCandleError(exchange, symbol, exchangeSymbol, '15m', fromTs, toTs);
            }
            rows.push(...fifteenMinuteRows);

            if (includeOneMinute) {
                const oneMinuteRows = await fetchSymbolRows(
                    exchange,
                    symbol,
                    exchangeSymbol,
                    '1m',
                    fromTs,
                    toTs,
                    oneMinuteLookback,
                    limit,
                    disableCache
                );
                rows.push(...oneMinuteRows);
            }
        }
        return buildDatasetFromRows(normalizeRawCandles(rows));
    } finally {
        await exchange.close();
    }
};
