"""
Shared domain utilities for consistent domain extraction and family enrichment
across all services (L2A, Dedupe, Routing, etc.)
"""

import logging
import pandas as pd
import re
import time
from typing import Optional, Tuple
from urllib.parse import urlparse
from functools import wraps

logger = logging.getLogger(__name__)

try:
    import tldextract

    # Disable live TLD list updates for consistent performance
    _tld_extractor = tldextract.TLDExtract(cache_dir=False, suffix_list_urls=())
    TLDEXTRACT_AVAILABLE = True
except ImportError:
    TLDEXTRACT_AVAILABLE = False
    logger.warning("tldextract not available, falling back to basic domain extraction")

# Pre-compile regex patterns for performance
PROTOCOL_PATTERN = re.compile(r"^https?://", re.IGNORECASE)
WWW_PATTERN = re.compile(r"^www\.", re.IGNORECASE)
DOMAIN_VALIDATE_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$")
INVALID_DOMAIN_VALUES = {"nan", "none", "", "n/a", "null", "-"}
FALLBACK_TWO_PART_TLDS = {
    "co.uk",
    "com.au",
    "co.jp",
    "co.nz",
    "co.in",
    "co.za",
    "com.br",
    "com.cn",
    "com.sg",
    "com.hk",
    "com.mx",
    "org.uk",
    "gov.uk",
    "ac.uk",
    "net.au",
    "org.au",
    "co.id",
    "co.kr",
    "co.th",
    "com.tw",
    "com.ar",
}


def normalize_domain_series(series: pd.Series) -> pd.Series:
    """Normalize domains for exact matching (strip scheme, www, ports, etc.)."""
    if series.empty:
        return series

    s = series.fillna("").astype(str).str.strip().str.lower()
    s = s.str.replace(PROTOCOL_PATTERN, "", regex=True)
    s = s.str.replace(WWW_PATTERN, "", regex=True)
    s = s.str.split("@").str[-1]
    s = s.str.split("/").str[0]
    s = s.str.split("?").str[0]
    s = s.str.split("#").str[0]
    s = s.str.split(":").str[0]
    s = s.str.rstrip(".")
    s = s.str.replace(r"[^a-z0-9.-]", "", regex=True)

    valid_mask = (
        (s.str.contains(".", na=False))
        & (s.str.len() > 3)
        & (~s.isin(INVALID_DOMAIN_VALUES))
    )
    return s.where(valid_mask, None)


def get_registrable_domain(domain: Optional[str]) -> Optional[str]:
    """Return the registrable (eTLD+1) domain for a host."""
    if not domain:
        return None
    domain = str(domain).strip().lower().rstrip(".")
    if not domain or domain in INVALID_DOMAIN_VALUES:
        return None

    if TLDEXTRACT_AVAILABLE:
        try:
            extracted = _tld_extractor(domain)
        except Exception:
            extracted = None
        if extracted:
            if extracted.domain and extracted.suffix:
                return f"{extracted.domain}.{extracted.suffix}"
            if extracted.domain:
                return extracted.domain

    parts = domain.split(".")
    if len(parts) <= 2:
        return domain
    last_two = ".".join(parts[-2:])
    if last_two in FALLBACK_TWO_PART_TLDS and len(parts) >= 3:
        return ".".join(parts[-3:])
    return last_two


def extract_registrable_domains(series: pd.Series) -> pd.Series:
    """Vectorized registrable domain extraction."""
    if series.empty:
        return series
    normalized = normalize_domain_series(series)
    return normalized.apply(get_registrable_domain)


def monitor_performance(func):
    """Decorator to monitor domain extraction performance"""

    @wraps(func)
    def wrapper(df, *args, **kwargs):
        start_time = time.time()
        row_count = len(df) if isinstance(df, pd.DataFrame) else 0

        result = func(df, *args, **kwargs)

        elapsed = time.time() - start_time
        if row_count > 1000:
            logger.info(
                f"Domain extraction for {row_count:,} rows took {elapsed:.2f}s "
                f"({row_count/elapsed:.0f} rows/sec)"
            )
        elif elapsed > 1.0:  # Log if it took more than 1 second
            logger.debug(f"Domain extraction took {elapsed:.2f}s for {row_count} rows")

        return result

    return wrapper


