"""Extract provider identifiers from known sales and marketing URLs."""

from __future__ import annotations

import re
import unicodedata
from typing import Any, Dict, Iterable, Optional, Tuple
from urllib.parse import unquote, urlparse

SF_ID_RE = re.compile(r"\b([0-9A-Za-z]{15,18})\b")
SF_PREFIX_TYPES = {
    "001": "account",
    "003": "contact",
    "00Q": "lead",
    "00T": "task",
    "005": "user",
    "006": "opportunity",
}


def _clean_identifier(segment: str) -> str:
    """Clean and normalize identifier segment."""
    segment = (segment or "").strip()
    if not segment:
        return ""
    segment = segment.split("?", 1)[0]
    segment = segment.split("#", 1)[0]
    segment = segment.strip("/")
    segment = unquote(segment)
    return segment


def _canonicalize_id(identifier: str) -> str:
    """Create safe canonical ID stripped to [a-z0-9._-]."""
    if not identifier:
        return ""

    # Percent decode
    try:
        identifier = unquote(identifier)
    except Exception:
        pass

    # NFKC normalization (canonical decomposition + compatibility composition)
    try:
        identifier = unicodedata.normalize("NFKC", identifier)
    except Exception:
        pass

    # Strip to safe characters [a-z0-9._-]
    # Convert to lowercase and keep only allowed chars
    safe_chars = []
    for char in identifier.lower():
        if char.isalnum() or char in "._-":
            safe_chars.append(char)

    return "".join(safe_chars)


def _split_segments(s: str) -> Iterable[str]:
    if not s:
        return []
    s = s.lstrip("#")
    return [unquote(part) for part in s.split("/") if part]


def _detect_linkedin(
    host: str, segments: Iterable[str]
) -> Optional[Tuple[str, str, str]]:
    if "linkedin" not in host:
        return None
    segments_list = list(segments)
    lower = [seg.lower() for seg in segments_list]
    for idx, seg in enumerate(lower):
        if seg == "in" and idx + 1 < len(segments_list):
            return "linkedin", "profile", _clean_identifier(segments_list[idx + 1])
        if seg == "company" and idx + 1 < len(segments_list):
            return "linkedin", "company", _clean_identifier(segments_list[idx + 1])
        if seg == "sales" and idx + 2 < len(segments_list) and lower[idx + 1] == "lead":
            return "linkedin", "sales_lead", _clean_identifier(segments_list[idx + 2])
    return None


def _detect_salesforce(host: str, text: str) -> Optional[Tuple[str, str, str]]:
    if "salesforce" not in host and "force.com" not in host:
        return None
    match = SF_ID_RE.search(text or "")
    if not match:
        return None
    sf_id = match.group(1).upper()
    record_type = SF_PREFIX_TYPES.get(sf_id[:3], "record")
    return "salesforce", record_type, sf_id


def _detect_hubspot(
    host: str, path_segments: Iterable[str]
) -> Optional[Tuple[str, str, str]]:
    if "hubspot.com" not in host:
        return None
    segments_list = list(path_segments)
    lower = [seg.lower() for seg in segments_list]
    for idx, seg in enumerate(lower):
        if seg == "contacts":
            # Pattern: contacts/{portalId}/view/{id}
            if idx + 3 < len(lower) and lower[idx + 2] == "view":
                return "hubspot", "contact", _clean_identifier(segments_list[idx + 3])
            # Pattern: contacts/view/{id}
            if idx + 2 < len(lower) and lower[idx + 1] == "view":
                return "hubspot", "contact", _clean_identifier(segments_list[idx + 2])
        if seg == "companies" and idx + 1 < len(lower):
            return "hubspot", "company", _clean_identifier(segments_list[idx + 1])
    return None


def _detect_apollo(
    host: str, path_segments: Iterable[str], fragment_segments: Iterable[str]
) -> Optional[Tuple[str, str, str]]:
    if "apollo.io" not in host:
        return None
    for segments_list in (list(path_segments), list(fragment_segments)):
        lower = [seg.lower() for seg in segments_list]
        for idx, seg in enumerate(lower):
            if seg == "people" and idx + 1 < len(lower):
                return "apollo", "person", _clean_identifier(segments_list[idx + 1])
            if seg == "companies" and idx + 1 < len(lower):
                return "apollo", "company", _clean_identifier(segments_list[idx + 1])
    return None


def _detect_zoominfo(
    host: str, path_segments: Iterable[str]
) -> Optional[Tuple[str, str, str]]:
    if "zoominfo.com" not in host:
        return None
    segments_list = [seg for seg in path_segments if seg]
    if len(segments_list) < 2:
        return None
    first = segments_list[0].lower()
    if first not in {"p", "c"}:
        return None
    identifier = _clean_identifier(segments_list[-1])
    if not identifier or identifier.lower() in {first, "people", "companies"}:
        return None
    record_type = "person" if first == "p" else "company"
    return "zoominfo", record_type, identifier


