"""DNS validation and domain candidate generation.

This module provides pure functions for generating domain candidates
from company names and validating them via DNS.
"""

from __future__ import annotations

import logging
import re
import time
from typing import Any, Dict, List, Optional, Set

logger = logging.getLogger(__name__)

# Try to import dnspython for DNS validation
try:
    import dns.resolver
    import dns.exception

    HAS_DNS = True
except ImportError:
    HAS_DNS = False
    logger.debug("dnspython not available, DNS validation disabled")


_SLUG = re.compile(r"[^a-z0-9]+")

ORG_HINT_EDU = {"university", "college", "school", "academy", "institute"}
ORG_HINT_HEALTH = {
    "health",
    "hospital",
    "clinic",
    "medical",
    "med",
    "care",
    "medicine",
    "physician",
}
ORG_HINT_GOV = {
    "department",
    "ministry",
    "city",
    "county",
    "state",
    "government",
    "municipal",
    "district",
}
ORG_HINT_NONPROFIT = {
    "foundation",
    "association",
    "nonprofit",
    "ngo",
    "charity",
    "society",
    "alliance",
}


def infer_org_hint(tokens: List[str]) -> Optional[str]:
    """Infer organization type from name tokens.

    Args:
        tokens: List of name tokens

    Returns:
        Organization hint: 'edu', 'health', 'gov', 'nonprofit', or None
    """
    lowered = [t.lower() for t in tokens if t]
    token_set = set(lowered)
    if token_set & ORG_HINT_EDU:
        return "edu"
    if token_set & ORG_HINT_HEALTH:
        return "health"
    if token_set & ORG_HINT_GOV or any(" of " in t for t in lowered):
        return "gov"
    if token_set & ORG_HINT_NONPROFIT:
        return "nonprofit"
    return None


def generate_domain_candidates(
    company_name: str,
    max_candidates: int = 5,
    org_hint: Optional[str] = None,
    prefer_tlds: Optional[List[str]] = None,
) -> List[str]:
    """Generate likely domain candidates from a company name.

    Examples:
        "Acme Corp" -> ["acme.com", "acmecorp.com", "getacme.com"]
        "ABC Inc" -> ["abc.com", "abcinc.com"]
        "UK Software Ltd" -> ["uksoftware.co.uk", "uksoftware.com"]

    Args:
        company_name: Company name to generate candidates for
        max_candidates: Maximum number of candidates to generate
        org_hint: Organization type hint ('edu', 'health', 'gov', 'nonprofit')
        prefer_tlds: List of preferred TLDs

    Returns:
        List of candidate domains, ordered by likelihood
    """
    if not company_name:
        return []

    name = company_name.strip().lower()

    # Detect country hints
    has_uk = any(k in name for k in ["uk", "united kingdom", "england", "british"])
    has_de = any(k in name for k in ["germany", "deutschland", "german"])
    has_fr = any(k in name for k in ["france", "french"])

    # Generate slugs
    # Base slug: strip all special chars
    base = _SLUG.sub("", name.replace("&", "and"))

    # Common legal suffixes to strip
    legal_suffixes = {
        "inc",
        "incorporated",
        "llc",
        "ltd",
        "limited",
        "corp",
        "corporation",
        "co",
        "company",
        "gmbh",
        "sa",
        "spa",
        "bv",
        "plc",
        "ag",
    }

    # Generate variations
    tokens = [t for t in name.split() if t]
    clean_tokens = [t for t in tokens if t not in legal_suffixes]

    # Slug without legal suffixes
    clean_slug = _SLUG.sub("", " ".join(clean_tokens).replace("&", "and"))

    if org_hint is None:
        org_hint = infer_org_hint(clean_tokens)

    tld_preferences: List[str] = []
    if prefer_tlds:
        tld_preferences.extend(prefer_tlds)
    if org_hint == "edu":
        tld_preferences.extend([".edu", ".edu.mx", ".edu.au"])
    elif org_hint == "health":
        tld_preferences.extend([".org", ".com"])
    elif org_hint == "gov":
        tld_preferences.extend([".gov", ".gov.uk", ".gov.au", ".com"])
    elif org_hint == "nonprofit":
        tld_preferences.extend([".org", ".com"])

    # Fallback ordering for TLDs
    tld_preferences.extend([".com", ".org", ".net"])
    # Add country hints
    if has_uk:
        tld_preferences.insert(0, ".co.uk")
    if has_de:
        tld_preferences.insert(0, ".de")
    if has_fr:
        tld_preferences.insert(0, ".fr")

    # Ensure uniqueness and preserve order
    seen_tlds: Set[str] = set()
    tld_ordered: List[str] = []
    for tld in tld_preferences:
        if not tld.startswith("."):
            tld = f".{tld}"
        if tld not in seen_tlds:
            seen_tlds.add(tld)
            tld_ordered.append(tld)

    candidates: List[str] = []

    def _append_candidate(base_label: str, suffixes: List[str]) -> None:
        for suffix in suffixes:
            domain = f"{base_label}{suffix}"
            if domain:
                candidates.append(domain)

    primary_slug = clean_slug or base
    if primary_slug:
        _append_candidate(primary_slug, tld_ordered[: max_candidates * 2])

    # Pattern 2: Full slug (including legal suffix)
    if base != clean_slug:
        _append_candidate(base, tld_ordered[: max_candidates * 2])

    # Pattern 3: SaaS pattern "get<company>.com" (common for B2B SaaS)
    if clean_slug and len(clean_tokens) <= 2 and len(clean_slug) < 15:
        candidates.append(f"get{clean_slug}.com")

    # Pattern 4: First word only (for multi-word companies)
    if len(clean_tokens) > 1:
        first = _SLUG.sub("", clean_tokens[0])
        if first and first not in candidates:
            _append_candidate(first, tld_ordered[:max_candidates])

    # Pattern 5: Acronym (if multi-word)
    if len(clean_tokens) >= 2:
        acronym = "".join(t[0] for t in clean_tokens if t)
        if len(acronym) >= 2:
            _append_candidate(acronym, tld_ordered[:max_candidates])

    # Deduplicate and limit
    seen: Set[str] = set()
    unique_candidates = []
    for c in candidates:
        if c not in seen and c:
            seen.add(c)
            unique_candidates.append(c)
            if len(unique_candidates) >= max_candidates:
                break

    return unique_candidates