def extract_domains_vectorized(
    series: pd.Series, source_type: str = "url"
) -> pd.Series:
    """
    Vectorized domain extraction for better performance.

    Args:
        series: Pandas series containing URLs or emails
        source_type: 'url' for websites or 'email' for email addresses

    Returns:
        Series with extracted domains
    """
    if series.empty:
        return series

    # Convert to string and handle nulls
    series_str = series.fillna("").astype(str).str.strip().str.lower()

    if source_type == "email":
        # Extract domain from email
        domains = series_str.str.split("@").str[-1]
        # Validate domains
        valid_mask = (domains.str.contains(".", na=False)) & (domains.str.len() > 3)
        domains = domains.where(valid_mask, None)
    else:
        # URL processing
        # Remove protocols
        domains = series_str.str.replace(PROTOCOL_PATTERN, "", regex=True)
        # Remove www
        domains = domains.str.replace(WWW_PATTERN, "", regex=True)
        # Extract domain (before first /)
        domains = domains.str.split("/").str[0]
        # Remove port if present
        domains = domains.str.split(":").str[0]

        # Validate domains
        valid_mask = (
            (domains.str.contains(".", na=False))
            & (domains.str.len() > 3)
            & (~domains.isin(["nan", "none", "", "n/a", "null", "-"]))
        )
        domains = domains.where(valid_mask, None)

    return domains


def extract_domain_from_url_robust(url) -> Optional[str]:
    """
    Enhanced domain extraction handling edge cases including:
    - International domains (IDN)
    - IP addresses
    - Subdomains (configurable behavior)
    - Port numbers
    - Two-part TLDs (co.uk, com.au, etc.) - auto-maintained via tldextract

    Note on subdomain handling:
    - By default, returns registered domain only (example.com from api.example.com)
    - This ensures proper matching within domain families
    - Subdomains are preserved in the raw data for specific matching if needed
    """
    if not url or pd.isna(url):
        return None

    url_str = str(url).strip().lower()

    # Skip invalid values
    invalid_values = {"-", "n/a", "na", "null", "undefined", "none", "nan", ""}
    if url_str in invalid_values:
        return None

    # Check if it's an IP address (skip for domain matching)
    try:
        # Handle IP with port (e.g., "192.168.1.1:8080")
        potential_ip = url_str.split(":")[0].split("/")[0]
        # Simple IP validation
        parts = potential_ip.split(".")
        if len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts):
            logger.debug(f"Skipping IP address: {potential_ip}")
            return None
    except (ValueError, AttributeError):
        pass

    # Use tldextract if available for robust parsing
    if TLDEXTRACT_AVAILABLE:
        try:
            # Add protocol if missing for better parsing
            if not url_str.startswith(("http://", "https://", "ftp://")):
                url_str = "https://" + url_str

            # Extract components
            extracted = _tld_extractor(url_str)

            # Build the registered domain (without subdomain)
            # This ensures api.microsoft.com and portal.microsoft.com both -> microsoft.com
            if extracted.domain and extracted.suffix:
                return f"{extracted.domain}.{extracted.suffix}"
            elif extracted.domain:
                return extracted.domain
            else:
                return None

        except Exception as e:
            logger.debug(f"tldextract failed for '{url}', falling back: {e}")
            # Fall through to manual parsing

    # Fallback: Manual parsing (if tldextract not available or failed)
    # Add protocol if missing
    if not url_str.startswith(("http://", "https://", "ftp://")):
        if "/" not in url_str and "." in url_str and ":" not in url_str:
            domain = url_str
        else:
            url_str = "https://" + url_str
            try:
                parsed = urlparse(url_str)
                domain = parsed.netloc or parsed.path.split("/")[0]
            except Exception:
                domain = url_str
    else:
        try:
            parsed = urlparse(url_str)
            domain = parsed.netloc or parsed.path.split("/")[0]
        except Exception:
            return None

    # Remove port if present
    domain = domain.split(":")[0]

    # Remove www prefix
    if domain.startswith("www."):
        domain = domain[4:]

    # Manual two-part TLD handling (fallback only)
    parts = domain.split(".")
    if len(parts) > 2:
        # Common two-part TLDs (maintained for fallback)
        two_part_tlds = {
            "co.uk",
            "com.au",
            "co.jp",
            "co.nz",
            "co.in",
            "co.za",
            "com.br",
            "com.cn",
            "com.sg",
            "com.hk",
            "com.mx",
            "org.uk",
            "gov.uk",
            "ac.uk",
            "net.au",
            "org.au",
            "co.id",
            "co.kr",
            "co.th",
            "com.tw",
            "com.ar",
        }

        if ".".join(parts[-2:]) in two_part_tlds:
            # Return registered domain only (no subdomain)
            domain = ".".join(parts[-3:]) if len(parts) >= 3 else domain
        else:
            # Return registered domain only (no subdomain)
            domain = ".".join(parts[-2:])

    # Final validation
    if domain and "." in domain and len(domain) > 3:
        # Check for valid characters
        if all(c in "abcdefghijklmnopqrstuvwxyz0123456789.-_" for c in domain):
            return domain

    return None


