"""
Data issue detection - identifies common problems in datasets.
"""

from dataclasses import dataclass
from enum import Enum
from typing import Any

import pandas as pd
import numpy as np

from app.core.logging import get_logger

logger = get_logger(__name__)


class IssueType(str, Enum):
    """Types of data issues."""
    
    MISSING_HEADERS = "missing_headers"
    MIXED_DATE_FORMATS = "mixed_date_formats"
    MIXED_TYPES = "mixed_types"
    ENCODING_ISSUE = "encoding_issue"
    DUPLICATE_ROWS = "duplicate_rows"
    EXTRA_WHITESPACE = "extra_whitespace"
    INCONSISTENT_NULLS = "inconsistent_nulls"
    MISALIGNED_COLUMNS = "misaligned_columns"


@dataclass
class DataIssue:
    """Represents a detected data issue."""
    
    issue_type: IssueType
    severity: str  # "low", "medium", "high"
    column: str | None = None
    description: str = ""
    affected_rows: int = 0
    details: dict[str, Any] = None
    
    def __post_init__(self):
        if self.details is None:
            self.details = {}


class IssueDetector:
    """Detects common data quality issues."""
    
    def __init__(self):
        """Initialize detector."""
        self.logger = logger
        self.issues: list[DataIssue] = []
    
    def detect_all(self, df: pd.DataFrame) -> list[DataIssue]:
        """
        Detect all common issues in a DataFrame.
        
        Args:
            df: DataFrame to analyze
            
        Returns:
            List of detected issues
        """
        self.issues = []
        
        # Run all detectors
        self._detect_missing_headers(df)
        self._detect_duplicate_rows(df)
        self._detect_mixed_types(df)
        self._detect_mixed_dates(df)
        self._detect_whitespace_issues(df)
        self._detect_inconsistent_nulls(df)
        
        self.logger.info(f"Detected {len(self.issues)} issues")
        return self.issues
    
    def _detect_missing_headers(self, df: pd.DataFrame) -> None:
        """Detect if first row should be headers."""
        # Check if column names are numeric (0, 1, 2, ...)
        if all(isinstance(col, int) for col in df.columns):
            issue = DataIssue(
                issue_type=IssueType.MISSING_HEADERS,
                severity="high",
                description="File appears to be missing column headers",
                details={"current_columns": list(df.columns)},
            )
            self.issues.append(issue)
            return
        
        # Check if column names look like data values
        if all(str(col).replace(".", "").replace("-", "").isdigit() for col in df.columns):
            issue = DataIssue(
                issue_type=IssueType.MISSING_HEADERS,
                severity="medium",
                description="Column names look like data values",
                details={"current_columns": list(df.columns)},
            )
            self.issues.append(issue)
    
    def _detect_duplicate_rows(self, df: pd.DataFrame) -> None:
        """Detect duplicate rows."""
        duplicates = df.duplicated()
        dup_count = duplicates.sum()
        
        if dup_count > 0:
            issue = DataIssue(
                issue_type=IssueType.DUPLICATE_ROWS,
                severity="medium" if dup_count < len(df) * 0.1 else "high",
                description=f"Found {dup_count} duplicate rows",
                affected_rows=dup_count,
                details={"duplicate_count": int(dup_count)},
            )
            self.issues.append(issue)
    
    def _detect_mixed_types(self, df: pd.DataFrame) -> None:
        """Detect columns with mixed data types."""
        for col in df.columns:
            if df[col].dtype == object:  # Only check object columns
                # Get non-null values
                values = df[col].dropna()
                
                if len(values) == 0:
                    continue
                
                # Check if values have mixed types
                types_found = set()
                for val in values.head(100):  # Sample first 100
                    if isinstance(val, (int, np.integer)):
                        types_found.add("int")
                    elif isinstance(val, (float, np.floating)):
                        types_found.add("float")
                    elif isinstance(val, str):
                        types_found.add("string")
                
                if len(types_found) > 1:
                    issue = DataIssue(
                        issue_type=IssueType.MIXED_TYPES,
                        severity="medium",
                        column=col,
                        description=f"Column '{col}' has mixed types: {', '.join(types_found)}",
                        details={"types": list(types_found)},
                    )
                    self.issues.append(issue)
    
    def _detect_mixed_dates(self, df: pd.DataFrame) -> None:
        """Detect columns with mixed date formats."""
        for col in df.columns:
            if df[col].dtype == object:
                values = df[col].dropna().astype(str)
                
                if len(values) == 0:
                    continue
                
                # Common date patterns
                date_patterns = {
                    "YYYY-MM-DD": r'^\d{4}-\d{2}-\d{2}$',
                    "MM/DD/YYYY": r'^\d{2}/\d{2}/\d{4}$',
                    "DD-MM-YYYY": r'^\d{2}-\d{2}-\d{4}$',
                    "DD/MM/YYYY": r'^\d{2}/\d{2}/\d{4}$',
                }
                
                formats_found = set()
                for pattern_name, pattern in date_patterns.items():
                    if values.str.match(pattern).any():
                        formats_found.add(pattern_name)
                
                if len(formats_found) > 1:
                    issue = DataIssue(
                        issue_type=IssueType.MIXED_DATE_FORMATS,
                        severity="high",
                        column=col,
                        description=f"Column '{col}' has mixed date formats: {', '.join(formats_found)}",
                        details={"formats": list(formats_found)},
                    )
                    self.issues.append(issue)
    
    def _detect_whitespace_issues(self, df: pd.DataFrame) -> None:
        """Detect extra whitespace in string columns."""
        for col in df.columns:
            if df[col].dtype == object:
                values = df[col].dropna().astype(str)
                
                if len(values) == 0:
                    continue
                
                # Check for leading/trailing whitespace
                has_leading = values.str.match(r'^\s').any()
                has_trailing = values.str.match(r'\s$').any()
                
                if has_leading or has_trailing:
                    issue = DataIssue(
                        issue_type=IssueType.EXTRA_WHITESPACE,
                        severity="low",
                        column=col,
                        description=f"Column '{col}' has extra whitespace",
                        details={
                            "leading": bool(has_leading),
                            "trailing": bool(has_trailing),
                        },
                    )
                    self.issues.append(issue)
    
    def _detect_inconsistent_nulls(self, df: pd.DataFrame) -> None:
        """Detect inconsistent null representations (None, NaN, 'null', 'N/A', etc.)."""
        null_patterns = ["null", "n/a", "na", "none", "nan", "", " "]
        
        for col in df.columns:
            if df[col].dtype == object:
                values = df[col].astype(str).str.lower()
                
                # Check for string representations of null
                null_like = values.isin(null_patterns)
                null_count = null_like.sum()
                
                if null_count > 0:
                    issue = DataIssue(
                        issue_type=IssueType.INCONSISTENT_NULLS,
                        severity="low",
                        column=col,
                        description=f"Column '{col}' has {null_count} string null values",
                        affected_rows=null_count,
                        details={"null_count": int(null_count)},
                    )
                    self.issues.append(issue)
