from __future__ import annotations

from typing import Any, Dict, List, Optional

import pandas as pd

from ..services.match_gateway import MatchGateway, MatchResult
from ..services.domain_utils import extract_registrable_domains, normalize_domain_series


class IDHunterGatewayAdapter:
    """Adapts ID Hunter to MatchGateway."""

    DOMAIN_NORM_COL = "__domain_norm"
    DOMAIN_REG_COL = "__domain_reg"
    NAME_GATE_THRESHOLD = 0.85

    def __init__(self, gateway: MatchGateway):
        self.gateway = gateway

    @classmethod
    async def create(cls) -> "IDHunterGatewayAdapter":
        gateway = await MatchGateway.create()
        return cls(gateway)

    async def match_accounts(
        self,
        source_df: pd.DataFrame,
        reference_df: pd.DataFrame,
        threshold: float = 0.85,
        use_domain: bool = True,
        use_fuzzy: bool = True,
    ) -> MatchResult:
        source_df, reference_df = self._ensure_id_columns(source_df, reference_df)
        source_df = self._add_domain_columns(source_df)
        reference_df = self._add_domain_columns(reference_df)

        mappings = self._build_mappings(
            source_df, reference_df, use_domain, use_fuzzy=use_fuzzy
        )
        if not mappings:
            raise ValueError("No valid mappings available for ID Hunter matching.")

        primary_result = await self.gateway.match(
            source_df,
            reference_df,
            threshold=threshold,
            mappings=mappings,
        )

        if not use_domain:
            return primary_result

        fallback_result = await self._match_on_registrable_domain(
            source_df,
            reference_df,
            threshold=threshold,
            use_fuzzy=use_fuzzy,
        )
        if fallback_result is None or fallback_result.matches.empty:
            return primary_result

        combined = pd.concat(
            [primary_result.matches, fallback_result.matches], ignore_index=True
        )
        stats = self._build_stats(combined, threshold)
        return MatchResult(
            matches=combined,
            stats=stats,
            config_used=primary_result.config_used,
            elapsed_ms=primary_result.elapsed_ms,
        )

    def _ensure_id_columns(
        self, source_df: pd.DataFrame, reference_df: pd.DataFrame
    ) -> tuple[pd.DataFrame, pd.DataFrame]:
        source_df = source_df.copy()
        reference_df = reference_df.copy()

        if "__source_id" not in source_df.columns:
            if "row_id" in source_df.columns:
                source_df["__source_id"] = source_df["row_id"]
            else:
                source_df["__source_id"] = range(len(source_df))

        if "__reference_id" not in reference_df.columns:
            if "sf_id" in reference_df.columns:
                reference_df["__reference_id"] = reference_df["sf_id"]
            else:
                reference_df["__reference_id"] = range(len(reference_df))

        return source_df, reference_df

    def _add_domain_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        df = df.copy()
        if "domain" in df.columns:
            domain_norm = normalize_domain_series(df["domain"])
        else:
            domain_norm = pd.Series([None] * len(df))
        df[self.DOMAIN_NORM_COL] = domain_norm
        df[self.DOMAIN_REG_COL] = extract_registrable_domains(domain_norm)
        return df

    def _build_stats(self, matches: pd.DataFrame, threshold: float) -> Dict[str, Any]:
        if matches is None or matches.empty:
            return {"total": 0}
        above = 0
        if "score" in matches.columns:
            above = int((matches["score"] >= threshold).sum())
        return {"total": len(matches), "above_threshold": above}

    def _build_mappings(
        self,
        source_df: pd.DataFrame,
        reference_df: pd.DataFrame,
        use_domain: bool,
        use_fuzzy: bool = True,
    ) -> List[Dict[str, Any]]:
        mappings: List[Dict[str, Any]] = []
        if (
            use_domain
            and self.DOMAIN_NORM_COL in source_df.columns
            and self.DOMAIN_NORM_COL in reference_df.columns
        ):
            mappings.append(
                {
                    "source": self.DOMAIN_NORM_COL,
                    "reference": self.DOMAIN_NORM_COL,
                    "weight": 0.6,
                    "algorithm": "Exact",
                }
            )
        if (
            use_fuzzy
            and "company_name" in source_df.columns
            and "company_name" in reference_df.columns
        ):
            mappings.append(
                {
                    "source": "company_name",
                    "reference": "company_name",
                    "weight": 0.4,
                    "algorithm": "WRatio",
                }
            )
        return mappings

    async def _match_on_registrable_domain(
        self,
        source_df: pd.DataFrame,
        reference_df: pd.DataFrame,
        threshold: float,
        use_fuzzy: bool = True,
    ) -> Optional[MatchResult]:
        if (
            self.DOMAIN_REG_COL not in source_df.columns
            or self.DOMAIN_REG_COL not in reference_df.columns
        ):
            return None

        src_subset = source_df[source_df[self.DOMAIN_REG_COL].notna()].copy()
        ref_subset = reference_df[reference_df[self.DOMAIN_REG_COL].notna()].copy()
        if src_subset.empty or ref_subset.empty:
            return None

        mappings = [
            {
                "source": self.DOMAIN_REG_COL,
                "reference": self.DOMAIN_REG_COL,
                "weight": 1.0 if not use_fuzzy else 0.6,
                "algorithm": "Exact",
            }
        ]
        if use_fuzzy:
            mappings.append(
                {
                    "source": "company_name",
                    "reference": "company_name",
                    "weight": 0.4,
                    "algorithm": "WRatio",
                }
            )

        result = await self.gateway.match(
            src_subset,
            ref_subset,
            threshold=threshold,
            mappings=mappings,
        )

        filtered = self._filter_reg_domain_matches(
            result.matches, source_df, reference_df
        )
        if filtered is None or filtered.empty:
            return None
        return MatchResult(
            matches=filtered,
            stats=self._build_stats(filtered, threshold),
            config_used=result.config_used,
            elapsed_ms=result.elapsed_ms,
        )

    def _filter_reg_domain_matches(
        self,
        matches: pd.DataFrame,
        source_df: pd.DataFrame,
        reference_df: pd.DataFrame,
    ) -> pd.DataFrame:
        if matches is None or matches.empty:
            return pd.DataFrame()
        if "source_id" not in matches.columns or "reference_id" not in matches.columns:
            return pd.DataFrame()

        src_index = source_df["__source_id"].astype(str)
        ref_index = reference_df["__reference_id"].astype(str)

        src_domain = pd.Series(source_df[self.DOMAIN_NORM_COL].values, index=src_index)
        ref_domain = pd.Series(
            reference_df[self.DOMAIN_NORM_COL].values, index=ref_index
        )
        src_reg = pd.Series(source_df[self.DOMAIN_REG_COL].values, index=src_index)
        ref_reg = pd.Series(reference_df[self.DOMAIN_REG_COL].values, index=ref_index)
        src_name_col = (
            source_df["company_name"]
            if "company_name" in source_df.columns
            else pd.Series([None] * len(source_df))
        )
        ref_name_col = (
            reference_df["company_name"]
            if "company_name" in reference_df.columns
            else pd.Series([None] * len(reference_df))
        )
        src_name = pd.Series(src_name_col.values, index=src_index)
        ref_name = pd.Series(ref_name_col.values, index=ref_index)

        df = matches.copy()
        df["__src_id"] = df["source_id"].astype(str)
        df["__ref_id"] = df["reference_id"].astype(str)

        df["__src_domain"] = df["__src_id"].map(src_domain)
        df["__ref_domain"] = df["__ref_id"].map(ref_domain)
        df["__src_reg"] = df["__src_id"].map(src_reg)
        df["__ref_reg"] = df["__ref_id"].map(ref_reg)

        reg_match = df["__src_reg"].notna() & (
            df["__src_reg"] == df["__ref_reg"]
        )
        host_match = df["__src_domain"].notna() & (
            df["__src_domain"] == df["__ref_domain"]
        )
        candidate = df[reg_match & ~host_match].copy()
        if candidate.empty:
            return pd.DataFrame()

        candidate["__src_name"] = candidate["__src_id"].map(src_name).fillna("")
        candidate["__ref_name"] = candidate["__ref_id"].map(ref_name).fillna("")

        def _name_score(row) -> float:
            try:
                src = str(row["__src_name"] or "").strip()
                ref = str(row["__ref_name"] or "").strip()
                if not src or not ref:
                    return 0.0
                return float(fuzz.WRatio(src, ref)) / 100.0
            except Exception:
                return 0.0

        candidate["__name_score"] = candidate.apply(_name_score, axis=1)
        candidate = candidate[candidate["__name_score"] >= self.NAME_GATE_THRESHOLD]
        if candidate.empty:
            return pd.DataFrame()

        drop_cols = [
            "__src_id",
            "__ref_id",
            "__src_domain",
            "__ref_domain",
            "__src_reg",
            "__ref_reg",
            "__src_name",
            "__ref_name",
            "__name_score",
        ]
        return candidate.drop(columns=[c for c in drop_cols if c in candidate.columns])
