use crate::fetcher::BinanceS3Client;
use crate::parser;
use chrono::{Datelike, Duration, NaiveDate, Utc};
use sqlx::PgPool;
use swym_dal::models::ingest_config::ActiveIngestConfigRow;
use swym_dal::repo::{ingest_config, market_event};
use tracing::{debug, info, warn};

/// Monthly archives on data.binance.vision are published with approximately this lag.
const MONTHLY_LAG_DAYS: i64 = 45;

/// After this many consecutive daily-file misses we assume we have reached the earliest
/// available data for this instrument and stop iterating further back.
const MAX_CONSECUTIVE_MISSES: u32 = 30;

/// Earliest possible date by source type.
fn binance_epoch(source_type: &str) -> NaiveDate {
    match source_type {
        "futures_um" => NaiveDate::from_ymd_opt(2019, 9, 8).unwrap(),
        "futures_cm" => NaiveDate::from_ymd_opt(2020, 8, 10).unwrap(),
        _ => NaiveDate::from_ymd_opt(2017, 7, 1).unwrap(),
    }
}

/// Process one ingest config: fetch missing data, insert it, then purge beyond retention.
pub async fn process_config(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    batch_size: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let today = Utc::now().date_naive();
    let yesterday = today - Duration::days(1);
    let trade_retention_start = today - Duration::days(cfg.retention_days as i64);
    let trade_cutoff = Utc::now() - Duration::days(cfg.retention_days as i64);

    // First date we need to fetch.
    // - No cursor: start from the retention window start (initial backfill).
    // - Cursor set: resume from the day after the last successful fetch, clamped so we never
    //   fetch data older than the current retention window (avoids redundant S3 downloads when
    //   the cursor is already ahead of the retention start).
    // NOTE: if retention_days is increased, existing data older than the old retention_start will
    // not be automatically backfilled; reset last_ingested_date to NULL to trigger a full refetch.
    let start_date = match cfg.last_ingested_date {
        Some(cursor) => std::cmp::max(cursor + Duration::days(1), trade_retention_start),
        None => trade_retention_start,
    };

    // Sync trades unless already up to date.
    if start_date > yesterday {
        debug!(symbol = %cfg.symbol, source_type = %cfg.source_type, "trades already up to date");
    } else {
        info!(
            symbol = %cfg.symbol, source_type = %cfg.source_type,
            %start_date,
            %yesterday,
            "beginning ingestion"
        );

        // Monthly archives are available for months that ended at least MONTHLY_LAG_DAYS ago.
        let monthly_cutoff = today - Duration::days(MONTHLY_LAG_DAYS);

        let mut cursor = start_date;
        let mut consecutive_misses: u32 = 0;

        while cursor <= yesterday {
            // Try to consume a full month at a time when the monthly archive is available.
            let month_end = last_day_of_month(cursor.year(), cursor.month());

            let use_monthly = cursor.day() == 1          // start is the first of the month
                && month_end <= yesterday                 // the month is complete
                && month_end < monthly_cutoff;            // the archive is available

            if use_monthly {
                let fetched = fetch_and_insert_monthly(
                    pool,
                    client,
                    cfg,
                    cursor.year(),
                    cursor.month(),
                    batch_size,
                )
                .await?;

                if fetched {
                    consecutive_misses = 0;
                    let advanced = ingest_config::update_last_ingested_date(pool, cfg.id, month_end).await;
                    if let Err(e) = advanced {
                        warn!(?e, "failed to advance ingestion cursor after monthly file");
                    }
                    cursor = month_end + Duration::days(1);
                    continue;
                }
                // Monthly file not available — fall through to daily.
            }

            // Fetch the individual daily file for `cursor`.
            let fetched =
                fetch_and_insert_daily(pool, client, cfg, cursor, batch_size).await?;

            if fetched {
                consecutive_misses = 0;
                let advanced = ingest_config::update_last_ingested_date(pool, cfg.id, cursor).await;
                if let Err(e) = advanced {
                    warn!(?e, "failed to advance ingestion cursor after daily file");
                }
            } else {
                consecutive_misses += 1;
                if consecutive_misses >= MAX_CONSECUTIVE_MISSES {
                    // This many consecutive misses means we've reached the earliest available
                    // data for this instrument (it may have listed after the retention_start).
                    // Advance the cursor past the gap so we don't re-scan on future polls.
                    info!(
                        symbol = %cfg.symbol, source_type = %cfg.source_type,
                        %cursor,
                        consecutive_misses,
                        "no trade data for consecutive days; reached earliest available data",
                    );
                    let advanced = ingest_config::update_last_ingested_date(pool, cfg.id, cursor).await;
                    if let Err(e) = advanced {
                        warn!(?e, "failed to advance ingestion cursor past consecutive misses");
                    }
                    break;
                }
                debug!(symbol = %cfg.symbol, source_type = %cfg.source_type, %cursor, consecutive_misses, "daily file not available, continuing");
            }

            cursor += Duration::days(1);
        }

        // Purge trades beyond the retention window.
        match ingest_config::purge_old_trades(pool, cfg.instrument_id, trade_cutoff).await {
            Ok(0) => {}
            Ok(n) => info!(symbol = %cfg.symbol, source_type = %cfg.source_type, deleted = n, "purged old trades"),
            Err(e) => warn!(symbol = %cfg.symbol, source_type = %cfg.source_type, ?e, "purge old trades failed"),
        }
    }

    // Sync and purge candles for each configured interval (always runs, independent of trades).
    for interval in &cfg.candle_intervals {
        process_candles(pool, client, cfg, interval, batch_size).await?;
        if let Some(candle_ret) = cfg.candle_retention_days {
            let candle_cutoff = Utc::now() - Duration::days(candle_ret as i64);
            match market_event::purge_old_candles(pool, cfg.instrument_id, interval, candle_cutoff).await {
                Ok(0) => {}
                Ok(n) => info!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, deleted = n, "purged old candles"),
                Err(e) => warn!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, ?e, "purge old candles failed"),
            }
        }
        // candle_retention_days = None means unlimited: no purge.
    }

    Ok(())
}

