"""
Country Code Conversion Module.

This module handles the conversion of country codes, names, and formats.
It utilizes Polars for efficient data handling and caching for performance.
"""

import re
import functools
from pathlib import Path

import polars as pl
from .country_types import Formats

# Constants
DATA_DIR = Path(__file__).parent / "data"
DATA_FILE = DATA_DIR / "codelist.parquet"
INDEX_FILE = DATA_DIR / "search_index.parquet"

class _CountryEngine:
    """
    Internal engine handling data loading, caching, and lookups.
    """

    def __init__(self) -> None:
        self._df: pl.DataFrame | None = None
        self._search_index: pl.DataFrame | None = None

    @property
    def df(self) -> pl.DataFrame:
        """Lazy load the main data table."""
        if self._df is None:
            if not DATA_FILE.exists():
                raise FileNotFoundError(f"Country data not found at: {DATA_FILE}")
            self._df = pl.read_parquet(DATA_FILE)
        return self._df

    @property
    def index(self) -> pl.DataFrame:
        """Lazy load the search index for inference."""
        if self._search_index is None:
            if not INDEX_FILE.exists():
                raise FileNotFoundError(f"Search index not found at: {INDEX_FILE}")
            self._search_index = pl.read_parquet(INDEX_FILE)
        return self._search_index

    @functools.lru_cache(maxsize=128)
    def get_mapping(self, source_col: str, target_col: str) -> dict[str, str]:
        """
        Creates and caches a lookup dictionary between two columns.
        """
        try:
            # Select, drop nulls, and cast to string to ensure consistency
            subset = self.df.select(
                pl.col(source_col).cast(pl.String), 
                pl.col(target_col).cast(pl.String)
            ).drop_nulls()
            
            return dict(subset.iter_rows())
        except pl.exceptions.ColumnNotFoundError:
            return {}

    def infer_format(self, value: str) -> str | None:
        """
        Guesses the format of the input value.
        """
        # 1. Fast Lookup via Search Index
        # We use .item() if rows exist, which is faster than index access
        res = self.index.filter(pl.col("value") == value)
        if not res.is_empty():
            formats = res["format"].item(0)
            if formats is not None and len(formats) > 0:
                return formats[0]

        # 2. Heuristics for common ISO codes (Fallbacks)
        if len(value) == 3 and value.isupper():
            if not self.df.filter(pl.col("iso3c") == value).is_empty():
                return Formats.ISO3C
        
        if len(value) == 2 and value.isupper():
            if not self.df.filter(pl.col("iso2c") == value).is_empty():
                return Formats.ISO2C

        if value.isdigit():
            if not self.df.filter(pl.col("iso3n") == value).is_empty():
                return Formats.ISO3N

        return None

    def resolve_fuzzy_name(self, value: str, target_format: str) -> str | None:
        """
        Attempts to find a match by normalizing input against known name columns.
        """
        normalized = value.strip().lower()
        
        # List of columns to search against (Priority order)
        name_cols = [
            Formats.Country.Name.EN,
            Formats.Country.Name.FR,
            Formats.Country.Name.DE,
            Formats.Country.Name.IT
        ]

        for col_name in name_cols:
            try:
                res = self.df.filter(
                    pl.col(col_name).str.to_lowercase() == normalized
                )
                if not res.is_empty():
                    val = res[target_format].item(0)
                    return str(val) if val is not None else None
            except pl.exceptions.ColumnNotFoundError:
                continue
                
        return None

    def resolve_regex(self, value: str, source_col: str, target_col: str) -> str | None:
        """
        Iterates through regex patterns defined in the source column.
        """
        mapping = self.get_mapping(source_col, target_col)
        
        for pattern, result in mapping.items():
            try:
                if re.search(pattern, value, re.IGNORECASE):
                    return result
            except re.error:
                continue
        return None

# Initialize the private engine instance
_ENGINE = _CountryEngine()

@functools.lru_cache(maxsize=1024)
def convert(
    value: str | int, 
    to: str, 
    from_format: str | None = None
) -> str | None:
    """
    Convert a country code or name to a specific target format.

    Args:
        value: The input country code or name (e.g., "USA", 840).
        to: The target format identifier (from Formats class).
        from_format: Optional explicit format of the input. If None, 
                     the function attempts to infer it.

    Returns:
        The converted value as a string, or None if conversion failed.
    """
    str_value = str(value)
    origin = from_format

    # 1. Inference
    if not origin:
        origin = _ENGINE.infer_format(str_value)

    # 2. Direct Lookup (if origin is known)
    if origin:
        # Handle Regex Origins
        if ".regex" in origin:
            return _ENGINE.resolve_regex(str_value, origin, to)
        
        # Handle Standard Dictionary Lookup
        mapping = _ENGINE.get_mapping(origin, to)
        result = mapping.get(str_value)
        
        if result:
            return result

    # 3. Fallback: Fuzzy Name Matching
    # If inference failed or lookup failed, try matching against name columns
    return _ENGINE.resolve_fuzzy_name(str_value, to)
