"""
Polars Engine Wrapper - High-performance data processing.

Provides unified interface for Polars operations with pandas fallback.
10x faster than pandas for large datasets.
"""

import polars as pl
import pandas as pd
from typing import Union, List, Dict, Any, Optional
from pathlib import Path


class PolarsEngine:
    """High-performance data processing engine using Polars."""
    
    @staticmethod
    def read_csv(
        path: Union[str, Path],
        **kwargs
    ) -> pl.DataFrame:
        """
        Read CSV using Polars (much faster than pandas).
        
        Args:
            path: Path to CSV file
            **kwargs: Additional arguments for pl.read_csv
        """
        return pl.read_csv(path, **kwargs)
    
    @staticmethod
    def to_pandas(df: pl.DataFrame) -> pd.DataFrame:
        """Convert Polars DataFrame to pandas (for compatibility)."""
        return df.to_pandas()
    
    @staticmethod
    def from_pandas(df: pd.DataFrame) -> pl.DataFrame:
        """Convert pandas DataFrame to Polars."""
        return pl.from_pandas(df)
    
    @staticmethod
    def detect_duplicates(df: pl.DataFrame) -> pl.DataFrame:
        """Detect duplicate rows (Polars is 10x faster)."""
        return df.filter(df.is_duplicated())
    
    @staticmethod
    def remove_duplicates(df: pl.DataFrame, keep: str = 'first') -> pl.DataFrame:
        """
        Remove duplicates keeping first/last occurrence.
        
        Args:
            df: Polars DataFrame
            keep: 'first' or 'last'
        """
        if keep == 'first':
            return df.unique(maintain_order=True)
        else:
            return df.unique(maintain_order=True, keep='last')
    
    @staticmethod
    def detect_null_patterns(df: pl.DataFrame) -> Dict[str, List[str]]:
        """
        Detect various null representations in all columns.
        
        Returns:
            Dict of column -> list of null patterns found
        """
        null_patterns = ['null', 'NULL', 'N/A', 'n/a', 'NA', 'None', '', 'NaN', 'nan']
        
        results = {}
        for col in df.columns:
            if df[col].dtype == pl.Utf8:
                found_patterns = []
                for pattern in null_patterns:
                    if df.filter(pl.col(col) == pattern).height > 0:
                        found_patterns.append(pattern)
                if found_patterns:
                    results[col] = found_patterns
        
        return results
    
    @staticmethod
    def standardize_nulls(df: pl.DataFrame) -> pl.DataFrame:
        """
        Standardize all null representations to proper null.
        
        Replaces: 'null', 'NULL', 'N/A', 'NA', '', 'None', etc. → None
        """
        null_patterns = ['null', 'NULL', 'N/A', 'n/a', 'NA', 'None', '', 'NaN', 'nan']
        
        # For each string column, replace null patterns
        exprs = []
        for col in df.columns:
            if df[col].dtype == pl.Utf8:
                expr = pl.col(col)
                for pattern in null_patterns:
                    expr = expr.map_elements(lambda x: None if x == pattern else x)
                exprs.append(expr.alias(col))
            else:
                exprs.append(pl.col(col))
        
        return df.select(exprs)
    
    @staticmethod
    def detect_type_inconsistencies(df: pl.DataFrame) -> Dict[str, str]:
        """
        Detect columns with mixed types.
        
        Returns:
            Dict of column -> description of inconsistency
        """
        issues = {}
        
        for col in df.columns:
            # Try to detect if column should be numeric but contains strings
            if df[col].dtype == pl.Utf8:
                # Try converting to numeric
                try:
                    numeric_count = df.select(
                        pl.col(col).cast(pl.Float64, strict=False).is_not_null().sum()
                    ).item()
                    total_count = df.height
                    
                    if 0 < numeric_count < total_count:
                        issues[col] = f"Mixed numeric/string ({numeric_count}/{total_count} numeric)"
                except:
                    pass
        
        return issues
    
    @staticmethod
    def fuzzy_column_match(col1: str, col2: str) -> float:
        """
        Calculate similarity between column names (0-1).
        
        Uses normalized edit distance.
        """
        from difflib import SequenceMatcher
        
        # Normalize
        c1 = col1.lower().replace('_', '').replace(' ', '').replace('-', '')
        c2 = col2.lower().replace('_', '').replace(' ', '').replace('-', '')
        
        return SequenceMatcher(None, c1, c2).ratio()
    
    @staticmethod
    def align_schemas(
        dfs: Dict[str, pl.DataFrame],
        threshold: float = 0.85
    ) -> Dict[str, pl.DataFrame]:
        """
        Align schemas across multiple DataFrames using fuzzy matching.
        
        Args:
            dfs: Dict of name -> DataFrame
            threshold: Fuzzy matching threshold
            
        Returns:
            Dict of aligned DataFrames with unified column names
        """
        # Get all unique columns
        all_columns = set()
        for df in dfs.values():
            all_columns.update(df.columns)
        
        # Group similar columns
        column_groups = {}
        for col in all_columns:
            matched = False
            for canonical, group in column_groups.items():
                if PolarsEngine.fuzzy_column_match(col, canonical) >= threshold:
                    group.append(col)
                    matched = True
                    break
            
            if not matched:
                column_groups[col] = [col]
        
        # Align all DataFrames
        aligned = {}
        for name, df in dfs.items():
            # Rename columns to canonical names
            rename_map = {}
            for canonical, group in column_groups.items():
                for variant in group:
                    if variant in df.columns:
                        rename_map[variant] = canonical
            
            df_aligned = df.rename(rename_map)
            
            # Add missing columns
            for canonical in column_groups.keys():
                if canonical not in df_aligned.columns:
                    df_aligned = df_aligned.with_columns(pl.lit(None).alias(canonical))
            
            aligned[name] = df_aligned
        
        return aligned
    
    @staticmethod
    def merge_dataframes(
        dfs: List[pl.DataFrame],
        how: str = 'vertical'
    ) -> pl.DataFrame:
        """
        Merge multiple DataFrames.
        
        Args:
            dfs: List of DataFrames
            how: 'vertical' (concat) or 'horizontal' (join)
        """
        if how == 'vertical':
            return pl.concat(dfs, how='vertical')
        else:
            raise NotImplementedError("Horizontal merge not yet implemented")
    
    @staticmethod
    def profile_dataframe(df: pl.DataFrame) -> Dict[str, Any]:
        """
        Generate statistical profile of DataFrame.
        
        Returns:
            Dict with statistics for each column
        """
        profile = {
            'row_count': df.height,
            'column_count': len(df.columns),
            'columns': {}
        }
        
        for col in df.columns:
            col_stats = {
                'dtype': str(df[col].dtype),
                'null_count': df[col].null_count(),
                'null_percentage': (df[col].null_count() / df.height * 100) if df.height > 0 else 0,
                'unique_count': df[col].n_unique(),
            }
            
            # Add numeric stats if applicable
            if df[col].dtype in [pl.Int64, pl.Int32, pl.Float64, pl.Float32]:
                col_stats['min'] = df[col].min()
                col_stats['max'] = df[col].max()
                col_stats['mean'] = df[col].mean()
            
            profile['columns'][col] = col_stats
        
        return profile