/// Fetch and insert missing kline data for one interval, advancing the cursor per date.
async fn process_candles(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    interval: &str,
    batch_size: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let today = Utc::now().date_naive();
    let yesterday = today - Duration::days(1);
    let candle_retention_start = match cfg.candle_retention_days {
        Some(days) => today - Duration::days(days as i64),
        None => binance_epoch(&cfg.source_type),
    };

    let last_cursor = ingest_config::get_candle_cursor(pool, cfg.instrument_id, interval).await?;

    // Resume from the day after the last successful fetch, clamped to the retention window.
    // Same semantics as the trade cursor: retention expansion requires a cursor reset.
    let start_date = match last_cursor {
        Some(cursor) => std::cmp::max(cursor + Duration::days(1), candle_retention_start),
        None => candle_retention_start,
    };

    if start_date > yesterday {
        debug!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, "candles already up to date");
        return Ok(());
    }

    info!(
        symbol = %cfg.symbol, source_type = %cfg.source_type,
        interval,
        %start_date,
        %yesterday,
        "beginning candle ingestion"
    );

    let monthly_cutoff = today - Duration::days(MONTHLY_LAG_DAYS);
    let mut cursor = start_date;
    let mut consecutive_misses: u32 = 0;

    while cursor <= yesterday {
        let month_end = last_day_of_month(cursor.year(), cursor.month());
        let use_monthly = cursor.day() == 1
            && month_end <= yesterday
            && month_end < monthly_cutoff;

        if use_monthly {
            let fetched = fetch_and_insert_klines_monthly(
                pool, client, cfg, interval, cursor.year(), cursor.month(), batch_size,
            )
            .await?;

            if fetched {
                consecutive_misses = 0;
                if let Err(e) =
                    ingest_config::update_candle_cursor(pool, cfg.instrument_id, interval, month_end).await
                {
                    warn!(?e, "failed to advance candle cursor after monthly file");
                }
                cursor = month_end + Duration::days(1);
                continue;
            }
        }

        let fetched =
            fetch_and_insert_klines_daily(pool, client, cfg, interval, cursor, batch_size).await?;

        if fetched {
            consecutive_misses = 0;
            if let Err(e) =
                ingest_config::update_candle_cursor(pool, cfg.instrument_id, interval, cursor).await
            {
                warn!(?e, "failed to advance candle cursor after daily file");
            }
        } else {
            consecutive_misses += 1;
            if consecutive_misses >= MAX_CONSECUTIVE_MISSES {
                // Reached the earliest available data for this instrument/interval.
                // Advance the cursor so we don't re-scan this gap on future polls.
                info!(
                    symbol = %cfg.symbol, source_type = %cfg.source_type,
                    interval,
                    %cursor,
                    consecutive_misses,
                    "no candle data for consecutive days; reached earliest available data",
                );
                if let Err(e) =
                    ingest_config::update_candle_cursor(pool, cfg.instrument_id, interval, cursor).await
                {
                    warn!(?e, "failed to advance candle cursor past consecutive misses");
                }
                break;
            }
            debug!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, %cursor, consecutive_misses, "daily kline file not available, continuing");
        }

        cursor += Duration::days(1);
    }

    Ok(())
}

