"""
Data validators for exploratory data analysis.

This module provides reusable validation functions for data quality assessment,
including duplicate detection, date logic validation, and value range validation.
"""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from customer_retention.core.compat import (
    DataFrame,
    _is_spark_pandas,
    head_as_list,
    is_datetime64_any_dtype,
    is_numeric_dtype,
    pd,
    resolve_column_name,
    to_datetime,
)
from customer_retention.core.compat.bulk_profiling import (
    bulk_validate_ranges,
)
from customer_retention.core.components.enums import Severity


@dataclass
class DuplicateResult:
    """Result of duplicate analysis."""
    key_column: str
    total_rows: int
    unique_keys: int
    duplicate_keys: int
    duplicate_rows: int
    duplicate_percentage: float
    has_value_conflicts: bool
    conflict_columns: List[str] = field(default_factory=list)
    conflict_examples: List[Dict[str, Any]] = field(default_factory=list)
    exact_duplicate_rows: int = 0
    severity: Severity = Severity.INFO

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for display."""
        return {
            "key_column": self.key_column,
            "total_rows": self.total_rows,
            "unique_keys": self.unique_keys,
            "duplicate_keys": self.duplicate_keys,
            "duplicate_rows": self.duplicate_rows,
            "duplicate_percentage": round(self.duplicate_percentage, 2),
            "has_value_conflicts": self.has_value_conflicts,
            "conflict_columns": self.conflict_columns,
            "exact_duplicate_rows": self.exact_duplicate_rows,
            "severity": self.severity.value
        }


@dataclass
class DateLogicResult:
    """Result of date logic validation."""
    date_columns: List[str]
    total_rows: int
    valid_rows: int
    invalid_rows: int
    invalid_percentage: float
    violations: List[Dict[str, Any]] = field(default_factory=list)
    violation_types: Dict[str, int] = field(default_factory=dict)
    severity: Severity = Severity.INFO

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for display."""
        return {
            "date_columns": self.date_columns,
            "total_rows": self.total_rows,
            "valid_rows": self.valid_rows,
            "invalid_rows": self.invalid_rows,
            "invalid_percentage": round(self.invalid_percentage, 2),
            "violation_types": self.violation_types,
            "severity": self.severity.value
        }


