from __future__ import annotations

from typing import Any

import pandas as pd
from pandas.api.types import is_datetime64_any_dtype

from featureleak.report import LeakIssue
from featureleak.utils.datetime import safe_to_datetime


FUTURE_TOKENS = (
    "future",
    "next",
    "lead",
    "ahead",
    "t+1",
    "tomorrow",
    "following",
)

ROLLING_TOKENS = (
    "rolling",
    "expanding",
    "window",
    "mean",
    "avg",
    "sum",
)

LAG_TOKENS = (
    "lag",
    "prev",
    "previous",
    "prior",
)


def _safe_to_datetime(series: pd.Series) -> pd.Series:
    try:
        return safe_to_datetime(series)
    except Exception:
        return pd.Series([pd.NaT] * len(series), index=series.index)


def _name_flags(column_name: str) -> dict[str, Any]:
    col = str(column_name).lower()
    matched_future = [tok for tok in FUTURE_TOKENS if tok in col]
    matched_rolling = [tok for tok in ROLLING_TOKENS if tok in col]
    matched_lag = [tok for tok in LAG_TOKENS if tok in col]

    return {
        "future_tokens": matched_future,
        "rolling_tokens": matched_rolling,
        "lag_tokens": matched_lag,
    }


def _future_datetime_ratio(feature_dt: pd.Series, base_dt: pd.Series) -> float | None:
    try:
        valid = feature_dt.notna() & base_dt.notna()
        if valid.sum() == 0:
            return None
        return float((feature_dt[valid] > base_dt[valid]).mean())
    except Exception:
        return None


def _split_time_violation(
    train_time: pd.Series,
    test_time: pd.Series,
) -> dict[str, Any]:
    train_dt = _safe_to_datetime(train_time)
    test_dt = _safe_to_datetime(test_time)

    try:
        train_max = train_dt.max()
        test_min = test_dt.min()

        if pd.isna(train_max) or pd.isna(test_min):
            return {
                "train_max_time": None,
                "test_min_time": None,
                "train_after_test_start": None,
            }

        return {
            "train_max_time": str(train_max),
            "test_min_time": str(test_min),
            "train_after_test_start": bool(train_max >= test_min),
        }
    except Exception:
        return {
            "train_max_time": None,
            "test_min_time": None,
            "train_after_test_start": None,
        }


def detect_time_leakage(
    df: pd.DataFrame,
    target: str,
    time_col: str,
    train_df: pd.DataFrame | None = None,
    test_df: pd.DataFrame | None = None,
    future_ratio_threshold: float = 0.05,
) -> list[LeakIssue]:
    """
    Detect temporal leakage risks.

    v1 signals:
    - feature names with future semantics
    - datetime feature values occurring after the row timestamp
    - suspicious rolling/future combinations in names
    - train/test temporal order violations
    """
    issues: list[LeakIssue] = []

    if df.empty or time_col not in df.columns:
        return issues

    base_time = _safe_to_datetime(df[time_col])

    if base_time.notna().sum() == 0:
        return issues

    for col in df.columns:
        if col in {target, time_col}:
            continue

        feature = df[col]
        flags = _name_flags(col)
        reasons: list[str] = []
        details: dict[str, Any] = {
            "future_tokens": flags["future_tokens"],
            "rolling_tokens": flags["rolling_tokens"],
            "lag_tokens": flags["lag_tokens"],
        }
        severity: str | None = None

        if flags["future_tokens"]:
            reasons.append("future_semantic_name")
            severity = "medium"

        if flags["future_tokens"] and flags["rolling_tokens"]:
            reasons.append("future_rolling_name_pattern")
            severity = "high"

        if is_datetime64_any_dtype(feature):
            feature_dt = _safe_to_datetime(feature)
            future_ratio = _future_datetime_ratio(feature_dt, base_time)
            details["future_timestamp_ratio"] = future_ratio

            if future_ratio is not None and future_ratio >= future_ratio_threshold:
                reasons.append("feature_timestamp_after_row_time")
                severity = "high" if future_ratio >= 0.25 else "medium"

        else:
            # Attempt parse for object-like potential datetime columns
            parsed = _safe_to_datetime(feature)
            parsed_rate = float(parsed.notna().mean()) if len(parsed) else 0.0
            details["parsed_datetime_rate"] = round(parsed_rate, 6)

            if parsed_rate >= 0.80:
                future_ratio = _future_datetime_ratio(parsed, base_time)
                details["future_timestamp_ratio"] = future_ratio
                if future_ratio is not None and future_ratio >= future_ratio_threshold:
                    reasons.append("parsed_feature_timestamp_after_row_time")
                    severity = "high" if future_ratio >= 0.25 else "medium"

        if severity is not None:
            details["reasons"] = sorted(set(reasons))
            issues.append(
                LeakIssue(
                    severity=severity,
                    check="temporal_leakage",
                    title="Potential temporal leakage detected",
                    summary=(
                        f"Column `{col}` may use information unavailable at prediction time "
                        f"relative to `{time_col}`."
                    ),
                    affected_columns=[col],
                    details=details,
                    suggested_fix=(
                        "Ensure this feature is computed using strictly past-only information "
                        "available at the prediction timestamp."
                    ),
                )
            )

    if (
        train_df is not None
        and test_df is not None
        and time_col in train_df.columns
        and time_col in test_df.columns
    ):
        train_max_time = train_df[time_col].max()
        test_min_time = test_df[time_col].min()

        if test_min_time < train_max_time:
            # Flag as medium risk for temporal leakage
            issues.append(
                LeakIssue(
                    severity="medium",
                    check="temporal_leakage",
                    title="Temporal Leakage Detected",
                    summary=f"Test data starts before training data ends (test_min: {test_min_time}, train_max: {train_max_time}).",
                    affected_columns=[time_col],
                    details={
                        "train_max_time": str(train_max_time),
                        "test_min_time": str(test_min_time),
                    },
                    suggested_fix="Ensure training data is from before test data chronologically.",
                )
            )

    return issues