from typing import Iterable
import warnings

import polars as pl

from .bloomberg import BloombergSchema
from .canonical import (
    CanonicalUnderlyingDaily,
    CanonicalOptionChainSnapshot,
    CanonicalIVSurfaceSnapshot,
)


# Generic helpers
def _missing_columns(df: pl.DataFrame, required: Iterable[str]) -> set[str]:
    return set(required) - set(df.columns)


def _extra_columns(
    df: pl.DataFrame, allowed: Iterable[str]
) -> set[str]:
    return set(df.columns) - set(allowed)


def _validate_semantic_schema(
    *,
    df: pl.DataFrame,
    name: str,
    required: Iterable[str],
    derived: Iterable[str] | None = None,
    optional: Iterable[str] | None = None,
) -> None:
    """
    Validates a semantic canonical table.
    """

    # Required
    missing_req = _missing_columns(df, required)
    if missing_req:
        raise ValueError(
            f"[{name}] Missing required columns: {sorted(missing_req)}"
        )

    # Derived
    if derived:
        missing_der = _missing_columns(df, derived)
        if missing_der:
            warnings.warn(
                f"[{name}] Derived columns missing (may be expected): "
                f"{sorted(missing_der)}",
                UserWarning,
            )

    # Unexpected
    allowed = set(required)
    if derived:
        allowed |= set(derived)
    if optional:
        allowed |= set(optional)

    extra = _extra_columns(df, allowed)
    if extra:
        warnings.warn(
            f"[{name}] Unexpected columns present: {sorted(extra)}",
            UserWarning,
        )


# Raw Bloomberg CSV validation
def validate_bloomberg_schema(
    df: pl.DataFrame,
    schema: BloombergSchema,
) -> None:
    missing = _missing_columns(df, schema.required_columns)
    if missing:
        raise ValueError(
            f"[{schema.name}] Missing required columns: {sorted(missing)}"
        )

    extra = _extra_columns(
        df,
        schema.required_columns
        + (schema.optional_columns or []),
    )
    if extra:
        warnings.warn(
            f"[{schema.name}] Unexpected columns present: {sorted(extra)}",
            UserWarning,
        )


# Canonical: Underlying daily
def validate_canonical_underlying_daily(df: pl.DataFrame) -> None:
    c = CanonicalUnderlyingDaily()

    _validate_semantic_schema(
        df=df,
        name="canonical_underlying_daily",
        required=c.required_columns,
        derived=c.derived_columns,
        optional=c.optional_columns,
    )

    # Domain checks
    if (df["px_close"] <= 0).any():
        raise ValueError("Non-positive px_close detected")

    if "iv_atm_hist" in df.columns:
        if (df["iv_atm_hist"] <= 0).any():
            raise ValueError("ATM IV must be strictly positive")

    iv_cols = [c for c in df.columns if c.startswith("iv_")]
    for col in iv_cols:
        if (df[col] <= 0).any():
            raise ValueError(f"IV column {col} contains non-positive values")
        if (df[col] > 300).any():
            warnings.warn(
                f"IV column {col} contains extremely large values (>300%)",
                UserWarning,
            )

    for rate_col in ("rate_sofr_90d", "rate_usgg3m"):
        if rate_col in df.columns and (df[rate_col] >= 1).any():
            raise ValueError(f"{rate_col} appears unscaled (must be decimal)")

    if df["date"].is_null().any():
        raise ValueError("Null dates detected")

    if not df.sort("date")["date"].is_sorted():
        warnings.warn(
            "Dates are not monotonic increasing",
            UserWarning,
        )



# Canonical: Option chain snapshot
def validate_canonical_option_chain_snapshot(df: pl.DataFrame) -> None:
    """
    Validate the canonical_option_chain_snapshot table.
    """
    c = CanonicalOptionChainSnapshot()

    _validate_semantic_schema(
        df=df,
        name="canonical_option_chain_snapshot",
        required=c.required_columns,
        derived=c.derived_columns,
    )

    # Domain checks
    if (df["strike"] <= 0).any():
        raise ValueError("Option strikes must be positive")

    if "time_to_expiry" in df.columns:
        if (df["time_to_expiry"] <= 0).any():
            raise ValueError("Options with non-positive time_to_expiry found")

    if df["expiration"].is_null().any():
        raise ValueError("Null expiration dates in option chain snapshot")



# Canonical: IV surface snapshot
def validate_canonical_iv_surface_snapshot(df: pl.DataFrame) -> None:
    c = CanonicalIVSurfaceSnapshot()

    _validate_semantic_schema(
        df=df,
        name="canonical_iv_surface_snapshot",
        required=c.required_columns,
        optional=c.optional_columns,
    )

    if (df["iv"] <= 0).any():
        raise ValueError("IV surface contains non-positive IV values")

    if (df["delta"] <= 0).any() or (df["delta"] >= 100).any():
        raise ValueError("Delta must be in (0, 100)")

    if "spot" in df.columns and df["spot"].is_null().any():
        raise ValueError("Underlying spot price missing")



# Dispatch helper
def validate_canonical_table(name: str, df: pl.DataFrame) -> None:
    if name == "underlying_daily":
        validate_canonical_underlying_daily(df)
    elif name == "option_chain_snapshot":
        validate_canonical_option_chain_snapshot(df)
    elif name == "iv_surface_snapshot":
        validate_canonical_iv_surface_snapshot(df)
    else:
        raise ValueError(f"Unknown canonical table: {name}")


def validate_canonical_bundle(
    tables: dict[str, pl.DataFrame],
    *,
    require_chain: bool = True,
    require_iv_surface: bool = False,
) -> None:
    """
    Validates a bundle of canonical tables produced or supplied to the pipeline.
    """

    # Underlying is always required
    if "underlying_daily" not in tables:
        raise ValueError("Missing required table: underlying_daily")

    validate_canonical_underlying_daily(tables["underlying_daily"])

    # Option chain (optional)
    if require_chain:
        if "option_chain_snapshot" not in tables:
            raise ValueError(
                "Pipeline requires option_chain_snapshot but it was not provided"
            )
        validate_canonical_option_chain_snapshot(
            tables["option_chain_snapshot"]
        )

    # IV surface (optional / future)
    if require_iv_surface:
        if "iv_surface_snapshot" not in tables:
            raise ValueError(
                "Pipeline requires iv_surface_snapshot but it was not provided"
            )
        validate_canonical_iv_surface_snapshot(
            tables["iv_surface_snapshot"]
        )
