"""Candidate search helpers for the promotion pipeline."""

from __future__ import annotations

import json
import logging
import os
import re
from difflib import SequenceMatcher
from typing import Dict, Iterable, List, Optional, Tuple

import duckdb

# Default DuckDB path, override with WIKIDATA_INDEX_PATH env var
# Use the combined (SEC+GLEIF-enriched) DB by default to ensure authority signals fire.
DEFAULT_DUCKDB_PATH = "config/foundrygraph.duckdb"

from .promotion_models import CandidateMatch, PromotionTargetRecord

logger = logging.getLogger(__name__)

try:  # pragma: no cover - optional dependency
    from rapidfuzz import fuzz as rapidfuzz_fuzz
except Exception:  # pragma: no cover
    rapidfuzz_fuzz = None

try:  # pragma: no cover - normalization helper
    from fmatch.utils_normalize import normalize_company_name as _normalize_company_name
except Exception:  # pragma: no cover

    def _normalize_company_name(value: str) -> str:
        text = value.lower()
        text = re.sub(r"[^\w\s]", " ", text)
        text = re.sub(r"\s+", " ", text).strip()
        return text


def _fuzzy_score(a: str, b: str) -> float:
    if not a or not b:
        return 0.0
    if rapidfuzz_fuzz is not None:
        return float(rapidfuzz_fuzz.token_sort_ratio(a, b))
    return SequenceMatcher(None, a, b).ratio() * 100.0


def _normalize_domain(domain: Optional[str]) -> Optional[str]:
    if not domain:
        return None
    text = domain.strip().lower()
    if text.startswith("http://"):
        text = text[7:]
    elif text.startswith("https://"):
        text = text[8:]
    if text.startswith("www."):
        text = text[4:]
    return text.rstrip("/") or None


def _parse_json_field(value: Optional[str]) -> Dict[str, object]:
    if not value:
        return {}
    try:
        parsed = json.loads(value)
        if isinstance(parsed, dict):
            return parsed
    except Exception as exc:  # pragma: no cover
        logger.debug("Failed to parse JSON field: %s", exc)
    return {}


