"""Deterministic scoring for promotion candidates."""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Dict, Iterable, Optional, Set

from .promotion_models import CandidateMatch, PromotionTargetRecord
from .promotion_service import PromotionEvidencePayload

logger = logging.getLogger(__name__)


def _truthy(value: object) -> bool:
    if isinstance(value, str):
        return bool(value.strip())
    return bool(value)


COUNTRY_LOOKUP: Dict[str, Set[str]] = {
    "US": {"united states", "usa", "united states of america", "america"},
    "GB": {"united kingdom", "uk", "great britain", "england"},
    "CA": {"canada"},
    "DE": {"germany"},
    "FR": {"france"},
    "JP": {"japan"},
    "CN": {"china", "people's republic of china", "peopleâ€™s republic of china"},
    "IN": {"india"},
    "KR": {"south korea", "korea, republic of"},
    "AU": {"australia"},
    "ES": {"spain"},
    "IT": {"italy"},
    "NL": {"netherlands"},
    "SE": {"sweden"},
    "CH": {"switzerland"},
    "BR": {"brazil"},
    "MX": {"mexico"},
    "IE": {"ireland"},
    "SG": {"singapore"},
    "HK": {"hong kong"},
}

COUNTRY_WIKIDATA_IDS: Dict[str, str] = {
    "Q30": "US",  # United States of America
    "Q145": "GB",  # United Kingdom
    "Q16": "CA",  # Canada
    "Q183": "DE",  # Germany
    "Q142": "FR",  # France
    "Q17": "JP",  # Japan
    "Q148": "CN",  # China
    "Q668": "IN",  # India
    "Q884": "KR",  # South Korea
    "Q408": "AU",  # Australia
    "Q29": "ES",  # Spain
    "Q38": "IT",  # Italy
    "Q55": "NL",  # Netherlands
    "Q34": "SE",  # Sweden
    "Q39": "CH",  # Switzerland
    "Q155": "BR",  # Brazil
    "Q96": "MX",  # Mexico
    "Q27": "IE",  # Ireland
    "Q334": "SG",  # Singapore
    "Q8646": "HK",  # Hong Kong
}


