"""LinkedIn parameter enrichment — resolve LLM-generated names to LinkedIn codes.

Calls the Unipile search/parameters API to map ICP field values
(e.g., "Information Technology") to LinkedIn's internal codes.
Then filters results using heuristic Jaccard similarity (no LLM dependency).

Ported from the original AI in Charge IcpParamRetriever.
"""

from __future__ import annotations

import logging
import re
from typing import Any

from .icp_schemas import (
    EnrichedField,
    EnrichedIcpParams,
    EnrichedParam,
    SingleIcp,
)

logger = logging.getLogger(__name__)

# Field name → Unipile search parameter type
_FIELD_TO_TYPE: dict[str, str] = {
    "job_titles": "JOB_TITLE",
    "industries": "INDUSTRY",
    "locations": "LOCATION",
    "company_locations": "LOCATION",
    "departments": "DEPARTMENT",
}

# Minimum Jaccard similarity for fuzzy matching
_JACCARD_THRESHOLD = 0.7

# Location aliases for common variants
_LOCATION_ALIASES: dict[str, set[str]] = {
    "united states": {"united states", "united states of america", "usa", "us", "u s a", "u s"},
    "united kingdom": {"united kingdom", "uk", "great britain", "gb"},
}


async def enrich_icp_linkedin_params(
    icp: SingleIcp,
    get_params_fn: Any,
) -> EnrichedIcpParams:
    """Resolve ICP field values to LinkedIn search parameter codes.

    Args:
        icp: A SingleIcp with LLM-generated field values.
        get_params_fn: Async callable(type, keywords) -> list[{name, code}]
            Typically bound to BackendClient.get_search_params or
            UnipileClient equivalent.

    Returns:
        EnrichedIcpParams with validated LinkedIn codes.
    """
    result = EnrichedIcpParams()

    # Industries
    if icp.industries and icp.industries.include:
        result.industries = await _enrich_field(
            "industries", icp.industries.include, icp.industries.exclude, get_params_fn,
        )

    # Job titles
    if icp.job_titles and icp.job_titles.include:
        result.job_titles = await _enrich_field(
            "job_titles", icp.job_titles.include, icp.job_titles.exclude, get_params_fn,
        )

    # Locations
    if icp.locations and icp.locations.include:
        result.locations = await _enrich_field(
            "locations", icp.locations.include, icp.locations.exclude, get_params_fn,
        )

    # Company locations
    if icp.company_locations and icp.company_locations.include:
        result.company_locations = await _enrich_field(
            "company_locations", icp.company_locations.include,
            icp.company_locations.exclude, get_params_fn,
        )

    # Departments
    if icp.departments and icp.departments.include:
        result.departments = await _enrich_field(
            "departments", icp.departments.include, icp.departments.exclude, get_params_fn,
        )

    return result


async def _enrich_field(
    field_name: str,
    include_values: list[str],
    exclude_values: list[str],
    get_params_fn: Any,
) -> EnrichedField:
    """Enrich a single include/exclude field."""
    param_type = _FIELD_TO_TYPE.get(field_name)
    if not param_type:
        return EnrichedField()

    include_params = await _resolve_values(param_type, include_values, field_name, get_params_fn)
    exclude_params = await _resolve_values(param_type, exclude_values, field_name, get_params_fn)

    # Remove overlaps: exclude wins
    exclude_codes = {p.code for p in exclude_params}
    include_params = [p for p in include_params if p.code not in exclude_codes]

    return EnrichedField(include=include_params, exclude=exclude_params)


async def _resolve_values(
    param_type: str,
    values: list[str],
    field_name: str,
    get_params_fn: Any,
) -> list[EnrichedParam]:
    """Resolve a list of string values to EnrichedParam objects."""
    all_params: list[EnrichedParam] = []
    seen_codes: set[str] = set()

    for value in values:
        try:
            candidates = await get_params_fn(type=param_type, keywords=value)
        except Exception as e:
            logger.warning(f"Search params lookup failed for {param_type}/{value}: {e}")
            continue

        if not candidates:
            continue

        # Filter candidates using heuristic matching
        filtered = _filter_candidates(value, candidates, field_name)

        for p in filtered:
            code = p.get("code", "")
            if code and code not in seen_codes:
                seen_codes.add(code)
                all_params.append(EnrichedParam(name=p.get("name", ""), code=code))

    return all_params


def _filter_candidates(
    original: str,
    candidates: list[dict[str, str]],
    field_name: str,
) -> list[dict[str, str]]:
    """Filter Unipile search parameter candidates using heuristic matching.

    Uses exact match → alias match → Jaccard similarity fallback.
    """
    if not candidates:
        return []

    # Validate structure first
    valid = _validate_candidates(candidates)
    if not valid:
        return []

    o_norm = _norm(original)

    # 1. Exact match (including aliases)
    aliases = _get_aliases(o_norm)
    exact = [c for c in valid if _norm(c["name"]) in aliases]
    if exact:
        return _dedupe_by_code(exact)

    # 2. For locations, only accept exact matches
    if field_name in ("locations", "company_locations"):
        return _dedupe_by_code(
            [c for c in valid if _norm(c["name"]) == o_norm]
        )

    # 3. Jaccard similarity for other fields
    if field_name in ("job_titles", "industries", "departments"):
        o_tokens = _tokens(o_norm)
        scored: list[tuple[float, dict[str, str]]] = []
        for c in valid:
            c_tokens = _tokens(_norm(c["name"]))
            jacc = _jaccard(o_tokens, c_tokens)
            if jacc >= _JACCARD_THRESHOLD:
                scored.append((jacc, c))
        scored.sort(key=lambda x: x[0], reverse=True)
        return _dedupe_by_code([c for _, c in scored])

    return []


# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────

def _validate_candidates(candidates: list[dict]) -> list[dict[str, str]]:
    """Validate candidate structure, returning only well-formed entries."""
    valid: list[dict[str, str]] = []
    seen: set[str] = set()
    for c in candidates:
        if not isinstance(c, dict):
            continue
        name = c.get("name")
        code = c.get("code")
        if not isinstance(name, str) or not name.strip():
            continue
        if not isinstance(code, str) or not code.strip():
            continue
        code = code.strip()
        if code in seen:
            continue
        seen.add(code)
        valid.append({"name": name.strip(), "code": code})
    return valid


def _dedupe_by_code(items: list[dict[str, str]]) -> list[dict[str, str]]:
    """Deduplicate by code, preserving order."""
    seen: set[str] = set()
    out: list[dict[str, str]] = []
    for c in items:
        if c["code"] in seen:
            continue
        seen.add(c["code"])
        out.append(c)
    return out


def _norm(s: str) -> str:
    """Normalize text for comparison."""
    return re.sub(r"[^a-z0-9 ]+", "", s.lower()).strip()


def _tokens(s: str) -> set[str]:
    """Split normalized text into token set."""
    return {t for t in s.split() if t}


def _jaccard(a: set[str], b: set[str]) -> float:
    """Jaccard similarity between two token sets."""
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)


def _get_aliases(norm: str) -> set[str]:
    """Get known aliases for a normalized string."""
    for canonical, alias_set in _LOCATION_ALIASES.items():
        if norm in alias_set:
            return alias_set
    return {norm}
