"""Automatic domain cleanup for SaaS pre-flight flows."""

from __future__ import annotations

import logging
import re
from typing import Any, Dict, Iterable, List, Optional, Sequence

import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype

from fmatch.core.b2c_bloom_filter import is_b2c_domain
from fmatch.saas.core.psl_root import registrable_root
from fmatch.saas.services.domain_utils import extract_domain_from_url_robust

log = logging.getLogger(__name__)

AUTO_B2C_COL = "__auto_b2c_stripped"
AUTO_EMAIL_COL = "__auto_email_enriched"
AUTO_NAME_SHARD_COL = "__auto_name_shard"
AUTO_DOMAIN_FOR_BLOCKING_COL = "domain_for_blocking"
AUTO_DOMAIN_EXTRACTED_COL = "domain_extracted"
AUTO_NEEDS_COMPANY_LOOKUP_COL = "__auto_needs_company_lookup"

_AUTO_METRICS_ATTR = "_auto_clean_metrics"
_AUTO_APPLIED_ATTR = "_auto_clean_applied"
_AUTO_DOMAIN_HINTS_ATTR = "_auto_clean_domain_hints"
_AUTO_EMAIL_HINTS_ATTR = "_auto_clean_email_hints"
_AUTO_WEBSITE_HINTS_ATTR = "_auto_clean_website_hints"
_AUTO_RAW_PREFIX = "__auto_raw_"

