# src/web/api/v2/routers/calibration_data.py

import logging
import threading
from datetime import datetime, timedelta
from typing import Dict, Optional, Literal

import pandas as pd

from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy import text

from src.data.storage import StorageManager
from src.data.downloader import OHLCVDownloader
from src.data.download_job import DownloadJob

from ..schemas.calibration_data import (
    CalibrationDataRequest,
    CalibrationDataResponse,
    CalibrationDataDownloadRequest,
    CalibrationDataDownloadResponse,
    CalibrationDataDownloadJobStartRequest,
    CalibrationDataDownloadJobStartResponse,
    CalibrationDataDownloadJobStatusResponse,
    CalibrationDataDownloadJobCancelResponse,
)

logger = logging.getLogger("tradingbot.api.v2")

router = APIRouter(
    prefix="/calibration/data",
    tags=["calibration"],
)

# =================================================
# In-memory job store (Option A)
# =================================================

DOWNLOAD_JOBS: Dict[str, DownloadJob] = {}
DOWNLOAD_JOBS_LOCK = threading.Lock()

# =================================================
# Dependencies
# =================================================

def get_storage() -> StorageManager:
    return StorageManager.from_env()


def get_downloader() -> OHLCVDownloader:
    return OHLCVDownloader(exchange_name="binance")


# =================================================
# Helpers
# =================================================

def _db_summary(
    storage: StorageManager,
    symbol: str,
    timeframe: str,
    start_date: Optional[datetime] = None,
    end_date: Optional[datetime] = None,
):
    query = """
        SELECT
            MIN(timestamp) AS first_ts,
            MAX(timestamp) AS last_ts,
            COUNT(*)        AS candles
        FROM ohlcv
        WHERE symbol = :symbol AND timeframe = :timeframe
    """

    params = {"symbol": symbol, "timeframe": timeframe}

    if start_date:
        query += " AND timestamp >= :start_date"
        params["start_date"] = start_date

    if end_date:
        query += " AND timestamp <= :end_date"
        params["end_date"] = end_date

    with storage.engine.connect() as conn:
        row = conn.execute(text(query), params).mappings().fetchone()

    if not row:
        return {"first_ts": None, "last_ts": None, "candles": 0}

    return {
        "first_ts": row["first_ts"],
        "last_ts": row["last_ts"],
        "candles": int(row["candles"] or 0),
    }