# Keep the original function name for backward compatibility
def extract_domain_from_url(url) -> Optional[str]:
    """
    Extract domain from URL or website field.
    Handles various formats and edge cases.
    """
    return extract_domain_from_url_robust(url)


def extract_domain_from_email(email) -> Optional[str]:
    """Extract domain from email address."""
    if not email or pd.isna(email) or "@" not in str(email):
        return None

    try:
        domain = str(email).strip().lower().split("@")[-1]
        # Validate it looks like a domain
        if domain and "." in domain and len(domain) > 3:
            return domain
        return None
    except Exception as e:
        logger.debug(f"Failed to extract domain from email '{email}': {e}")
        return None


def enrich_with_domain_families_batched(
    df: pd.DataFrame, domain_service=None, domain_col: str = "Domain"
) -> Tuple[pd.DataFrame, float]:
    """
    Add DomainFamily column to any DataFrame with Domain column using batch lookups.
    Much faster than individual lookups for large datasets.
    Returns the enriched DataFrame and coverage percentage.
    """
    if domain_service is None:
        from .domain_family import get_registry

        domain_service = get_registry()

    if domain_col in df.columns:
        # Get unique domains for batch processing
        unique_domains = df[domain_col].dropna().unique()

        # Filter out invalid values
        invalid_values = {"nan", "none", "", "n/a", "null", "-"}
        valid_domains = [
            str(d).lower().strip()
            for d in unique_domains
            if d and str(d).lower().strip() not in invalid_values
        ]

        # Use true batch lookup if available, otherwise fall back to individual lookups
        family_map = {}

        # Check if batch method is available
        if hasattr(domain_service, "lookup_families_batch"):
            try:
                # Use efficient batch lookup
                family_map = domain_service.lookup_families_batch(valid_domains)
                logger.debug(f"Batch lookup returned {len(family_map)} domain families")
            except Exception as e:
                logger.warning(
                    f"Batch lookup failed, falling back to individual lookups: {e}"
                )
                # Fall back to individual lookups
                for domain in valid_domains:
                    try:
                        family_id, confidence = domain_service.lookup_family_by_domain(
                            domain
                        )
                        if family_id:
                            family_map[domain] = family_id
                    except Exception as e:
                        logger.debug(f"Domain lookup failed for '{domain}': {e}")
                        continue
        else:
            # Registry doesn't support batch lookup, use individual lookups
            logger.debug("Batch lookup not available, using individual lookups")
            for domain in valid_domains:
                try:
                    family_id, confidence = domain_service.lookup_family_by_domain(
                        domain
                    )
                    if family_id:
                        family_map[domain] = family_id
                except AttributeError as e:
                    logger.warning(f"Domain service not properly initialized: {e}")
                    break  # Stop if service not available
                except Exception as e:
                    logger.debug(f"Domain lookup failed for '{domain}': {e}")
                    continue  # Skip this domain but continue with others

        # Normalize domain column for mapping
        df_domain_normalized = df[domain_col].astype(str).str.lower().str.strip()

        # Vectorized map back to DataFrame
        df["DomainFamily"] = df_domain_normalized.map(family_map)

        # Calculate coverage
        total_with_domain = df[domain_col].notna().sum()
        if total_with_domain > 0:
            families_found = df["DomainFamily"].notna().sum()
            coverage = (families_found / total_with_domain) * 100
            logger.info(
                f"Domain family enrichment: {families_found}/{total_with_domain} domains "
                f"mapped to families ({coverage:.1f}% coverage)"
            )
        else:
            coverage = 0.0
            logger.debug("No domains to enrich with families")

        return df, coverage

    logger.debug(f"Column '{domain_col}' not found in DataFrame")
    return df, 0.0


# Use batched version as default
enrich_with_domain_families = enrich_with_domain_families_batched