_INVALID_STRINGS = {"", "nan", "none", "null", "n/a", "na", "-", "--"}
_EMAIL_PATTERN = re.compile(r"([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
_DOMAIN_PATTERN = re.compile(r"([A-Za-z0-9][-A-Za-z0-9.]*\.[A-Za-z]{2,})")


def auto_clean_domains(
    df: pd.DataFrame,
    *,
    dataset: str = "source",
    domain_hints: Optional[Sequence[str]] = None,
    email_hints: Optional[Sequence[str]] = None,
    website_hints: Optional[Sequence[str]] = None,
    force: bool = False,
) -> Dict[str, Any]:
    """Normalize domain data, remove consumer hosts, and derive blocking columns."""

    import time

    t_start = time.time()

    if df is None:
        return {}

    if df.attrs.get(_AUTO_APPLIED_ATTR) and not force:
        metrics = dict(df.attrs.get(_AUTO_METRICS_ATTR, {}))
        if metrics:
            return metrics

    dataset_key = (dataset or "source").strip().lower()
    if dataset_key not in {"source", "reference", "ref"}:
        dataset_key = "source"

    row_count = len(df)

    domain_cols = _discover_domain_columns(df, domain_hints)
    website_cols = _discover_website_columns(df, website_hints)
    email_cols = _discover_email_columns(df, email_hints)

    metrics: Dict[str, Any] = {
        "row_count": int(row_count),
        "domain_column_count": len(domain_cols),
        "website_column_count": len(website_cols),
        "email_column_count": len(email_cols),
    }

    if AUTO_NAME_SHARD_COL not in df.columns:
        df[AUTO_NAME_SHARD_COL] = pd.Series(False, index=df.index, dtype=bool)

    domain_union_before = _build_union_for_metrics(df, domain_cols, website_cols)
    fill_before = int(domain_union_before.notna().sum())
    coverage_before = (fill_before / row_count * 100.0) if row_count else 0.0
    b2c_rows_before = (
        int(domain_union_before.apply(_is_b2c_safe).sum()) if fill_before else 0
    )

    metrics["domain_fill_before"] = fill_before
    metrics["domain_coverage_before_pct"] = coverage_before
    metrics["consumer_domain_ratio"] = (
        (b2c_rows_before / fill_before) if fill_before else 0.0
    )

    _ensure_flag_column(df, AUTO_B2C_COL)
    _ensure_flag_column(df, AUTO_EMAIL_COL)

    t_domain_start = time.time()
    total_b2c_rows = 0
    normalized_domain_cols: Dict[str, pd.Series] = {}
    reference_b2c_hits = 0

    for col in domain_cols:
        raw_series = df[col]
        normalized = raw_series.apply(_normalize_domain_value)
        b2c_mask = normalized.apply(_is_b2c_safe)
        flagged_count = int(b2c_mask.sum())

        if dataset_key == "source":
            total_b2c_rows += flagged_count
            if flagged_count:
                df.loc[b2c_mask.index[b2c_mask], AUTO_B2C_COL] = True
                normalized = normalized.where(~b2c_mask, None)
        elif dataset_key == "reference":
            # Preserve reference domains even if they appear in the B2C list.
            if flagged_count:
                reference_b2c_hits += flagged_count
        else:
            total_b2c_rows += flagged_count
            if flagged_count:
                normalized = normalized.where(~b2c_mask, None)

        df[f"{_AUTO_RAW_PREFIX}{col}"] = raw_series
        df[col] = normalized
        normalized_domain_cols[col] = normalized

    metrics["b2c_domains_cleared"] = (
        int(total_b2c_rows) if dataset_key == "source" else 0
    )
    if dataset_key == "reference" and reference_b2c_hits:
        metrics["reference_b2c_hits"] = int(reference_b2c_hits)
    t_domain_elapsed = time.time() - t_domain_start

    domain_from_domains = _coalesce_series(normalized_domain_cols.values(), df.index)

    t_website_start = time.time()
    website_series = {
        col: df[col].apply(
            lambda value: _normalize_domain_value(value, source="website")
        )
        for col in website_cols
    }
    for col, series in website_series.items():
        website_series[col] = series.where(~series.apply(_is_b2c_safe), None)
    domain_from_websites = _coalesce_series(website_series.values(), df.index)
    t_website_elapsed = time.time() - t_website_start

    t_email_start = time.time()
    email_series = {
        col: df[col].apply(lambda value: _normalize_domain_value(value, source="email"))
        for col in email_cols
    }
    for col, series in email_series.items():
        email_series[col] = series.where(~series.apply(_is_b2c_safe), None)
    domain_from_email = _coalesce_series(email_series.values(), df.index)
    t_email_elapsed = time.time() - t_email_start

    email_enriched_mask = (
        domain_from_email.notna()
        & domain_from_domains.isna()
        & domain_from_websites.isna()
    )

    if email_enriched_mask.any():
        df.loc[email_enriched_mask.index[email_enriched_mask], AUTO_EMAIL_COL] = True

    df[AUTO_DOMAIN_EXTRACTED_COL] = domain_from_email.astype(object)

    # Prioritize organization website domains over contact/email domains when constructing
    # the canonical blocking column. Only fall back to the legacy domain column (often an
    # email-derived host) when we cannot derive a usable website domain, then finally use
    # the email host as a last resort.
    domain_union_after = domain_from_websites.combine_first(
        domain_from_domains
    ).combine_first(domain_from_email)

    df[AUTO_DOMAIN_FOR_BLOCKING_COL] = domain_union_after.astype(object)

    company_cols = _discover_company_columns(df)
    domain_missing_mask = domain_union_after.isna() | domain_union_after.eq("")
    if AUTO_NEEDS_COMPANY_LOOKUP_COL not in df.columns:
        df[AUTO_NEEDS_COMPANY_LOOKUP_COL] = pd.Series(False, index=df.index, dtype=bool)
    company_name_mask = pd.Series(False, index=df.index, dtype=bool)
    for col in company_cols:
        series = df[col]
        if series is None:
            continue
        series_str = series.astype(str).str.strip()
        company_name_mask |= (
            series_str.notna()
            & series_str.ne("")
            & ~series_str.str.lower().isin({"nan", "none", "null"})
        )
    needs_company_lookup = domain_missing_mask & company_name_mask
    df.loc[:, AUTO_NEEDS_COMPANY_LOOKUP_COL] = needs_company_lookup.astype(bool)

    fill_after = int(domain_union_after.notna().sum())
    coverage_after = (fill_after / row_count * 100.0) if row_count else 0.0
    metrics["domain_fill_after"] = fill_after
    metrics["domain_coverage_after_pct"] = coverage_after
    metrics["domain_fill_delta"] = fill_after - fill_before
    metrics["domain_enriched_from_email_count"] = int(email_enriched_mask.sum())
    metrics["needs_company_to_domain_count"] = int(needs_company_lookup.sum())

    if domain_cols:
        for col in domain_cols:
            series = df[col]
            needs_fill = series.isna() | series.eq("")
            if needs_fill.any():
                df.loc[needs_fill.index[needs_fill], col] = domain_union_after[
                    needs_fill
                ]

    df.attrs[_AUTO_METRICS_ATTR] = dict(metrics)
    df.attrs[_AUTO_APPLIED_ATTR] = True
    if domain_cols:
        df.attrs[_AUTO_DOMAIN_HINTS_ATTR] = list(domain_cols)
    if email_cols:
        df.attrs[_AUTO_EMAIL_HINTS_ATTR] = list(email_cols)
    if website_cols:
        df.attrs[_AUTO_WEBSITE_HINTS_ATTR] = list(website_cols)

    log.info(
        "[AUTO CLEAN][%s] rows=%s domain_cols=%s website_cols=%s email_cols=%s b2c_cleared=%s fill %s -> %s",
        dataset_key,
        row_count,
        len(domain_cols),
        len(website_cols),
        len(email_cols),
        metrics.get("b2c_domains_cleared", 0),
        fill_before,
        fill_after,
    )

    t_elapsed = time.time() - t_start
    log.info(
        "[AUTO_CLEAN:%s] Complete in %.2fs (domain: %.2fs, website: %.2fs, email: %.2fs) - %d rows, %d domain cols, %d email cols, %d B2C cleared, coverage: %.1f%% -> %.1f%%",
        dataset.upper(),
        t_elapsed,
        t_domain_elapsed,
        t_website_elapsed,
        t_email_elapsed,
        row_count,
        len(domain_cols),
        len(email_cols),
        metrics.get("b2c_domains_cleared", 0),
        coverage_before,
        (fill_after / row_count * 100) if row_count else 0,
    )

    return metrics


def _discover_domain_columns(
    df: pd.DataFrame, hints: Optional[Sequence[str]]
) -> List[str]:
    result: List[str] = []
    for name in _iter_valid_names(hints, df.columns):
        if name not in result:
            result.append(name)
    for col in df.columns:
        if _is_auto_column(col) or col in result:
            continue
        lowered = str(col).lower()
        if "domain" in lowered or lowered.endswith("_dom"):
            result.append(col)
    return result


def _discover_website_columns(
    df: pd.DataFrame, hints: Optional[Sequence[str]]
) -> List[str]:
    result: List[str] = []
    for name in _iter_valid_names(hints, df.columns):
        if name not in result:
            result.append(name)
    for col in df.columns:
        if _is_auto_column(col) or col in result:
            continue
        lowered = str(col).lower()
        if "website" in lowered or lowered.endswith("_url") or " url" in lowered:
            result.append(col)
    return result


def _discover_email_columns(
    df: pd.DataFrame, hints: Optional[Sequence[str]]
) -> List[str]:
    result: List[str] = []
    for name in _iter_valid_names(hints, df.columns):
        if name not in result:
            result.append(name)
    for col in df.columns:
        if _is_auto_column(col) or col in result:
            continue
        lowered = str(col).lower()
        if "email" in lowered or "e-mail" in lowered:
            result.append(col)
    for col in df.columns:
        if _is_auto_column(col) or col in result:
            continue
        series = df[col]
        if is_numeric_dtype(series.dtype):
            continue
        sample = series.dropna().astype(str).head(200)
        if len(sample) < 5:
            continue
        hit_rate = sample.str.contains("@").mean()
        if hit_rate >= 0.2:
            result.append(col)
    return result


def _discover_company_columns(df: pd.DataFrame) -> List[str]:
    result: List[str] = []
    tokens = (
        "company",
        "account",
        "organization",
        "organisation",
        "business",
        "employer",
        "firm",
        "corp",
        "corporation",
        "enterprise",
    )
    for col in df.columns:
        if _is_auto_column(col) or col in result:
            continue
        lowered = str(col).lower()
        if any(tok in lowered for tok in tokens):
            result.append(col)
    return result


def _iter_valid_names(
    hints: Optional[Sequence[str]], columns: Iterable[str]
) -> List[str]:
    if not hints:
        return []
    col_set = set(columns)
    names: List[str] = []
    for name in hints:
        if not name:
            continue
        if name in col_set and not _is_auto_column(name):
            names.append(name)
    return names


def _coalesce_series(series_iter: Iterable[pd.Series], index: pd.Index) -> pd.Series:
    result = pd.Series([None] * len(index), index=index, dtype=object)
    for series in series_iter:
        if series is None:
            continue
        result = result.combine_first(series.astype(object))
    return result


def _ensure_flag_column(df: pd.DataFrame, column: str) -> None:
    if column not in df.columns:
        df[column] = pd.Series(False, index=df.index, dtype=bool)
    else:
        df[column] = df[column].fillna(False).astype(bool)


def _is_auto_column(name: str) -> bool:
    lowered = str(name).lower()
    return lowered.startswith("__auto_") or lowered in {
        AUTO_DOMAIN_FOR_BLOCKING_COL.lower(),
        AUTO_DOMAIN_EXTRACTED_COL.lower(),
    }


def _normalize_domain_value(value: object, *, source: str = "domain") -> Optional[str]:
    if value is None:
        return None
    if isinstance(value, float) and np.isnan(value):
        return None
    text = str(value).strip()
    if not text:
        return None
    lowered = text.lower()

    lowered = lowered.replace("\r", " ").replace("\n", " ").strip()
    if lowered in _INVALID_STRINGS:
        return None

    lowered = lowered.replace("mailto:", "")

    email_domain = _extract_email_domain(lowered)
    if email_domain:
        lowered = email_domain

    if "://" in lowered or "/" in lowered or source == "website":
        host = extract_domain_from_url_robust(lowered)
        if host:
            lowered = host

    if "/" in lowered:
        lowered = lowered.split("/", 1)[0]
    if ":" in lowered:
        lowered = lowered.split(":", 1)[0]

    lowered = lowered.strip(" .;\"'")
    if lowered.startswith("www."):
        lowered = lowered[4:]
    if " " in lowered:
        lowered = lowered.split()[0]

    match = _DOMAIN_PATTERN.search(lowered)
    candidate = match.group(1) if match else lowered
    if "." not in candidate:
        return None

    root = registrable_root(candidate)
    if not root:
        return None
    return root.lower()


def _extract_email_domain(value: str) -> Optional[str]:
    match = _EMAIL_PATTERN.search(value)
    if match:
        return match.group(2).lower()
    return None


def _is_b2c_safe(domain: Optional[str]) -> bool:
    if not domain:
        return False
    try:
        return bool(is_b2c_domain(domain))
    except Exception:
        return False


def _build_union_for_metrics(
    df: pd.DataFrame,
    domain_cols: Sequence[str],
    website_cols: Sequence[str],
) -> pd.Series:
    union = pd.Series([None] * len(df), index=df.index, dtype=object)
    for col in domain_cols:
        normalized = df[col].apply(_normalize_domain_value)
        union = union.combine_first(normalized)
    for col in website_cols:
        normalized = df[col].apply(
            lambda value: _normalize_domain_value(value, source="website")
        )
        union = union.combine_first(normalized)
    return union.astype(object)
