"""
Data loading and validation module.

This module handles:
- Loading CSV files into canonical DataFrames
- Validating data schema and integrity
- Normalizing column names (case-insensitive)
"""

from pathlib import Path

import pandas as pd

from jbqlab.types import ValidationResult

# =============================================================================
# Constants
# =============================================================================

REQUIRED_COLUMNS = {"date", "close"}
NUMERIC_COLUMNS = {"close", "open", "high", "low", "volume"}


# =============================================================================
# Public API
# =============================================================================


def load_csv(path: str | Path) -> pd.DataFrame:
    """Load a CSV file and normalize it to canonical format.

    The canonical format has:
    - Lowercase column names
    - 'date' column as datetime index
    - 'close' column as float64
    - Sorted by date ascending

    Args:
        path: Path to the CSV file.

    Returns:
        Normalized DataFrame with 'date' as index and 'close' column.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If required columns are missing or data is invalid.
    """
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")

    # Read CSV with flexible date parsing
    df = pd.read_csv(path)

    # Normalize column names to lowercase
    df.columns = df.columns.str.lower().str.strip()

    # Validate required columns exist
    missing = REQUIRED_COLUMNS - set(df.columns)
    if missing:
        raise ValueError(f"Missing required columns: {missing}")

    # Parse date column
    df["date"] = pd.to_datetime(df["date"])

    # Ensure close is numeric
    df["close"] = pd.to_numeric(df["close"], errors="coerce")

    # Check for NaN values in critical columns
    if df["close"].isna().any():
        nan_count = df["close"].isna().sum()
        raise ValueError(f"Found {nan_count} non-numeric values in 'close' column")

    if df["date"].isna().any():
        nan_count = df["date"].isna().sum()
        raise ValueError(f"Found {nan_count} unparseable values in 'date' column")

    # Sort by date and set as index
    df = df.sort_values("date").reset_index(drop=True)
    df = df.set_index("date")

    # Ensure close is float64
    df["close"] = df["close"].astype("float64")

    return df


def validate_dataframe(df: pd.DataFrame) -> ValidationResult:
    """Validate a DataFrame for backtesting compatibility.

    Checks:
    - Required columns present
    - Date column is parseable
    - Close column is numeric and positive
    - No duplicate dates
    - Chronological ordering

    Args:
        df: DataFrame to validate (can have original column names).

    Returns:
        ValidationResult with validation status and any errors/warnings.
    """
    errors: list[str] = []
    warnings: list[str] = []

    # Normalize column names for checking
    normalized_cols = {col.lower().strip() for col in df.columns}

    # Check required columns
    missing = REQUIRED_COLUMNS - normalized_cols
    if missing:
        errors.append(f"Missing required columns: {missing}")
        return ValidationResult(
            is_valid=False,
            errors=errors,
            warnings=warnings,
            row_count=len(df),
            date_range=None,
        )

    # Work with normalized copy
    df_check = df.copy()
    df_check.columns = df_check.columns.str.lower().str.strip()

    # Check date parseability
    try:
        dates = pd.to_datetime(df_check["date"])
        date_range = (dates.min().to_pydatetime(), dates.max().to_pydatetime())
    except Exception as e:
        errors.append(f"Cannot parse 'date' column: {e}")
        date_range = None

    # Check close is numeric
    try:
        close_values = pd.to_numeric(df_check["close"], errors="coerce")
        nan_count = close_values.isna().sum()
        if nan_count > 0:
            errors.append(f"Found {nan_count} non-numeric values in 'close' column")
    except Exception as e:
        errors.append(f"Error processing 'close' column: {e}")

    # Check for non-positive close values
    if "close_values" in dir() and not close_values.isna().all():
        non_positive = (close_values <= 0).sum()
        if non_positive > 0:
            warnings.append(f"Found {non_positive} non-positive values in 'close' column")

    # Check for duplicate dates
    if date_range is not None:
        dup_count = dates.duplicated().sum()
        if dup_count > 0:
            warnings.append(f"Found {dup_count} duplicate dates")

    # Row count warning
    if len(df) < 20:
        warnings.append(f"Very small dataset ({len(df)} rows). Results may not be meaningful.")

    return ValidationResult(
        is_valid=len(errors) == 0,
        errors=errors,
        warnings=warnings,
        row_count=len(df),
        date_range=date_range,
    )


def validate_csv(path: str | Path) -> ValidationResult:
    """Validate a CSV file for backtesting compatibility.

    Args:
        path: Path to the CSV file.

    Returns:
        ValidationResult with validation status and any errors/warnings.
    """
    path = Path(path)
    errors: list[str] = []

    if not path.exists():
        errors.append(f"File not found: {path}")
        return ValidationResult(
            is_valid=False,
            errors=errors,
            warnings=[],
            row_count=0,
            date_range=None,
        )

    try:
        df = pd.read_csv(path)
    except Exception as e:
        errors.append(f"Cannot read CSV file: {e}")
        return ValidationResult(
            is_valid=False,
            errors=errors,
            warnings=[],
            row_count=0,
            date_range=None,
        )

    return validate_dataframe(df)
