"""Business ID backfill via multi-pass CT SoS name/address matching.

Scans leads without business_id and attempts to find their CT SoS record
through escalating passes: address match → normalized name → token fuzzy.
"""

import re
from datetime import datetime
from typing import Dict, List, Optional

from integrations.ctsos import search_filings_by_address, search_filings_by_name
from state.backlog import get_lead, load_backlog, update_lead

ENTITY_SUFFIXES = [
    "llc", "pllc", "l.l.c.", "p.l.l.c.",
    "inc", "inc.", "corp", "corp.", "corporation",
    "ltd", "ltd.", "limited",
    "pc", "p.c.", "pa", "p.a.",
    "llp", "l.p.", "plc",
    "professional limited liability company",
    "limited liability company",
]

MIN_NAME_TOKEN_SCORE = 0.6


def _normalize_for_matching(name: str) -> str:
    """Normalize a business name for matching.

    Strips entity suffixes, punctuation, collapses whitespace, lowercases.
    """
    s = name.strip().lower()

    # Strip entity suffixes (with optional leading comma/space)
    for suffix in sorted(ENTITY_SUFFIXES, key=len, reverse=True):
        s = re.sub(rf',?\s*{re.escape(suffix)}\.?$', '', s)

    # Strip remaining punctuation (keep letters, digits, spaces)
    s = re.sub(r'[^a-z0-9\s]', '', s)

    # Collapse whitespace
    s = re.sub(r'\s+', ' ', s).strip()

    return s


def _tokenize(name: str) -> set:
    """Extract significant tokens from a business name."""
    tokens = set()
    skip_words = {"the", "of", "and", "for", "in", "at", "to", "a", "an", "by", "on", "its", "&"}
    for t in name.strip().lower().split():
        t = t.strip(".,;:()'\"-")
        if t and t not in skip_words and len(t) > 1:
            tokens.add(t)
    return tokens


def _score_name_tokens(name_a: str, name_b: str) -> float:
    """Compute token overlap score between two business names (0-1)."""
    tokens_a = _tokenize(name_a)
    tokens_b = _tokenize(name_b)

    if not tokens_a or not tokens_b:
        return 0.0

    intersection = tokens_a & tokens_b
    # Jaccard-like: intersection / max(len(a), len(b))
    # Heavily penalizes when one name has many extra tokens
    score = len(intersection) / max(len(tokens_a), len(tokens_b))

    return score


def _pass_address(lead: dict) -> Optional[str]:
    """Pass 1: match by billingstreet + billingcity.

    Returns business_id if exactly 1 match found, None otherwise.
    """
    street = lead.get("street_address", "")
    city = lead.get("city", "")
    if not street or not city:
        return None

    results = search_filings_by_address(street, city)
    if len(results) == 1:
        return results[0].get("id", "")
    if len(results) > 1:
        multi = [r for r in results if r.get("status", "").lower() == "active"]
        if len(multi) == 1:
            return multi[0].get("id", "")
    return None


def _pass_name_normalized(lead: dict, candidates: Optional[List[dict]] = None) -> Optional[str]:
    """Pass 2: match by normalized business name.

    If candidates provided (from Pass 1 address match that returned
    multiple results), match against those. Otherwise search by name.
    """
    name = lead.get("name", "")
    city = lead.get("city", "")
    if not name:
        return None

    lead_normalized = _normalize_for_matching(name)

    if candidates is None:
        results = search_filings_by_name(name, city)
    else:
        results = candidates

    for r in results:
        socrata_name = r.get("name", "")
        if not socrata_name:
            continue
        socrata_normalized = _normalize_for_matching(socrata_name)
        if lead_normalized == socrata_normalized:
            return r.get("id", "")

    return None


def _pass_token_fuzzy(lead: dict) -> Optional[str]:
    """Pass 3: token-based fuzzy match using contains(name) + city filter."""
    name = lead.get("name", "")
    city = lead.get("city", "")
    if not name:
        return None

    results = search_filings_by_name(name, city)
    if not results:
        return None

    best_id = None
    best_score = 0.0
    for r in results:
        socrata_name = r.get("name", "")
        if not socrata_name:
            continue
        score = _score_name_tokens(name, socrata_name)
        if score > best_score:
            best_score = score
            best_id = r.get("id", "")

    if best_id and best_score >= MIN_NAME_TOKEN_SCORE:
        return best_id
    return None


def backfill_business_ids(lead_ids: List[str], dry_run: bool = False) -> Dict:
    """Multi-pass business_id backfill for leads missing it.

    Pass 1: Match by street address + city (primary, strongest signal)
    Pass 2: Match by normalized business name (catches multi-tenant buildings)
    Pass 3: Token-based fuzzy match (fallback for remaining edge cases)

    Args:
        lead_ids: List of lead UUIDs to process
        dry_run: If True, print what would be done without writing

    Returns:
        Dict with counts per pass and any errors.
    """
    result = {
        "scanned": len(lead_ids),
        "already_had_id": 0,
        "pass1_address": 0,
        "pass2_name_normalized": 0,
        "pass3_token_fuzzy": 0,
        "failed": 0,
        "errors": [],
        "details": [],
    }

    for lead_id in lead_ids:
        try:
            lead = get_lead(lead_id)
            if not lead:
                result["errors"].append(f"Lead {lead_id} not found")
                result["failed"] += 1
                continue

            if lead.get("business_id"):
                result["already_had_id"] += 1
                continue

            business_id = None
            matched_by = ""

            # Pass 1: Address match
            business_id = _pass_address(lead)
            if business_id:
                matched_by = "address"
                result["pass1_address"] += 1

            # Pass 2: Normalized name match
            if not business_id:
                candidates = None
                # If address matched multiple, use those as candidate pool
                street = lead.get("street_address", "")
                city = lead.get("city", "")
                if street and city:
                    candidates = search_filings_by_address(street, city)
                business_id = _pass_name_normalized(lead, candidates)
                if business_id:
                    matched_by = "name_normalized"
                    result["pass2_name_normalized"] += 1

            # Pass 3: Token fuzzy
            if not business_id:
                business_id = _pass_token_fuzzy(lead)
                if business_id:
                    matched_by = "token_fuzzy"
                    result["pass3_token_fuzzy"] += 1

            if business_id:
                if not dry_run:
                    update_lead(lead_id, {
                        "business_id": business_id,
                        "last_updated": datetime.utcnow().isoformat() + "Z",
                    })
                result["details"].append({
                    "lead_id": lead_id,
                    "name": lead.get("name", ""),
                    "business_id": business_id,
                    "matched_by": matched_by,
                })
            else:
                result["failed"] += 1
                result["details"].append({
                    "lead_id": lead_id,
                    "name": lead.get("name", ""),
                    "business_id": None,
                    "matched_by": "FAILED",
                })

        except Exception as e:
            result["errors"].append(f"{lead_id}: {str(e)}")
            result["failed"] += 1

    # Summary stats
    result["total_found"] = (
        result["pass1_address"]
        + result["pass2_name_normalized"]
        + result["pass3_token_fuzzy"]
    )

    return result