async fn fetch_and_insert_klines_monthly(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    interval: &str,
    year: i32,
    month: u32,
    batch_size: usize,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
    let Some(bytes) = client.fetch_monthly_klines(&cfg.symbol, interval, year, month, &cfg.source_type).await? else {
        return Ok(false);
    };
    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, year, month, bytes = bytes.len(), "fetched monthly kline file");
    insert_klines_zip(pool, cfg, interval, &bytes, batch_size).await?;
    Ok(true)
}

async fn fetch_and_insert_klines_daily(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    interval: &str,
    date: NaiveDate,
    batch_size: usize,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
    let Some(bytes) = client.fetch_daily_klines(&cfg.symbol, interval, date, &cfg.source_type).await? else {
        return Ok(false);
    };
    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, %date, bytes = bytes.len(), "fetched daily kline file");
    insert_klines_zip(pool, cfg, interval, &bytes, batch_size).await?;
    Ok(true)
}

async fn insert_klines_zip(
    pool: &PgPool,
    cfg: &ActiveIngestConfigRow,
    interval: &str,
    bytes: &[u8],
    batch_size: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let candles_iter =
        parser::parse_klines_zip(std::io::Cursor::new(bytes), cfg.instrument_id, interval)?;

    let mut batch = Vec::with_capacity(batch_size);
    let mut total = 0usize;

    for result in candles_iter {
        batch.push(result?);
        if batch.len() >= batch_size {
            total += batch.len();
            market_event::batch_insert_candles(pool, &batch).await?;
            batch.clear();
        }
    }

    if !batch.is_empty() {
        total += batch.len();
        market_event::batch_insert_candles(pool, &batch).await?;
    }

    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, interval, inserted = total, "kline batch insert complete");
    Ok(())
}

async fn fetch_and_insert_monthly(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    year: i32,
    month: u32,
    batch_size: usize,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
    let Some(bytes) = client.fetch_monthly(&cfg.symbol, year, month, &cfg.source_type).await? else {
        return Ok(false);
    };

    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, year, month, bytes = bytes.len(), "fetched monthly file");
    insert_zip(pool, cfg, &bytes, batch_size).await?;
    Ok(true)
}

async fn fetch_and_insert_daily(
    pool: &PgPool,
    client: &BinanceS3Client,
    cfg: &ActiveIngestConfigRow,
    date: NaiveDate,
    batch_size: usize,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
    let Some(bytes) = client.fetch_daily(&cfg.symbol, date, &cfg.source_type).await? else {
        return Ok(false);
    };

    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, %date, bytes = bytes.len(), "fetched daily file");
    insert_zip(pool, cfg, &bytes, batch_size).await?;
    Ok(true)
}

async fn insert_zip(
    pool: &PgPool,
    cfg: &ActiveIngestConfigRow,
    bytes: &[u8],
    batch_size: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let time_received = Utc::now();
    let trades_iter =
        parser::parse_zip(std::io::Cursor::new(bytes), cfg.instrument_id, time_received)?;

    let mut batch = Vec::with_capacity(batch_size);
    let mut total = 0usize;

    for result in trades_iter {
        batch.push(result?);
        if batch.len() >= batch_size {
            total += batch.len();
            market_event::batch_insert_trades(pool, &batch).await?;
            batch.clear();
        }
    }

    if !batch.is_empty() {
        total += batch.len();
        market_event::batch_insert_trades(pool, &batch).await?;
    }

    info!(symbol = %cfg.symbol, source_type = %cfg.source_type, inserted = total, "batch insert complete");
    Ok(())
}

fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
    let (next_year, next_month) = if month == 12 {
        (year + 1, 1)
    } else {
        (year, month + 1)
    };
    NaiveDate::from_ymd_opt(next_year, next_month, 1).unwrap() - Duration::days(1)
}