def analyze_data_quality(df: pd.DataFrame, timeframe: str, mode: Literal["crypto", "session"] = "crypto"):
    # --- timeframe a timedelta ---
    tf_map = {
        "1m": timedelta(minutes=1),
        "5m": timedelta(minutes=5),
        "15m": timedelta(minutes=15),
        "30m": timedelta(minutes=30),
        "1h": timedelta(hours=1),
        "4h": timedelta(hours=4),
        "1d": timedelta(days=1),
    }
    tf_delta = tf_map.get(timeframe)
    if not tf_delta or df is None or df.empty:
        return None

    # sanity: DatetimeIndex ordenado
    if not isinstance(df.index, pd.DatetimeIndex):
        return {
            "status": "fail",
            "checks": {"index": "fail"},
            "message": "El dataframe no tiene DatetimeIndex.",
        }
    if not df.index.is_monotonic_increasing:
        df = df.sort_index()

    # --- cobertura ---
    start, end = df.index.min(), df.index.max()
    expected = int((end - start) / tf_delta) + 1
    actual = len(df)
    ratio = actual / expected if expected > 0 else 0.0

    coverage_status = "ok"
    if ratio < 0.98:
        coverage_status = "fail"
    elif ratio < 0.995:
        coverage_status = "warning"

    # --- continuidad / gaps ---
    diffs = df.index.to_series().diff().dropna()

    # Umbrales por timeframe (más realista para 1d)
    if timeframe == "1d":
        warn_gap = tf_delta * 2   # >= 2 días => warning
        fail_gap = tf_delta * 10  # >= 10 días => fail
    else:
        warn_gap = max(tf_delta * 6, timedelta(hours=6))     # intradía
        fail_gap = max(tf_delta * 48, timedelta(days=2))     # intradía

    # Si es mercado por sesiones (acciones), un gap de fin de semana no es "malo".
    # Aproximación simple: para 1d, no consideramos gaps de 2-3 días como warning/fail.
    # (Sin calendario real, esto es la mejor opción práctica).
    diffs_for_eval = diffs
    if mode == "session" and timeframe == "1d":
        diffs_for_eval = diffs[diffs > timedelta(days=3)]  # ignorar 2-3 días típicos de finde

    big_gaps = diffs[diffs > tf_delta * 1.5]
    max_gap = diffs.max() if len(diffs) else pd.NaT

    gaps_warn = diffs_for_eval[diffs_for_eval >= warn_gap]
    gaps_fail = diffs_for_eval[diffs_for_eval >= fail_gap]

    gaps_warn_ratio = (len(gaps_warn) / expected) if expected > 0 else 0.0

    continuity = "ok"
    if pd.notna(max_gap):
        if len(gaps_fail) > 0 or gaps_warn_ratio > 0.02:
            continuity = "fail"
        elif len(gaps_warn) > 0:
            continuity = "warning"

    # --- volumen ---
    zero_vol_ratio = None
    nan_vol_ratio = None
    volume_status = "warning"

    if "volume" in df.columns:
        nan_vol_ratio = df["volume"].isna().mean()
        zero_vol_ratio = (df["volume"].fillna(0) == 0).mean()

        volume_status = "ok"
        if nan_vol_ratio > 0.005 or zero_vol_ratio > 0.01:
            volume_status = "fail"
        elif nan_vol_ratio > 0.0005 or zero_vol_ratio > 0.001:
            volume_status = "warning"

    # --- estado global ---
    statuses = [continuity, coverage_status, volume_status]
    if "fail" in statuses:
        status = "fail"
        msg = "Datos incompletos. Se recomienda revisar o volver a descargar."
    elif "warning" in statuses:
        status = "warning"
        msg = "Datos utilizables con pequeñas discontinuidades."
    else:
        status = "ok"
        msg = "Datos continuos y completos. Aptos para calibración."

    # ⬇️ Mantener shape compatible con tu schema actual
    return {
        "status": status,
        "checks": {
            "continuity": continuity,
            "gaps": {
                "count": int(len(big_gaps)),
                "max_gap": str(max_gap) if pd.notna(max_gap) else None,
            },
            "coverage": {
                "expected": expected,
                "actual": actual,
                "ratio": round(ratio, 4),
            },
            "volume": volume_status,
        },
        "message": msg,
    }

# =================================================
# INSPECT (DB)
# =================================================

@router.post("/inspect", response_model=CalibrationDataResponse)
def inspect_calibration_data(
    payload: CalibrationDataRequest,
    storage: StorageManager = Depends(get_storage),
):
    summary = _db_summary(
        storage,
        payload.symbol,
        payload.timeframe,
        payload.start_date,
        payload.end_date,
    )

    data_quality = None
    if summary["candles"] > 0:
        df = storage.load_ohlcv(
            symbol=payload.symbol,
            timeframe=payload.timeframe,
            start_date=payload.start_date,
            end_date=payload.end_date,
        )
        data_quality = analyze_data_quality(df, payload.timeframe, mode=payload.mode)

    return CalibrationDataResponse(
        symbol=payload.symbol,
        timeframe=payload.timeframe,
        first_available=summary["first_ts"],
        last_available=summary["last_ts"],
        candles_count=summary["candles"],
        valid=summary["candles"] > 0,
        data_quality=data_quality,
    )


# =================================================
# DOWNLOAD (SYNC / legacy)
# =================================================