def probe_domain_dns(domain: str, timeout: float = 2.0) -> Dict[str, Any]:
    """Probe DNS for a domain and return structured information.

    Args:
        domain: Domain to probe
        timeout: DNS resolver timeout

    Returns:
        Dict containing:
            ok (bool) - whether domain resolved
            status (str) - 'ok', 'timeout', 'nxdomain', 'error'
            latency_ms (float)
            error (optional str)
    """
    result: Dict[str, Any] = {"ok": False, "status": "unavailable", "latency_ms": 0.0}
    if not HAS_DNS:
        return result

    if not domain or "." not in domain:
        result["status"] = "invalid_domain"
        return result

    start = time.time()
    try:
        resolver = dns.resolver.Resolver()
        resolver.timeout = timeout
        resolver.lifetime = timeout

        # Try A, AAAA, MX sequentially
        for record_type in ("A", "AAAA", "MX"):
            try:
                resolver.resolve(domain, record_type)
                end = time.time()
                result.update(
                    {
                        "ok": True,
                        "status": "ok",
                        "record": record_type,
                        "latency_ms": (end - start) * 1000.0,
                    }
                )
                return result
            except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
                continue

        result["status"] = "no_records"
    except dns.exception.Timeout:
        result["status"] = "timeout"
    except Exception as exc:  # broad catch to avoid crashing caller
        logger.debug(f"DNS validation error for {domain}: {exc}")
        result.update({"status": "error", "error": str(exc)})
    finally:
        result["latency_ms"] = max(
            result.get("latency_ms") or 0.0, (time.time() - start) * 1000.0
        )
    return result


def validate_domain_dns(domain: str, timeout: float = 2.0) -> bool:
    """Backwards-compatible wrapper for DNS validation.

    Args:
        domain: Domain to validate
        timeout: DNS resolver timeout

    Returns:
        True if domain resolves, False otherwise
    """
    info = probe_domain_dns(domain, timeout=timeout)
    return bool(info.get("ok"))


def find_valid_domain_candidate(
    company_name: str,
    max_candidates: int = 5,
    validate_dns: bool = True,
    dns_timeout: float = 2.0,
) -> Optional[str]:
    """Generate domain candidates and return the first one with valid DNS.

    Args:
        company_name: Company name
        max_candidates: Maximum candidates to test
        validate_dns: Whether to validate DNS (requires dnspython)
        dns_timeout: DNS timeout in seconds

    Returns:
        First valid domain candidate, or None
    """
    if not validate_dns or not HAS_DNS:
        # Just return first candidate without validation
        candidates = generate_domain_candidates(company_name, max_candidates=1)
        return candidates[0] if candidates else None

    try:
        max_candidates = int(max_candidates)
    except (TypeError, ValueError):
        max_candidates = 5
    if max_candidates <= 0:
        max_candidates = 1
    try:
        dns_timeout = float(dns_timeout)
    except (TypeError, ValueError):
        dns_timeout = 2.0
    if dns_timeout <= 0:
        dns_timeout = 0.1

    candidates = generate_domain_candidates(company_name, max_candidates=max_candidates)

    for candidate in candidates:
        info = probe_domain_dns(candidate, timeout=dns_timeout)
        if info["ok"]:
            return candidate

    return None