def _detect_crunchbase(
    host: str, path_segments: Iterable[str]
) -> Optional[Tuple[str, str, str]]:
    if "crunchbase.com" not in host:
        return None
    segments_list = list(path_segments)
    if len(segments_list) >= 2 and segments_list[0].lower() == "organization":
        identifier = _clean_identifier(segments_list[1])
        if identifier:
            return "crunchbase", "organization", identifier
    return None


DETECTORS = (
    _detect_linkedin,
    _detect_apollo,
    _detect_hubspot,
    _detect_zoominfo,
    _detect_crunchbase,
    _detect_salesforce,
)


def _parse_url(value: str) -> Tuple[str, Iterable[str], Iterable[str]]:
    parsed = urlparse(value)
    host = parsed.netloc.lower()
    if host.startswith("www."):
        host = host[4:]
    return host, _split_segments(parsed.path), _split_segments(parsed.fragment)


def _extract_id(raw: Optional[str]) -> Tuple[str, str, str]:
    if not raw or not str(raw).strip():
        return "", "", ""
    text = str(raw).strip()

    candidates = [text]
    if not re.match(r"^[a-z]+://", text, re.IGNORECASE):
        candidates.append("https://" + text.lstrip("/"))

    for candidate in candidates:
        try:
            host, path_segments, fragment_segments = _parse_url(candidate)
        except ValueError:
            continue

        for detector in DETECTORS:
            if detector is _detect_salesforce:
                match = detector(host, candidate)
            elif detector is _detect_apollo:
                match = detector(host, path_segments, fragment_segments)
            else:
                match = detector(host, path_segments)
            if match:
                provider, record_type, identifier = match
                if identifier:
                    return provider, record_type, identifier

    sf_match = _detect_salesforce("salesforce.com", text)
    if sf_match:
        return sf_match

    return "", "", ""


def extract_id_from_url(value: str | None) -> Dict[str, Any]:
    """Extract provider, type, and identifier from sales/marketing URLs.

    Returns canonical safe IDs without HTML escaping.
    UI layer should handle escaping for display.
    """
    provider, record_type, identifier = _extract_id(value)

    if not provider:
        return {
            "raw_id": "",
            "id": "",
            "provider": "",
            "type": "",
            "confidence": 0.0,
            "meta": {"reason": "unrecognized", "input": str(value) if value else ""},
        }

    # Create canonical ID
    canonical_id = _canonicalize_id(identifier)

    # Calculate confidence based on how much normalization was needed
    confidence = 1.0
    if canonical_id != identifier:
        # Reduced confidence if significant normalization was needed
        if len(canonical_id) < len(identifier) * 0.8:
            confidence = 0.7
        else:
            confidence = 0.9

    return {
        "raw_id": identifier,  # Original extracted ID
        "id": canonical_id,  # Safe canonical ID
        "provider": provider,
        "type": record_type,
        "confidence": confidence,
        "value": [provider, record_type, canonical_id],  # For backward compatibility
        "meta": {
            "provider": provider,
            "type": record_type,
            "raw_id": identifier,
            "canonical_id": canonical_id,
            "source": str(value) if value else "",
            "normalization_applied": canonical_id != identifier,
        },
    }


# Golden test vectors for validation
GOLDEN_VECTORS = [
    # LinkedIn profiles
    (
        "https://www.linkedin.com/in/john-doe-123",
        {"provider": "linkedin", "type": "profile", "id": "johndoe123"},
    ),
    (
        "linkedin.com/in/jane%20smith",
        {"provider": "linkedin", "type": "profile", "id": "janesmith"},
    ),
    # Salesforce IDs
    (
        "https://na1.salesforce.com/003D000004TmiQn",
        {"provider": "salesforce", "type": "contact", "id": "003d000004tmiqn"},
    ),
    (
        "force.com/001D000000AbCdE",
        {"provider": "salesforce", "type": "account", "id": "001d000000abcde"},
    ),
    # HubSpot
    (
        "https://app.hubspot.com/contacts/123/view/456",
        {"provider": "hubspot", "type": "contact", "id": "456"},
    ),
    # Malicious input tests
    (
        "https://evil.com/<script>alert('xss')</script>",
        {"provider": "", "type": "", "id": ""},
    ),
    ("../../../etc/passwd", {"provider": "", "type": "", "id": ""}),
    ("javascript:alert(1)", {"provider": "", "type": "", "id": ""}),
]


def validate_golden_vectors():
    """Validate implementation against golden test vectors."""
    failures = []
    for input_url, expected in GOLDEN_VECTORS:
        result = extract_id_from_url(input_url)
        if (
            result["provider"] != expected["provider"]
            or result["type"] != expected["type"]
            or result["id"] != expected["id"]
        ):
            failures.append(
                {
                    "input": input_url,
                    "expected": expected,
                    "actual": {
                        "provider": result["provider"],
                        "type": result["type"],
                        "id": result["id"],
                    },
                }
            )
    return failures