@router.post("/download", response_model=CalibrationDataDownloadResponse)
def download_calibration_data(
    payload: CalibrationDataDownloadRequest,
    storage: StorageManager = Depends(get_storage),
    downloader: OHLCVDownloader = Depends(get_downloader),
):
    logger.info("⬇️ HIT /calibration/data/download")

    if payload.start_date and payload.end_date and payload.start_date > payload.end_date:
        raise HTTPException(400, "start_date no puede ser posterior a end_date")

    before = _db_summary(
        storage,
        payload.symbol,
        payload.timeframe,
        payload.start_date,
        payload.end_date,
    )

    if payload.dry_run:
        return CalibrationDataDownloadResponse(
            symbol=payload.symbol,
            timeframe=payload.timeframe,
            start_date=payload.start_date,
            end_date=payload.end_date,
            started=False,
            dry_run=True,
            inserted_new_rows=0,
            first_available_after=before["first_ts"],
            last_available_after=before["last_ts"],
            candles_count_after=before["candles"],
            message="Dry-run: no se ha descargado nada",
        )

    if before["candles"] == 0:
        df = downloader.download_full(
            symbol=payload.symbol,
            timeframe=payload.timeframe,
            storage=storage,
        )
    else:
        if not payload.start_date or not payload.end_date:
            raise HTTPException(
                400,
                "start_date y end_date son obligatorios cuando ya hay datos",
            )

        df = downloader.download_range(
            symbol=payload.symbol,
            timeframe=payload.timeframe,
            start=payload.start_date,
            end=payload.end_date,
            storage=storage,
        )

    inserted = storage.save_ohlcv(df)

    after = _db_summary(
        storage,
        payload.symbol,
        payload.timeframe,
        payload.start_date,
        payload.end_date,
    )

    return CalibrationDataDownloadResponse(
        symbol=payload.symbol,
        timeframe=payload.timeframe,
        start_date=payload.start_date,
        end_date=payload.end_date,
        started=True,
        dry_run=False,
        inserted_new_rows=inserted,
        first_available_after=after["first_ts"],
        last_available_after=after["last_ts"],
        candles_count_after=after["candles"],
        message=f"Descarga completada. Filas nuevas: {inserted}",
    )

# =================================================
# DOWNLOAD JOB (ASYNC + PROGRESS)
# =================================================

def _run_download_job(
    job: DownloadJob,
    payload: CalibrationDataDownloadJobStartRequest,
):
    storage = StorageManager.from_env()
    downloader = get_downloader()

    try:
        job.update(status="downloading", message="Iniciando descarga")

        df = downloader.download_range(
            symbol=payload.symbol,
            timeframe=payload.timeframe,
            start=payload.start_date,
            end=payload.end_date,
            storage=storage,
            job=job,
        )

        if job.cancelled:
            return

        job.update(status="saving", message="Guardando en base de datos")

        inserted = storage.save_ohlcv(df)

        job.update(
            status="done",
            message=f"Descarga finalizada ({inserted} velas nuevas)",
            progress=100,
        )

    except Exception as exc:
        logger.exception("Error en job de descarga")
        job.update(status="failed", message=str(exc))


@router.post(
    "/download/job",
    response_model=CalibrationDataDownloadJobStartResponse,
)
def start_download_job(
    payload: CalibrationDataDownloadJobStartRequest,
    background: BackgroundTasks,
):
    job = DownloadJob()

    with DOWNLOAD_JOBS_LOCK:
        DOWNLOAD_JOBS[job.id] = job

    job.update(
        status="created",
        message="Job creado",
    )

    background.add_task(_run_download_job, job, payload)

    return CalibrationDataDownloadJobStartResponse(
        job_id=job.id,
        status=job.status,
        message=job.message,
    )


@router.get(
    "/download/job/{job_id}",
    response_model=CalibrationDataDownloadJobStatusResponse,
)
def get_download_job_status(job_id: str):
    job = DOWNLOAD_JOBS.get(job_id)

    if not job:
        raise HTTPException(404, "Job no encontrado")

    return CalibrationDataDownloadJobStatusResponse(**job.as_dict())


@router.post(
    "/download/job/{job_id}/cancel",
    response_model=CalibrationDataDownloadJobCancelResponse,
)
def cancel_download_job(job_id: str):
    job = DOWNLOAD_JOBS.get(job_id)

    if not job:
        raise HTTPException(404, "Job no encontrado")

    job.cancel()

    return CalibrationDataDownloadJobCancelResponse(
        job_id=job.id,
        status=job.status,
        message=job.message,
    )