class CandidateSearcher:
    """Queries the Wikidata DuckDB snapshot for likely company matches.

    Supports SEC+GLEIF enriched databases with company_profiles_gleif table.
    Configure DB path via WIKIDATA_INDEX_PATH env var.
    """

    def __init__(self, duckdb_path: str = None) -> None:
        self.duckdb_path = duckdb_path or os.getenv("WIKIDATA_INDEX_PATH", DEFAULT_DUCKDB_PATH)
        self.name_match_threshold = 95.0  # Raised from 82 to reduce false positives
        self._has_gleif_table: Optional[bool] = None
        self._schema_checked = False
        self._is_enriched = False

    def close(self) -> None:
        """Compatibility hook (no persistent connections)."""
        return

    def _connect(self, *, read_only: bool = True) -> duckdb.DuckDBPyConnection:
        conn = duckdb.connect(self.duckdb_path, read_only=read_only)
        if not self._schema_checked:
            self._schema_checked = True
            try:
                rows = conn.execute(
                    """
                    SELECT lower(table_name) AS name
                    FROM information_schema.tables
                    WHERE table_name IN ('sec_enrichment', 'company_profiles_gleif')
                    """
                ).fetchall()
                names = {row[0] for row in rows}
                self._is_enriched = "sec_enrichment" in names and "company_profiles_gleif" in names
                if not self._is_enriched:
                    logger.warning(
                        "Wikidata DB at %s is missing SEC/GLEIF tables; promotion signals may be incomplete",
                        self.duckdb_path,
                    )
            except Exception as exc:  # pragma: no cover - defensive
                logger.warning(
                    "Could not validate Wikidata DB schema for %s: %s",
                    self.duckdb_path,
                    exc,
                )
        return conn

    def _check_gleif_table(self, conn: duckdb.DuckDBPyConnection) -> bool:
        """Check if company_profiles_gleif table exists (GLEIF-enriched DB)."""
        if self._has_gleif_table is None:
            try:
                result = conn.execute(
                    "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'company_profiles_gleif'"
                ).fetchone()
                self._has_gleif_table = result and result[0] > 0
            except Exception:
                self._has_gleif_table = False
        return self._has_gleif_table

    def _get_gleif_info(
        self, conn: duckdb.DuckDBPyConnection, wikidata_id: str
    ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
        """Fetch LEI and GLEIF parent info for a wikidata_id.

        Returns: (lei, gleif_parent_lei, gleif_parent_wikidata_id, gleif_relationship_type)
        """
        if not self._check_gleif_table(conn):
            # Fall back to company_profiles.lei if no GLEIF table
            try:
                row = conn.execute(
                    "SELECT lei FROM company_profiles WHERE wikidata_id = ?",
                    [wikidata_id],
                ).fetchone()
                lei = row[0] if row and row[0] else None
                return (lei, None, None, None)
            except Exception:
                return (None, None, None, None)

        try:
            row = conn.execute(
                """
                SELECT lei, gleif_parent_lei, gleif_parent_wikidata_id, gleif_relationship_type
                FROM company_profiles_gleif
                WHERE wikidata_id = ?
                """,
                [wikidata_id],
            ).fetchone()
            if row:
                return (row[0], row[1], row[2], row[3])
        except Exception as exc:
            logger.debug("Failed to fetch GLEIF info for %s: %s", wikidata_id, exc)
        return (None, None, None, None)

    def search(self, target: PromotionTargetRecord) -> Optional[CandidateMatch]:
        """Look up best candidate using domain first, then ticker."""
        domain = _normalize_domain(target.domain_claimed)
        if domain:
            candidate = self._search_by_domain(domain)
            if candidate:
                return candidate

        ticker = (target.ticker or "").strip().upper()
        if ticker:
            return self._search_by_ticker(ticker)

        name_candidate = self._search_by_name(target)
        if name_candidate:
            return name_candidate

        return None

    def _search_by_domain(self, domain: str) -> Optional[CandidateMatch]:
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT cd.wikidata_id,
                       cd.domain,
                       cd.confidence AS domain_confidence,
                       cp.label,
                       cp.countries,
                       cp.revenue,
                       cp.employees,
                       cp.last_updated
                FROM company_domains cd
                LEFT JOIN company_profiles cp USING (wikidata_id)
                WHERE cd.domain = ?
                ORDER BY cd.confidence DESC NULLS LAST, cp.last_updated DESC NULLS LAST
                LIMIT 1
                """,
                [domain],
            ).fetchone()
            if not row:
                return None
            return self._hydrate_candidate(conn, row)

    def _search_by_ticker(self, ticker: str) -> Optional[CandidateMatch]:
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT ce.wikidata_id,
                       cd.domain,
                       cd.confidence AS domain_confidence,
                       cp.label,
                       cp.countries,
                       cp.revenue,
                       cp.employees,
                       cp.last_updated
                FROM company_external_ids ce
                LEFT JOIN company_domains cd USING (wikidata_id)
                LEFT JOIN company_profiles cp USING (wikidata_id)
                WHERE ce.id_type = 'ticker'
                  AND upper(ce.id_value) = upper(?)
                ORDER BY cd.confidence DESC NULLS LAST, cp.last_updated DESC NULLS LAST
                LIMIT 1
                """,
                [ticker],
            ).fetchone()
            if not row:
                return None
            return self._hydrate_candidate(conn, row)

    def _hydrate_candidate_by_id(self, wikidata_id: str) -> Optional[CandidateMatch]:
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT cp.wikidata_id,
                       cd.domain,
                       cd.confidence AS domain_confidence,
                       cp.label,
                       cp.countries,
                       cp.revenue,
                       cp.employees,
                       cp.last_updated
                FROM company_profiles cp
                LEFT JOIN company_domains cd USING (wikidata_id)
                WHERE cp.wikidata_id = ?
                ORDER BY cd.confidence DESC NULLS LAST, cp.last_updated DESC NULLS LAST
                LIMIT 1
                """,
                [wikidata_id],
            ).fetchone()
            if not row:
                return None
            return self._hydrate_candidate(conn, row)

    def _search_by_name(
        self, target: PromotionTargetRecord
    ) -> Optional[CandidateMatch]:
        company_name = (target.company_name or "").strip()
        if not company_name:
            return None

        normalized = _normalize_company_name(company_name)
        if not normalized:
            return None

        tokens = [tok for tok in normalized.split() if len(tok) > 2][:4]
        if not tokens:
            tokens = normalized.split()[:2]
        if not tokens:
            tokens = [normalized]

        like_patterns = [f"%{tok}%" for tok in tokens]

        def _fetch_candidates(
            conn: duckdb.DuckDBPyConnection, patterns: List[str], use_all: bool
        ) -> List[tuple]:
            if not patterns:
                return []
            params: List[str] = []
            if use_all:
                where_clause = " AND ".join("lower(cp.label) LIKE ?" for _ in patterns)
                params.extend(patterns)
            else:
                where_clause = " OR ".join("lower(cp.label) LIKE ?" for _ in patterns)
                params.extend(patterns)

            query = f"""
                SELECT DISTINCT cp.wikidata_id, cp.label
                FROM company_profiles cp
                WHERE cp.label IS NOT NULL AND ({where_clause})
                LIMIT 50
            """
            return conn.execute(query, params).fetchall()

        with self._connect() as conn:
            rows = _fetch_candidates(conn, like_patterns[:2], use_all=True)
            if not rows:
                rows = _fetch_candidates(conn, like_patterns, use_all=False)
            if not rows:
                return None

            best_score = 0.0
            best_wikidata_id: Optional[str] = None

            for wikidata_id, label in rows:
                score = _fuzzy_score(company_name, label or "")
                if score > best_score:
                    best_score = score
                    best_wikidata_id = wikidata_id

            if best_wikidata_id is None or best_score < self.name_match_threshold:
                logger.debug(
                    "Name-based search below threshold (score=%.2f) for %s",
                    best_score,
                    company_name,
                )
                return None

            logger.debug(
                "Name-based match %s -> %s (score=%.2f)",
                company_name,
                best_wikidata_id,
                best_score,
            )
        return self._hydrate_candidate_by_id(best_wikidata_id)

    def _hydrate_candidate(
        self, conn: duckdb.DuckDBPyConnection, row: Iterable
    ) -> CandidateMatch:
        (
            wikidata_id,
            domain,
            _domain_confidence,
            label,
            countries_json,
            revenue_value,
            employees_value,
            last_updated,
        ) = row

        tickers = [
            r[0]
            for r in conn.execute(
                """
                SELECT DISTINCT upper(id_value)
                FROM company_external_ids
                WHERE wikidata_id = ?
                  AND id_type = 'ticker'
            """,
                [wikidata_id],
            ).fetchall()
        ]

        authority_sources = [
            r[0]
            for r in conn.execute(
                """
                SELECT DISTINCT source
                FROM company_external_ids
                WHERE wikidata_id = ?
                  AND source IN ('sec_edgar', 'companies_house')
            """,
                [wikidata_id],
            ).fetchall()
        ]

        all_domains = [
            r[0]
            for r in conn.execute(
                """
                SELECT DISTINCT domain
                FROM company_domains
                WHERE wikidata_id = ?
            """,
                [wikidata_id],
            ).fetchall()
        ]

        # Fetch GLEIF enrichment data
        lei, gleif_parent_lei, gleif_parent_wikidata_id, gleif_relationship_type = (
            self._get_gleif_info(conn, wikidata_id)
        )

        profile_payload: Dict[str, object] = {
            "label": label,
            "countries": countries_json,
            "revenue": revenue_value,
            "employees": employees_value,
            "last_refreshed": last_updated.isoformat() if last_updated else None,
            "domains": all_domains,
            "tickers": tickers,
            "authority_sources": authority_sources,
            "lei": lei,
            "gleif_parent_lei": gleif_parent_lei,
            "gleif_parent_wikidata_id": gleif_parent_wikidata_id,
            "gleif_relationship_type": gleif_relationship_type,
        }

        countries = []
        if countries_json:
            try:
                parsed = json.loads(countries_json) if isinstance(countries_json, str) else countries_json
                if isinstance(parsed, list):
                    for item in parsed:
                        if isinstance(item, dict):
                            countries.append({"id": item.get("id"), "label": item.get("label")})
                        elif isinstance(item, str) and item.startswith("Q"):
                            # Handle bare Wikidata IDs like "Q30" for United States
                            countries.append({"id": item, "label": None})
            except Exception as exc:  # pragma: no cover
                logger.debug("Failed to parse countries JSON: %s", exc)

        # Revenue and employees are now direct values in the schema
        revenue = float(revenue_value) if revenue_value is not None else None
        employees = int(employees_value) if employees_value is not None else None

        return CandidateMatch(
            wikidata_id=wikidata_id,
            label=label,
            domain=domain,
            all_domains=all_domains,
            tickers=tickers,
            authority_sources=authority_sources,
            countries=countries,
            revenue=revenue,
            employees=employees,
            last_refreshed=last_updated,
            source="wikidata",
            raw_profile=profile_payload,
            lei=lei,
            gleif_parent_lei=gleif_parent_lei,
            gleif_parent_wikidata_id=gleif_parent_wikidata_id,
            gleif_relationship_type=gleif_relationship_type,
        )