@dataclass
class RangeValidationResult:
    """Result of value range validation."""
    column_name: str
    total_values: int
    valid_values: int
    invalid_values: int
    invalid_percentage: float
    rule_type: str
    expected_range: str
    actual_range: str
    invalid_examples: List[Any] = field(default_factory=list)
    severity: Severity = Severity.INFO

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for display."""
        return {
            "column": self.column_name,
            "rule_type": self.rule_type,
            "expected_range": self.expected_range,
            "actual_range": self.actual_range,
            "invalid_count": self.invalid_values,
            "invalid_percentage": round(self.invalid_percentage, 2),
            "severity": self.severity.value
        }


def _severity_from_percentage(percentage: float) -> Severity:
    if percentage > 10:
        return Severity.CRITICAL
    if percentage > 5:
        return Severity.WARNING
    return Severity.INFO


_SEVERITY_ORDER = [Severity.INFO, Severity.WARNING, Severity.CRITICAL]


def _highest_severity(severities: List[Severity]) -> Severity:
    return max(severities, key=lambda s: _SEVERITY_ORDER.index(s))


class DataValidator:
    """
    Validator for data quality checks in exploratory analysis.

    Provides methods for duplicate detection, date logic validation,
    and value range validation.
    """

    def check_duplicates(
        self,
        df: DataFrame,
        key_column: str,
        check_value_conflicts: bool = True,
        exclude_columns: Optional[List[str]] = None
    ) -> DuplicateResult:
        """
        Comprehensive duplicate analysis with conflict detection.

        Parameters
        ----------
        df : DataFrame
            Data to analyze
        key_column : str
            Column to check for duplicates (e.g., customer ID)
        check_value_conflicts : bool
            Whether to check if duplicate keys have different values
        exclude_columns : List[str], optional
            Columns to exclude from conflict checking

        Returns
        -------
        DuplicateResult
            Detailed analysis of duplicates and conflicts
        """
        try:
            resolved_key = resolve_column_name(df.columns, key_column)
        except KeyError:
            resolved_key = key_column
        if resolved_key not in df.columns:
            return DuplicateResult(
                key_column=key_column,
                total_rows=len(df),
                unique_keys=0,
                duplicate_keys=0,
                duplicate_rows=0,
                duplicate_percentage=0.0,
                has_value_conflicts=False,
                severity=Severity.CRITICAL
            )

        total_rows = len(df)
        unique_keys = df[resolved_key].nunique()
        duplicate_mask = df[resolved_key].duplicated(keep=False)
        duplicate_rows = duplicate_mask.sum()
        duplicate_keys = df[duplicate_mask][resolved_key].nunique()
        duplicate_percentage = (duplicate_rows / total_rows * 100) if total_rows > 0 else 0.0

        exact_duplicate_rows = df.duplicated(keep=False).sum()
        severity = _severity_from_percentage(duplicate_percentage)

        has_value_conflicts = False
        conflict_columns = []
        conflict_examples = []

        if check_value_conflicts and duplicate_keys > 0:
            exclude = set(exclude_columns or [])
            exclude.add(resolved_key)
            value_columns = [c for c in df.columns if c not in exclude]

            sample_keys = head_as_list(df[duplicate_mask][resolved_key].unique(), 5)

            for key_value in sample_keys:
                key_rows = df[df[resolved_key] == key_value]
                for col in value_columns:
                    unique_vals = key_rows[col].dropna().unique()
                    if len(unique_vals) > 1:
                        has_value_conflicts = True
                        if col not in conflict_columns:
                            conflict_columns.append(col)
                        if len(conflict_examples) < 3:
                            conflict_examples.append({
                                "key": key_value,
                                "column": col,
                                "values": unique_vals[:5]
                            })

            if has_value_conflicts and severity == Severity.INFO:
                severity = Severity.WARNING

        return DuplicateResult(
            key_column=key_column,
            total_rows=total_rows,
            unique_keys=unique_keys,
            duplicate_keys=duplicate_keys,
            duplicate_rows=duplicate_rows,
            duplicate_percentage=duplicate_percentage,
            has_value_conflicts=has_value_conflicts,
            conflict_columns=conflict_columns,
            conflict_examples=conflict_examples,
            exact_duplicate_rows=exact_duplicate_rows,
            severity=severity
        )

    def validate_date_logic(
        self,
        df: DataFrame,
        date_columns: List[str],
        expected_order: Optional[List[str]] = None
    ) -> DateLogicResult:
        """
        Validate temporal consistency of date fields.

        Parameters
        ----------
        df : DataFrame
            Data to validate
        date_columns : List[str]
            List of date column names in expected chronological order
        expected_order : List[str], optional
            Explicit order of dates (if different from date_columns order)

        Returns
        -------
        DateLogicResult
            Detailed analysis of date logic violations
        """
        existing_cols = [c for c in date_columns if c in df.columns]

        if len(existing_cols) < 2:
            return DateLogicResult(
                date_columns=existing_cols,
                total_rows=len(df),
                valid_rows=len(df),
                invalid_rows=0,
                invalid_percentage=0.0,
                severity=Severity.INFO
            )

        order = expected_order if expected_order else existing_cols
        order = [c for c in order if c in existing_cols]

        df_dates = df[order].copy()
        for col in order:
            if not is_datetime64_any_dtype(df_dates[col]):
                df_dates[col] = to_datetime(df_dates[col], errors='coerce', format='mixed')

        violations = []
        violation_types = {}
        invalid_mask = pd.Series(False, index=df.index)

        for i in range(len(order) - 1):
            col1, col2 = order[i], order[i + 1]
            invalid = df_dates[col1] > df_dates[col2]
            valid_comparison = df_dates[col1].notna() & df_dates[col2].notna()
            invalid = invalid & valid_comparison

            if invalid.any():
                violation_key = f"{col1} > {col2}"
                violation_count = invalid.sum()
                violation_types[violation_key] = int(violation_count)
                invalid_mask = invalid_mask | invalid

                if len(violations) < 5:
                    sample_idx = df[invalid].head(3).index
                    for idx in sample_idx:
                        violations.append({
                            "row": int(idx),
                            "violation": violation_key,
                            col1: str(df_dates.loc[idx, col1]),
                            col2: str(df_dates.loc[idx, col2])
                        })

        total_rows = len(df)
        invalid_rows = int(invalid_mask.sum())
        valid_rows = total_rows - invalid_rows
        invalid_percentage = (invalid_rows / total_rows * 100) if total_rows > 0 else 0.0

        severity = _severity_from_percentage(invalid_percentage)

        return DateLogicResult(
            date_columns=order,
            total_rows=total_rows,
            valid_rows=valid_rows,
            invalid_rows=invalid_rows,
            invalid_percentage=invalid_percentage,
            violations=violations,
            violation_types=violation_types,
            severity=severity
        )

    def validate_value_ranges(
        self,
        df: DataFrame,
        rules: Optional[Dict[str, Dict[str, Any]]] = None
    ) -> List[RangeValidationResult]:
        """
        Validate logical ranges for numeric fields.

        Parameters
        ----------
        df : DataFrame
            Data to validate
        rules : Dict[str, Dict[str, Any]], optional
            Custom validation rules. If None, uses default rules.
            Format: {"column": {"type": "percentage|binary|non_negative", "min": 0, "max": 100}}

        Returns
        -------
        List[RangeValidationResult]
            Validation results for each rule
        """
        results = []

        if rules is None:
            rules = self._infer_default_rules(df)

        if _is_spark_pandas(df):
            return self._validate_ranges_bulk(df, rules)

        for col_name, rule in rules.items():
            if col_name not in df.columns:
                continue

            series = df[col_name].dropna()
            total_values = len(series)

            if total_values == 0:
                continue

            if not is_numeric_dtype(series):
                continue

            rule_type = rule.get("type", "range")
            min_val = rule.get("min")
            max_val = rule.get("max")

            if rule_type == "percentage":
                min_val = min_val if min_val is not None else 0
                max_val = max_val if max_val is not None else 100
                invalid_mask = (series < min_val) | (series > max_val)
                expected_range = f"[{min_val}, {max_val}]"
            elif rule_type == "binary":
                valid_values = rule.get("valid_values", [0, 1])
                invalid_mask = ~series.isin(valid_values)
                expected_range = str(valid_values)
            elif rule_type == "non_negative":
                invalid_mask = series < 0
                expected_range = "[0, +∞)"
            else:  # general range
                invalid_mask = pd.Series(False, index=series.index)
                if min_val is not None:
                    invalid_mask = invalid_mask | (series < min_val)
                if max_val is not None:
                    invalid_mask = invalid_mask | (series > max_val)
                expected_range = f"[{min_val or '-∞'}, {max_val or '+∞'}]"

            invalid_values = int(invalid_mask.sum())
            valid_values = total_values - invalid_values
            invalid_percentage = (invalid_values / total_values * 100) if total_values > 0 else 0.0

            actual_min = float(series.min())
            actual_max = float(series.max())
            actual_range = f"[{actual_min:.2f}, {actual_max:.2f}]"

            invalid_examples = head_as_list(series[invalid_mask], 5) if invalid_values > 0 else []
            severity = _severity_from_percentage(invalid_percentage)

            results.append(RangeValidationResult(
                column_name=col_name,
                total_values=total_values,
                valid_values=valid_values,
                invalid_values=invalid_values,
                invalid_percentage=invalid_percentage,
                rule_type=rule_type,
                expected_range=expected_range,
                actual_range=actual_range,
                invalid_examples=invalid_examples,
                severity=severity
            ))

        return results

    def _validate_ranges_bulk(
        self,
        df: DataFrame,
        rules: Dict[str, Dict[str, Any]],
    ) -> List[RangeValidationResult]:
        """Bulk-validate value ranges using batched Spark aggregation."""
        bulk_results = bulk_validate_ranges(df, rules)
        results = []
        for col_name, br in bulk_results.items():
            if br.non_null_count == 0:
                continue
            rule = rules[col_name]
            rule_type = rule.get("type", "range")
            min_val = rule.get("min")
            max_val = rule.get("max")
            if rule_type == "percentage":
                mn = min_val if min_val is not None else 0
                mx = max_val if max_val is not None else 100
                expected_range = f"[{mn}, {mx}]"
            elif rule_type == "binary":
                expected_range = str(rule.get("valid_values", [0, 1]))
            elif rule_type == "non_negative":
                expected_range = "[0, +\u221e)"
            elif rule_type == "rate":
                expected_range = "[0, 1]"
            else:
                lo = min_val if min_val is not None else "-\u221e"
                hi = max_val if max_val is not None else "+\u221e"
                expected_range = f"[{lo}, {hi}]"

            actual_range = ""
            if br.actual_min is not None and br.actual_max is not None:
                actual_range = f"[{br.actual_min:.2f}, {br.actual_max:.2f}]"

            invalid_pct = (br.invalid_count / br.non_null_count * 100) if br.non_null_count > 0 else 0.0
            severity = _severity_from_percentage(invalid_pct)

            results.append(RangeValidationResult(
                column_name=col_name,
                total_values=br.non_null_count,
                valid_values=br.non_null_count - br.invalid_count,
                invalid_values=br.invalid_count,
                invalid_percentage=invalid_pct,
                rule_type=rule_type,
                expected_range=expected_range,
                actual_range=actual_range,
                invalid_examples=[],
                severity=severity,
            ))
        return results

    def _infer_default_rules(self, df: DataFrame) -> Dict[str, Dict[str, Any]]:
        """
        Infer default validation rules based on column names.

        Parameters
        ----------
        df : DataFrame
            Data to analyze

        Returns
        -------
        Dict[str, Dict[str, Any]]
            Inferred validation rules
        """
        rules = {}

        for col in df.columns:
            col_lower = col.lower()

            if any(pattern in col_lower for pattern in ['rate', 'pct', 'percent', 'ratio']):
                if df[col].dtype in ['float64', 'float32', 'int64', 'int32']:
                    max_val = df[col].max()
                    if max_val <= 1.0:
                        rules[col] = {"type": "percentage", "min": 0, "max": 1}
                    else:
                        rules[col] = {"type": "percentage", "min": 0, "max": 100}

            elif df[col].nunique() == 2:
                unique_vals = head_as_list(df[col].dropna().unique(), 10)
                if set(unique_vals).issubset({0, 1, True, False, 0.0, 1.0}):
                    rules[col] = {"type": "binary", "valid_values": [0, 1]}

            elif any(pattern in col_lower for pattern in ['count', 'amount', 'quantity', 'num_', 'n_']):
                if df[col].dtype in ['float64', 'float32', 'int64', 'int32']:
                    rules[col] = {"type": "non_negative"}

        return rules

    def validate_all(
        self,
        df: DataFrame,
        key_column: Optional[str] = None,
        date_columns: Optional[List[str]] = None,
        range_rules: Optional[Dict[str, Dict[str, Any]]] = None
    ) -> Dict[str, Any]:
        """
        Run all validations and return comprehensive results.

        Parameters
        ----------
        df : DataFrame
            Data to validate
        key_column : str, optional
            Column to check for duplicates
        date_columns : List[str], optional
            Date columns to validate
        range_rules : Dict[str, Dict[str, Any]], optional
            Custom range validation rules

        Returns
        -------
        Dict[str, Any]
            Comprehensive validation results
        """
        results = {
            "duplicates": None,
            "date_logic": None,
            "range_validations": [],
            "overall_severity": Severity.INFO
        }

        severities = []

        if key_column:
            dup_result = self.check_duplicates(df, key_column)
            results["duplicates"] = dup_result.to_dict()
            severities.append(dup_result.severity)

        if date_columns:
            date_result = self.validate_date_logic(df, date_columns)
            results["date_logic"] = date_result.to_dict()
            severities.append(date_result.severity)

        range_results = self.validate_value_ranges(df, range_rules)
        results["range_validations"] = [r.to_dict() for r in range_results]
        severities.extend([r.severity for r in range_results])

        if severities:
            results["overall_severity"] = _highest_severity(severities).value
        else:
            results["overall_severity"] = Severity.INFO.value

        return results
