# In fmatch/preprocessing/core/column_detector.py

import pandas as pd
from typing import List, Dict, Optional
import re
from dataclasses import dataclass, field
import logging

log = logging.getLogger(__name__)


@dataclass
class IDCandidate:
    """Represents a potential ID column with scoring metadata"""

    column_name: str
    score: float
    uniqueness_ratio: float
    pattern_matches: List[str]
    detected_system: Optional[str] = None
    issues: List[str] = field(default_factory=list)


class ColumnDetector:
    """Intelligent column type detection with enterprise system awareness"""

    STRICT_UNIQUENESS_FLOOR = 0.75

    PATTERNS = {
        "email": re.compile(r"email|mail|contact", re.I),
        "phone": re.compile(r"phone|mobile|cell|tel|fax", re.I),
        "company": re.compile(
            r"company|organization|account|customer|client|vendor|business", re.I
        ),
        "domain": re.compile(r"domain|website|url|site|web", re.I),
        "address": re.compile(r"address|street|addr", re.I),
        "name": re.compile(r"name|contact|person|first|last|full", re.I),
        "id": re.compile(
            r"id$|_id$|_id\b|^id_|uuid|guid|key$|_key$|code$|_code$|number$|_number$|_num$|_no$|identifier|ident\b|record|row|ref$|reference",
            re.I,
        ),
    }

    ENTERPRISE_ID_PATTERNS = {
        "salesforce": {
            "header_pattern": re.compile(r"sf.*id|salesforce.*id|^id$", re.I),
            "value_pattern": re.compile(r"^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$"),
            "common_prefixes": ["001", "003", "005", "006", "00Q", "00D", "a0", "ka"],
            "description": "Salesforce ID (15/18 chars)",
        },
        "hubspot": {
            "header_pattern": re.compile(r"hubspot.*id|hs.*id|objectid|recordid", re.I),
            "value_pattern": re.compile(r"^\d{6,12}$"),
            "description": "HubSpot ID (numeric)",
        },
        "dynamics": {
            "header_pattern": re.compile(
                r"dynamics.*id|crm.*id|entityid|recordguid", re.I
            ),
            "value_pattern": re.compile(
                r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
            ),
            "description": "Dynamics GUID",
        },
        "uuid": {
            "header_pattern": re.compile(r"uuid|guid|unique.*id", re.I),
            "value_pattern": re.compile(
                r"^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$"
            ),
            "description": "UUID/GUID",
        },
        "stripe": {
            "header_pattern": re.compile(
                r"stripe.*id|payment.*id|charge.*id|customer.*id", re.I
            ),
            "value_pattern": re.compile(
                r"^(cus|sub|ch|pm|pi|price|prod)_[a-zA-Z0-9]{14,}$"
            ),
            "description": "Stripe ID",
        },
        "mongodb": {
            "header_pattern": re.compile(r"objectid|_id|mongoid", re.I),
            "value_pattern": re.compile(r"^[0-9a-fA-F]{24}$"),
            "description": "MongoDB ObjectId",
        },
    }

    ID_NAME_PRIORITY = [
        (re.compile(r"^id$", re.I), 10),
        (re.compile(r"^record_?id$", re.I), 9),
        (re.compile(r"^row_?id$", re.I), 9),
        (re.compile(r"^unique_?id$", re.I), 9),
        (re.compile(r"^salesforce_?id$", re.I), 8),
        (re.compile(r"^sf_?id$", re.I), 8),
        (re.compile(r"^hubspot_?id$", re.I), 8),
        (re.compile(r"^object_?id$", re.I), 7),
        (re.compile(r"^entity_?id$", re.I), 7),
        (re.compile(r".*_id$", re.I), 6),
        (re.compile(r"^id_.*", re.I), 5),
        (re.compile(r".*identifier$", re.I), 4),
        (re.compile(r".*_key$", re.I), 3),
        (re.compile(r".*_code$", re.I), 2),
        (re.compile(r".*_number$", re.I), 1),
    ]

    @classmethod
    def detect_id_column(
        cls, df: pd.DataFrame, sample_size: int = 10000
    ) -> Optional[IDCandidate]:
        if df.empty:
            return None
        candidates = []
        df_sample = df.sample(n=min(len(df), sample_size), random_state=42)
        for col in df.columns:
            candidate = cls._score_id_column(df_sample, col, full_df_size=len(df))
            if candidate.score > 0:
                candidates.append(candidate)
        if not candidates:
            log.warning("No potential ID columns found")
            return None
        candidates.sort(key=lambda x: x.score, reverse=True)
        log.info(
            f"Top ID candidates: {[(c.column_name, f'{c.score:.2f}') for c in candidates[:3]]}"
        )
        best = candidates[0]
        if best.score < 2.0:
            best.issues.append("Low confidence score")
        if best.uniqueness_ratio < 0.95:
            best.issues.append(f"Only {best.uniqueness_ratio:.1%} unique values")
        return best

    @classmethod
    def _score_id_column(
        cls, df: pd.DataFrame, col: str, full_df_size: int
    ) -> IDCandidate:
        score, pattern_matches, issues = 0.0, [], []
        detected_system = None
        header_score = 0
        for pattern, points in cls.ID_NAME_PRIORITY:
            if pattern.search(col):
                header_score = max(header_score, points)
                pattern_matches.append(f"name_pattern:{pattern.pattern}")
        score += header_score
        try:
            unique_count = df[col].nunique()
            uniqueness_ratio = unique_count / len(df)
            if uniqueness_ratio >= 0.99:
                score += 10
            elif uniqueness_ratio >= 0.95:
                score += 8
            elif uniqueness_ratio >= 0.90:
                score += 5
            elif uniqueness_ratio >= 0.80:
                score += 2
            else:
                issues.append(f"Low uniqueness: {uniqueness_ratio:.1%}")
        except Exception:
            uniqueness_ratio = 0
            issues.append("Failed to calculate uniqueness")
        pattern_score = 0
        sample = df[col].dropna().astype(str).head(100)
        if not sample.empty:
            for system, config in cls.ENTERPRISE_ID_PATTERNS.items():
                header_match = config["header_pattern"].search(col)
                match_ratio = sample.str.match(config["value_pattern"]).mean()
                if system == "salesforce" and match_ratio > 0.5:
                    if any(
                        sample.str.startswith(p).any()
                        for p in config["common_prefixes"]
                    ):
                        match_ratio = min(match_ratio * 1.2, 1.0)
                if match_ratio > 0.8 or (header_match and match_ratio > 0.5):
                    system_score = 8 + (2 if header_match else 0)
                    if system_score > pattern_score:
                        pattern_score, detected_system = system_score, system
                        pattern_matches.append(f"system:{system}({match_ratio:.0%})")
        score += pattern_score
        null_ratio = df[col].isnull().sum() / len(df)
        if null_ratio > 0.1:
            score -= min(5, null_ratio * 10)
            issues.append(f"Contains {null_ratio:.1%} null values")
        if uniqueness_ratio < cls.STRICT_UNIQUENESS_FLOOR and score > 12:
            score = 12
            issues.append(
                f"Score capped: uniqueness below {cls.STRICT_UNIQUENESS_FLOOR:.0%}"
            )
        return IDCandidate(
            col,
            round(score, 2),
            uniqueness_ratio,
            pattern_matches,
            detected_system,
            issues,
        )

    @classmethod
    def generate_id_selection_report(cls, df: pd.DataFrame) -> Dict:
        candidates = []
        for col in df.columns[:20]:
            candidate = cls._score_id_column(df.head(1000), col, full_df_size=len(df))
            candidates.append(
                {
                    "column": candidate.column_name,
                    "score": candidate.score,
                    "uniqueness": f"{candidate.uniqueness_ratio:.1%}",
                    "patterns": candidate.pattern_matches,
                    "system": candidate.detected_system,
                    "issues": candidate.issues,
                }
            )
        candidates.sort(key=lambda x: x["score"], reverse=True)
        best = cls.detect_id_column(df)
        return {
            "selected": best.column_name if best else None,
            "confidence": "high"
            if best and best.score > 15
            else "medium"
            if best and best.score > 8
            else "low",
            "all_candidates": candidates[:10],
            "recommendation": cls._get_recommendation(best)
            if best
            else "No suitable ID column found",
        }

    @staticmethod
    def _get_recommendation(candidate: IDCandidate) -> str:
        if candidate.score > 20:
            return (
                f"High confidence: '{candidate.column_name}' is an excellent ID column"
            )
        elif candidate.score > 10:
            return f"Good choice: '{candidate.column_name}' appears to be a valid ID"
        elif candidate.score > 5:
            return f"Acceptable: '{candidate.column_name}' can work but has some issues: {', '.join(candidate.issues)}"
        else:
            return "Low confidence: Consider using a different column or generating row numbers"

    @classmethod
    def detect_string_columns(
        cls, df: pd.DataFrame, sample_size: int = 1000
    ) -> List[str]:
        string_cols = []
        for col in df.columns:
            if pd.api.types.is_string_dtype(df[col]) or pd.api.types.is_object_dtype(
                df[col]
            ):
                string_cols.append(col)
        return string_cols