class PromotionPolicy:
    """Compute signals and scores for promotion matches."""

    def __init__(
        self,
        *,
        auto_threshold: float = 0.75,
        freshness_limit_days: int = 365,
        weights: Optional[Dict[str, float]] = None,
    ) -> None:
        self.auto_threshold = float(auto_threshold)
        self.freshness_limit_days = int(freshness_limit_days)
        self.weights = weights or {
            "domain": 0.4,
            "ticker": 0.2,
            "authority": 0.2,
            "country": 0.2,
        }
        self.weight_sum = sum(self.weights.values()) or 1.0

    def evaluate(
        self,
        target: PromotionTargetRecord,
        candidate: Optional[CandidateMatch],
        *,
        existing_family: Optional[str] = None,
        domain_confidence: float = 0.0,
        engine_payload: Optional[Dict[str, object]] = None,
    ) -> PromotionEvidencePayload:
        signals: Dict[str, object] = {}
        components: Dict[str, float] = {}
        conflicts: Dict[str, object] = {}
        payload = engine_payload or {}
        payload_candidate = (
            payload.get("candidate")
            if isinstance(payload.get("candidate"), dict)
            else {}
        )

        candidate_domain = candidate.domain if candidate else None
        domain_verified = False
        if candidate_domain:
            if target.domain_claimed and not self._domains_match(
                target.domain_claimed, candidate_domain
            ):
                conflicts["domain_mismatch"] = {
                    "target": target.domain_claimed,
                    "candidate": candidate_domain,
                }
            else:
                domain_verified = True
        else:
            conflicts["missing_domain"] = True
        signals["domain_verified"] = domain_verified
        components["domain"] = 1.0 if domain_verified else 0.0

        ticker_match = bool(
            target.ticker
            and candidate
            and target.ticker.strip().upper()
            in {t.strip().upper() for t in candidate.tickers}
        )
        signals["ticker_match"] = ticker_match
        components["ticker"] = 1.0 if ticker_match else 0.0

        authority_sources_present = bool(candidate and candidate.authority_sources)
        has_lei = bool(candidate and candidate.lei)
        gleif_parent_lei = candidate.gleif_parent_lei if candidate else None
        gleif_relationship_type = candidate.gleif_relationship_type if candidate else None
        sec_cik = _truthy(
            payload.get("cik")
            or (
                payload_candidate.get("cik")
                if isinstance(payload_candidate, dict)
                else None
            )
        )
        sec_ticker = _truthy(
            payload.get("ticker")
            or (
                payload_candidate.get("ticker")
                if isinstance(payload_candidate, dict)
                else None
            )
        )
        companies_house = _truthy(
            payload.get("companies_house_number")
            or (
                payload_candidate.get("companies_house_number")
                if isinstance(payload_candidate, dict)
                else None
            )
        )
        qid_cik_crosswalk = bool(payload.get("qid_to_cik_match"))

        strength = 0.0
        if sec_cik:
            strength += 0.6
        if sec_ticker or authority_sources_present:
            strength += 0.3
        if companies_house:
            strength += 0.2
        if qid_cik_crosswalk:
            strength += 0.1
        if has_lei:
            strength += 0.4  # LEI is a strong authority signal

        # Penalize subsidiaries (we want to promote the parent, not the child)
        is_subsidiary = bool(gleif_parent_lei and gleif_relationship_type)
        if is_subsidiary:
            conflicts["gleif_is_subsidiary"] = {
                "parent_lei": gleif_parent_lei,
                "relationship_type": gleif_relationship_type,
                "parent_wikidata_id": candidate.gleif_parent_wikidata_id if candidate else None,
            }
            strength *= 0.5  # Reduce authority score for subsidiaries

        last_updated = self._authority_last_updated(payload, payload_candidate)
        if last_updated is not None:
            age_days = (datetime.now(tz=timezone.utc) - last_updated).days
            if age_days > 365:
                strength *= 0.7

        authority_score = round(min(strength, 1.0), 2)
        authority_match = authority_score >= 0.6
        signals["authority_match"] = authority_match
        signals["authority_score"] = authority_score
        signals["authority_via_cik"] = sec_cik
        signals["authority_via_ticker"] = sec_ticker or authority_sources_present
        signals["authority_via_companies_house"] = companies_house
        signals["authority_via_qid_crosswalk"] = qid_cik_crosswalk
        signals["authority_via_lei"] = has_lei
        signals["gleif_is_subsidiary"] = is_subsidiary
        signals["gleif_parent_lei"] = gleif_parent_lei
        components["authority"] = min(authority_score, 1.0)

        country_aligned = self._country_aligned(
            target.country_code,
            candidate.countries if candidate else [],
        )
        signals["country_aligned"] = country_aligned
        components["country"] = 1.0 if country_aligned else 0.0

        revenue_sane = self._ratio_ok(
            target.rev_usd, candidate.revenue if candidate else None
        )
        signals["revenue_sane"] = revenue_sane

        employees_sane = self._ratio_ok(
            target.employees,
            candidate.employees if candidate else None,
            lower=0.2,
            upper=5.0,
        )
        signals["employees_sane"] = employees_sane

        freshness_days = self._freshness(
            candidate.last_refreshed if candidate else None
        )
        signals["freshness_days"] = freshness_days

        if freshness_days is not None and freshness_days > self.freshness_limit_days:
            conflicts["stale_data"] = freshness_days

        if existing_family:
            conflicts["existing_family"] = {
                "family_id": existing_family,
                "confidence": domain_confidence,
            }

        if not candidate:
            conflicts["no_candidate"] = True

        total_score = sum(
            components.get(metric, 0.0) * self.weights.get(metric, 0.0)
            for metric in components
        )
        total_score = round(total_score / self.weight_sum, 4)

        return PromotionEvidencePayload(
            signals=signals,
            components=components,
            total_score=total_score,
            threshold_used=self.auto_threshold,
            freshness_days=freshness_days,
            conflicts=conflicts,
        )

    @staticmethod
    def _domains_match(domain_a: str, domain_b: str) -> bool:
        def normalize(domain: str) -> str:
            domain = domain.strip().lower()
            if domain.startswith("http://"):
                domain = domain[7:]
            elif domain.startswith("https://"):
                domain = domain[8:]
            if domain.startswith("www."):
                domain = domain[4:]
            return domain.rstrip("/")

        return normalize(domain_a) == normalize(domain_b)

    @staticmethod
    def _authority_last_updated(
        payload: Dict[str, object],
        candidate_payload: Dict[str, object],
    ) -> Optional[datetime]:
        value = payload.get("authority_last_updated")
        if value is None and candidate_payload:
            value = candidate_payload.get("authority_last_updated")
        if not value:
            return None
        if isinstance(value, datetime):
            return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
        if isinstance(value, (int, float)):
            try:
                return datetime.fromtimestamp(float(value), tz=timezone.utc)
            except Exception:
                return None
        if isinstance(value, str):
            try:
                parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
                return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
            except Exception:
                return None
        return None

    @staticmethod
    def _ratio_ok(
        target_value: Optional[int],
        candidate_value: Optional[float],
        *,
        lower: float = 0.1,
        upper: float = 10.0,
    ) -> Optional[bool]:
        if (
            not target_value
            or not candidate_value
            or target_value <= 0
            or candidate_value <= 0
        ):
            return None
        try:
            ratio = candidate_value / float(target_value)
            return lower <= ratio <= upper
        except Exception:  # pragma: no cover
            return None

    @staticmethod
    def _freshness(last_refreshed: Optional[datetime]) -> Optional[int]:
        if not last_refreshed:
            return None
        now = datetime.now(tz=timezone.utc)
        if last_refreshed.tzinfo is None:
            last_refreshed = last_refreshed.replace(tzinfo=timezone.utc)
        delta = now - last_refreshed
        return delta.days

    def _country_aligned(
        self,
        target_code: Optional[str],
        candidate_countries: Iterable[Dict[str, object]],
    ) -> bool:
        if not target_code:
            return True
        target_code_upper = target_code.strip().upper()
        expected_names = COUNTRY_LOOKUP.get(target_code_upper)
        if not expected_names:
            # Fallback: accept if any candidate label starts with the ISO code (rare but better than false negative)
            return any(
                isinstance(country, dict)
                and str(country.get("label", ""))
                .lower()
                .startswith(target_code_upper.lower())
                for country in candidate_countries
            )
        for country in candidate_countries:
            if not isinstance(country, dict):
                continue
            wikidata_id = str(country.get("id") or "").strip()
            if (
                wikidata_id
                and COUNTRY_WIKIDATA_IDS.get(wikidata_id) == target_code_upper
            ):
                return True
            label = str(country.get("label", "")).lower()
            if label and label in expected_names:
                return True
        return False


def is_medium_confidence(signals: Dict[str, object]) -> bool:
    """
    Identify candidates that are promising but still missing a verified domain.

    Medium confidence requires at least one strong authority signal, no critical conflicts,
    and the absence of a verified domain.
    """
    crit = bool(signals.get("critical_conflict"))
    if crit:
        return False
    return bool(
        (
            ((signals.get("authority_score") or 0.0) >= 0.3)
            or signals.get("country_aligned")
        )
        and not signals.get("domain_verified")
    )


def should_auto_promote(signals: Dict[str, object]) -> bool:
    """High-confidence path: all primary signals present, not a subsidiary."""
    # Don't auto-promote subsidiaries - prefer the parent company
    if signals.get("gleif_is_subsidiary"):
        return False
    return bool(
        (signals.get("authority_score") or 0) >= 0.6
        and signals.get("country_aligned")
        and signals.get("domain_verified")
    )