@monitor_performance
def extract_and_enrich_domains(
    df: pd.DataFrame,
    domain_service=None,
    prefer_website: bool = True,
    enrich_families: bool = None,
) -> Tuple[pd.DataFrame, float]:
    """
    Extract Domain from Website/Email if missing, then optionally enrich with families.

    Args:
        df: DataFrame to process
        domain_service: Optional domain registry service
        prefer_website: Use Website field before Email for extraction
        enrich_families: Whether to enrich with domain families
                        (None = check FEATURE_DOMAIN_FAMILY env var)

    Returns:
        Tuple of (enriched DataFrame, family coverage percentage)
    """
    # Check feature flag if not explicitly set
    if enrich_families is None:
        import os

        enrich_families = os.getenv("FEATURE_DOMAIN_FAMILY", "false").lower() == "true"
    # Extract domain if missing - use vectorized operations
    if "Domain" not in df.columns:
        extracted_count = 0

        # Try Website first (usually cleaner)
        if prefer_website and "Website" in df.columns:
            logger.info("Extracting domains from Website column (vectorized)")
            df["Domain"] = extract_domains_vectorized(df["Website"], source_type="url")
            extracted_count = df["Domain"].notna().sum()
            logger.info(f"Extracted {extracted_count} domains from Website")

        # Try Email as fallback or primary
        if (not prefer_website or extracted_count == 0) and "Email" in df.columns:
            if "Domain" in df.columns:
                # Fill nulls with email domains
                email_domains = extract_domains_vectorized(
                    df["Email"], source_type="email"
                )
                df["Domain"] = df["Domain"].fillna(email_domains)
                additional = email_domains.notna().sum() - extracted_count
                logger.info(f"Extracted {additional} additional domains from Email")
            else:
                # Use email as primary source
                logger.info("Extracting domains from Email column (vectorized)")
                df["Domain"] = extract_domains_vectorized(
                    df["Email"], source_type="email"
                )
                extracted_count = df["Domain"].notna().sum()
                logger.info(f"Extracted {extracted_count} domains from Email")

    # Normalize domains (lowercase, strip)
    if "Domain" in df.columns:
        df["Domain"] = df["Domain"].astype(str).str.strip().str.lower()
        df.loc[df["Domain"].isin(["nan", "none", ""]), "Domain"] = None

    # Conditionally enrich with families based on feature flag
    if enrich_families:
        logger.info("Domain family enrichment enabled via feature flag")
        return enrich_with_domain_families(df, domain_service)
    else:
        logger.debug(
            "Domain family enrichment disabled (set FEATURE_DOMAIN_FAMILY=true to enable)"
        )
        return df, 0.0


def get_domain_family_mappings(
    source_cols: list, target_cols: list, weight_multiplier: float = 1.0
) -> list:
    """
    Generate domain family mapping configurations for matching.
    Returns list of mapping dictionaries suitable for match configs.
    """
    mappings = []

    # Check for DomainFamily columns
    if "DomainFamily" in source_cols and "DomainFamily" in target_cols:
        mappings.append(
            {
                "source": "DomainFamily",
                "reference": "DomainFamily",
                "similarity_measure": "exact",
                "algorithm": "Exact",
                "weight": 0.9 * weight_multiplier,
                "is_domain_family": True,
            }
        )
        logger.debug("Added DomainFamily exact matching")

    # Check for regular Domain columns (fallback)
    if "Domain" in source_cols and "Domain" in target_cols:
        mappings.append(
            {
                "source": "Domain",
                "reference": "Domain",
                "similarity_measure": "token_sort_ratio",
                "algorithm": "Domain",
                "weight": 0.6 * weight_multiplier,
                "is_domain": True,
            }
        )
        logger.debug("Added Domain fuzzy matching")

    # Cross-match Domain with Website if available
    if "Domain" in source_cols and "Website" in target_cols:
        mappings.append(
            {
                "source": "Domain",
                "reference": "Website",
                "similarity_measure": "token_sort_ratio",
                "algorithm": "Domain",
                "weight": 0.5 * weight_multiplier,
                "is_cross_domain": True,
            }
        )
        logger.debug("Added Domain-Website cross matching")

    return mappings


def is_same_domain_family(domain1: str, domain2: str, domain_service=None) -> bool:
    """
    Check if two domains belong to the same family.
    Useful for quick checks in filtering/routing logic.
    """
    if not domain1 or not domain2:
        return False

    # Normalize domains
    domain1_str = str(domain1).lower().strip()
    domain2_str = str(domain2).lower().strip()

    # Check for invalid values
    invalid_values = {"nan", "none", "", "n/a", "null", "-"}
    if domain1_str in invalid_values or domain2_str in invalid_values:
        return False

    if domain_service is None:
        try:
            from .domain_family import get_registry

            domain_service = get_registry()
        except ImportError as e:
            logger.error(f"Failed to import domain registry: {e}")
            return False

    try:
        family1, conf1 = domain_service.lookup_family_by_domain(domain1_str)
        family2, conf2 = domain_service.lookup_family_by_domain(domain2_str)

        # Both must have families and they must match
        return family1 is not None and family1 == family2
    except AttributeError as e:
        logger.warning(f"Domain service method not available: {e}")
        return False
    except Exception as e:
        logger.debug(f"Failed to check domain family relationship: {e}")
        return False
