import type { Candle } from '../../connectors/types.js';
import type { BacktestRawCandle, BacktestTimeframe, HistoricalDataset, NormalizedBacktestCandle } from '../types.js';

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

const toNumber = (value: unknown, field: string): number => {
    const parsed = Number(value);
    if (!Number.isFinite(parsed)) {
        throw new Error(`Invalid numeric value for ${field}: ${String(value)}`);
    }
    return parsed;
};

const toTimestamp = (value: unknown): number => {
    if (typeof value === 'number' && Number.isFinite(value)) return value;
    if (typeof value === 'string') {
        const asNum = Number(value);
        if (Number.isFinite(asNum)) return asNum;
        const parsed = Date.parse(value);
        if (Number.isFinite(parsed)) return parsed;
    }
    throw new Error(`Invalid timestamp: ${String(value)}`);
};

export const normalizeTimeframe = (value: string): BacktestTimeframe => {
    const normalized = String(value || '').trim().toLowerCase();
    if (normalized === '1m' || normalized === '1min' || normalized === '1') return '1m';
    if (normalized === '15m' || normalized === '15min' || normalized === '15') return '15m';
    if (normalized === '1h' || normalized === '60m' || normalized === '60min') return '1h';
    if (normalized === '4h' || normalized === '240m' || normalized === '240min') return '4h';
    throw new Error(`Unsupported timeframe: ${value}`);
};

export const normalizeRawCandle = (row: BacktestRawCandle): NormalizedBacktestCandle => {
    const symbol = String(row.symbol || '').trim().toUpperCase();
    if (!symbol) throw new Error('Candle symbol is required.');
    const timeframe = normalizeTimeframe(row.timeframe);
    const timestamp = toTimestamp(row.timestamp);
    const open = toNumber(row.open, 'open');
    const high = toNumber(row.high, 'high');
    const low = toNumber(row.low, 'low');
    const close = toNumber(row.close, 'close');
    const volume = toNumber(row.volume, 'volume');

    if (high < low) {
        throw new Error(`Invalid candle range for ${symbol} @ ${timestamp}: high < low`);
    }
    if (open <= 0 || high <= 0 || low <= 0 || close <= 0) {
        throw new Error(`Non-positive OHLC value for ${symbol} @ ${timestamp}`);
    }
    if (volume < 0) {
        throw new Error(`Negative volume for ${symbol} @ ${timestamp}`);
    }

    return { symbol, timeframe, timestamp, open, high, low, close, volume };
};

export const normalizeRawCandles = (rows: BacktestRawCandle[]): NormalizedBacktestCandle[] =>
    rows.map(normalizeRawCandle);

const stableSortCandles = (rows: NormalizedBacktestCandle[]): NormalizedBacktestCandle[] =>
    [...rows].sort((a, b) => {
        if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
        if (a.symbol !== b.symbol) return a.symbol.localeCompare(b.symbol);
        if (a.timeframe !== b.timeframe) return a.timeframe.localeCompare(b.timeframe);
        return 0;
    });

const bucketTimestamp = (timestamp: number, timeframe: BacktestTimeframe): number => {
    const interval = TF_TO_MS[timeframe];
    return Math.floor(timestamp / interval) * interval;
};

const assertAlignment = (timestamp: number, timeframe: BacktestTimeframe): void => {
    const interval = TF_TO_MS[timeframe];
    if (timestamp % interval !== 0) {
        throw new Error(`Timestamp ${timestamp} is not aligned to ${timeframe}.`);
    }
};

export const aggregateCandles = (candles: Candle[], targetTimeframe: '15m' | '1h' | '4h'): Candle[] => {
    const grouped = new Map<number, Candle[]>();
    for (const candle of candles) {
        const bucket = bucketTimestamp(candle.timestamp, targetTimeframe);
        const group = grouped.get(bucket) || [];
        group.push(candle);
        grouped.set(bucket, group);
    }

    const out: Candle[] = [];
    const orderedBuckets = Array.from(grouped.keys()).sort((a, b) => a - b);
    for (const bucket of orderedBuckets) {
        const group = (grouped.get(bucket) || []).sort((a, b) => a.timestamp - b.timestamp);
        if (!group.length) continue;
        const first = group[0];
        const last = group[group.length - 1];
        out.push({
            timestamp: bucket,
            open: first.open,
            high: Math.max(...group.map((value) => value.high)),
            low: Math.min(...group.map((value) => value.low)),
            close: last.close,
            volume: group.reduce((sum, value) => sum + value.volume, 0)
        });
    }
    return out;
};

const toDatasetSkeleton = (): HistoricalDataset => ({});

export const buildDatasetFromRows = (rows: NormalizedBacktestCandle[]): HistoricalDataset => {
    const sorted = stableSortCandles(rows);
    const dataset = toDatasetSkeleton();

    for (const row of sorted) {
        assertAlignment(row.timestamp, row.timeframe);
        if (!dataset[row.symbol]) {
            dataset[row.symbol] = {
                '1m': [],
                '15m': [],
                '1h': [],
                '4h': []
            };
        }
        dataset[row.symbol][row.timeframe].push({
            timestamp: row.timestamp,
            open: row.open,
            high: row.high,
            low: row.low,
            close: row.close,
            volume: row.volume
        });
    }

    for (const symbol of Object.keys(dataset)) {
        const frames = dataset[symbol];
        if (!frames['15m'].length && frames['1m'].length) {
            frames['15m'] = aggregateCandles(frames['1m'], '15m');
        }
        if (!frames['15m'].length) {
            throw new Error(`Symbol ${symbol} is missing required 15m candles (or 1m source for aggregation).`);
        }
        if (!frames['1h'].length) {
            frames['1h'] = aggregateCandles(frames['15m'], '1h');
        }
        if (!frames['4h'].length) {
            frames['4h'] = aggregateCandles(frames['15m'], '4h');
        }
    }

    return dataset;
};

export const selectDatasetForReplayWindow = (
    input: HistoricalDataset,
    symbols: string[],
    fromTs: number,
    toTs: number
): HistoricalDataset => {
    const requested = new Set(symbols.map((value) => String(value || '').trim().toUpperCase()).filter(Boolean));
    if (!requested.size) {
        throw new Error('At least one symbol is required.');
    }
    if (!Number.isFinite(fromTs) || !Number.isFinite(toTs) || toTs < fromTs) {
        throw new Error('Invalid backtest date range.');
    }

    const out: HistoricalDataset = {};
    for (const symbol of Array.from(requested).sort((a, b) => a.localeCompare(b))) {
        const source = input[symbol];
        if (!source) {
            throw new Error(`Historical dataset does not include symbol ${symbol}.`);
        }

        out[symbol] = {
            // Keep candles before fromTs so indicator warm-up can be computed pre-window.
            '1m': source['1m'].filter((candle) => candle.timestamp <= toTs),
            '15m': source['15m'].filter((candle) => candle.timestamp <= toTs),
            '1h': source['1h'].filter((candle) => candle.timestamp <= toTs),
            '4h': source['4h'].filter((candle) => candle.timestamp <= toTs)
        };

        if (!out[symbol]['15m'].length || !out[symbol]['1h'].length || !out[symbol]['4h'].length) {
            throw new Error(`Insufficient candles for ${symbol} at or before replay end.`);
        }

        const hasReplaySlice = out[symbol]['15m'].some((candle) => candle.timestamp >= fromTs && candle.timestamp <= toTs);
        if (!hasReplaySlice) {
            throw new Error(`No replay candles found for ${symbol} in selected window.`);
        }
    }
    return out;
};
