"""
FoundryGraph API client for g-gremlin.

Provides company enrichment via FoundryGraph:
- Domain lookup (domain -> company profile)
- Name search (company name -> domain + profile)
- Parent company resolution (domain -> ultimate parent)
- Bulk enrichment for CSVs

Backend: Remote API (Cloud Run -> BigQuery). Local DuckDB is deprecated.
"""

from __future__ import annotations

import json
import logging
import os
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from g_gremlin.base import BaseIntegration, IntegrationError, RetryConfig
from g_gremlin.normalize import normalize_domain, normalize_name
from g_gremlin.paths import gremlin_path

logger = logging.getLogger(__name__)

FG_CONFIG_PATH = gremlin_path("foundrygraph.json")

# Default API endpoint (Cloud Run). Prefer explicit env override.
DEFAULT_API_BASE = (
    os.getenv("FOUNDRYGRAPH_API_BASE")
    or os.getenv("FM_FOUNDRYGRAPH_API")
    or "https://fuzzy-matcher-api-5pkec6diha-uc.a.run.app"
)
REMOTE_API_PREFIX = "/api/v2/foundrygraph"

# Deprecated env var - warn if still set
_DEPRECATED_LOCAL_ENV = os.getenv("FOUNDRYGRAPH_PREFER_LOCAL", "")
if _DEPRECATED_LOCAL_ENV.lower() in {"1", "true", "yes"}:
    logger.warning(
        "FOUNDRYGRAPH_PREFER_LOCAL is deprecated and ignored. "
        "FoundryGraph now uses the remote API exclusively."
    )


class FoundryGraphError(RuntimeError):
    """Raised when FoundryGraph operations fail."""


@dataclass
class FoundryGraphConfig:
    """FoundryGraph configuration."""
    api_base: str = DEFAULT_API_BASE
    api_token: Optional[str] = None  # For authenticated API calls


def save_foundrygraph_config(config: FoundryGraphConfig) -> None:
    """Persist FoundryGraph configuration to disk."""
    FG_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    FG_CONFIG_PATH.write_text(json.dumps(asdict(config), indent=2), encoding="utf-8")


def load_foundrygraph_config() -> FoundryGraphConfig:
    """Load FoundryGraph configuration from disk."""
    if not FG_CONFIG_PATH.exists():
        # Return defaults - FoundryGraph can work without explicit config
        return FoundryGraphConfig()
    try:
        data = json.loads(FG_CONFIG_PATH.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise FoundryGraphError(f"Invalid FoundryGraph config at {FG_CONFIG_PATH}: {exc}") from exc

    # Warn if old config has deprecated fields
    if data.get("use_local_db"):
        logger.warning("Config field 'use_local_db' is deprecated and ignored.")

    return FoundryGraphConfig(
        api_base=data.get("api_base", DEFAULT_API_BASE),
        api_token=data.get("api_token"),
    )


class FoundryGraphClient(BaseIntegration):
    """FoundryGraph client using remote API (Cloud Run -> BigQuery)."""

    SERVICE_NAME = "foundrygraph"

    def __init__(
        self,
        config: Optional[FoundryGraphConfig] = None,
    ) -> None:
        retry_cfg = RetryConfig(max_attempts=4, base_delay=1.0, max_delay=10.0)
        super().__init__(default_retry_config=retry_cfg)
        self.config = config or load_foundrygraph_config()

    def ping(self) -> bool:
        """Test connectivity to remote API."""
        try:
            _ = self._call_remote_api("/foundrygraph/ping")
            return True
        except IntegrationError:
            return False

    # ------------------------------------------------------------------
    # Public lookups
    # ------------------------------------------------------------------
    def lookup_by_domain(self, domain: str) -> Optional["CompanyProfile"]:
        """Lookup company by domain via remote API."""
        domain = normalize_domain(domain)
        if not domain:
            return None
        return self._remote_lookup_by_domain(domain)

    def search_by_name(self, name: str, limit: int = 10) -> List["CompanyProfile"]:
        """Search companies by name via remote API."""
        raw_query = name.strip()
        if not raw_query:
            return []

        normalized_query = normalize_name(raw_query)
        candidate_limit = max(limit * 3, limit)
        candidates = self._remote_search_by_name_candidates(
            raw_query, normalized_query, limit=candidate_limit
        )
        ranked = _rank_name_search_results(candidates, raw_query, normalized_query)
        return ranked[:limit]

    # ------------------------------------------------------------------
    # Remote helpers
    # ------------------------------------------------------------------
    def _remote_lookup_by_domain(self, domain: str) -> Optional["CompanyProfile"]:
        params = {"domain": domain}
        data = self._call_remote_api(
            f"{REMOTE_API_PREFIX}/lookup/domain",
            params=params,
            allow_statuses=(404,),
        )
        if not data:
            return None
        if isinstance(data, dict) and "company" in data:
            company = data.get("company")
            if not company:
                return None
            profile = _to_company_profile(company, domain_hint=data.get("domain") or domain)
            profile.confidence = float(data.get("confidence", profile.confidence))
            profile.source = data.get("source") or "remote_api"
            return profile
        return _to_company_profile(data, domain_hint=domain)

    def _remote_search_by_name(self, name: str, limit: int = 10) -> List["CompanyProfile"]:
        params = {"q": name, "limit": limit, "include_aliases": True}
        payload = self._call_remote_api(
            f"{REMOTE_API_PREFIX}/search",
            params=params,
            allow_statuses=(404,),
        )
        if isinstance(payload, dict) and "results" in payload:
            items = payload.get("results", []) or []
        elif isinstance(payload, list):
            items = payload
        elif isinstance(payload, dict):
            items = payload.get("items", []) or []
        else:
            items = []
        return [_to_company_profile(item) for item in items]

    def _remote_search_by_name_candidates(
        self,
        name: str,
        normalized_query: str,
        limit: int = 10,
    ) -> List["CompanyProfile"]:
        candidates = self._remote_search_by_name(name, limit=limit)
        if normalized_query and normalized_query != name.strip().lower():
            candidates.extend(self._remote_search_by_name(normalized_query, limit=limit))
        return _dedupe_profiles_by_name(candidates)

    def _call_remote_api(self, path: str, params: Optional[Dict[str, Any]] = None, allow_statuses: tuple[int, ...] = ()) -> Dict[str, Any]:
        base = self.config.api_base.rstrip("/")
        url = f"{base}{path}"
        headers: Dict[str, str] = {"Accept": "application/json"}

        # Prefer ID token for Cloud Run; fallback to api_token header
        id_token = self._fetch_id_token(base)
        if id_token:
            headers["Authorization"] = f"Bearer {id_token}"
        elif self.config.api_token:
            headers["Authorization"] = f"Bearer {self.config.api_token}"

        resp = self._get(url, headers=headers, params=params or {}, timeout=20, allow_statuses=allow_statuses)
        if resp.status_code in allow_statuses:
            return {}
        try:
            return resp.json()
        except Exception:
            return {}

    def _fetch_id_token(self, audience: str) -> Optional[str]:
        try:
            from google.auth.transport.requests import Request
            from google.oauth2 import id_token

            req = Request()
            return id_token.fetch_id_token(req, audience)
        except Exception:
            return None


@dataclass
class CompanyProfile:
    """Company profile from FoundryGraph."""
    domain: str
    canonical_name: str
    wikidata_id: Optional[str] = None
    industries: Optional[List[str]] = None
    headquarters: Optional[str] = None
    employee_count: Optional[int] = None
    stock_ticker: Optional[str] = None
    ultimate_parent: Optional[str] = None
    parent_domain: Optional[str] = None
    confidence: float = 0.0
    source: str = "foundrygraph"
    metadata: Optional[Dict[str, Any]] = None

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


def _to_company_profile(data: Dict[str, Any], domain_hint: Optional[str] = None) -> CompanyProfile:
    industries_raw = data.get("industries") or data.get("industry")
    industries_list: Optional[List[str]] = None
    if isinstance(industries_raw, list):
        industries_list = []
        for entry in industries_raw:
            if isinstance(entry, dict):
                industries_list.append(entry.get("label") or entry.get("name") or entry.get("id") or "")
            else:
                industries_list.append(str(entry))
    elif isinstance(industries_raw, str):
        industries_list = [industries_raw]

    name = (
        data.get("canonical_name")
        or data.get("label")
        or data.get("name")
        or data.get("company_name")
        or data.get("display_name")
        or ""
    )

    headquarters = data.get("headquarters") or data.get("hq") or data.get("headquarters_address")
    if isinstance(headquarters, dict):
        headquarters = headquarters.get("label") or headquarters.get("city") or str(headquarters)

    return CompanyProfile(
        domain=data.get("domain") or domain_hint or "",
        canonical_name=name,
        wikidata_id=data.get("wikidata_id") or data.get("qid"),
        industries=industries_list,
        headquarters=headquarters,
        stock_ticker=data.get("stock_ticker") or data.get("ticker"),
        employee_count=data.get("employee_count") or data.get("employees"),
        confidence=float(data.get("confidence", 0.9)),
        source=data.get("source") or "remote_api",
    )


def lookup_by_domain(domain: str) -> Optional[CompanyProfile]:
    """
    Lookup company by domain via remote API.
    """
    client = FoundryGraphClient()
    return client.lookup_by_domain(domain)


def search_by_name(
    name: str,
    limit: int = 10,
) -> List[CompanyProfile]:
    """
    Search companies by name via remote API.
    """
    client = FoundryGraphClient()
    return client.search_by_name(name, limit=limit)


def _profile_name_key(profile: CompanyProfile) -> str:
    name = (profile.canonical_name or "").strip().lower()
    if name:
        return name
    return (profile.domain or "").strip().lower()


def _dedupe_profiles_by_name(profiles: List[CompanyProfile]) -> List[CompanyProfile]:
    seen: set[str] = set()
    deduped: List[CompanyProfile] = []
    for profile in profiles:
        key = _profile_name_key(profile)
        if not key or key in seen:
            continue
        seen.add(key)
        deduped.append(profile)
    return deduped


def _rank_name_search_results(
    results: List[CompanyProfile],
    raw_query: str,
    normalized_query: str,
) -> List[CompanyProfile]:
    raw = raw_query.strip()
    raw_lower = raw.lower()
    normalized = normalized_query.strip()

    ranked: List[Tuple[int, float, int, int, CompanyProfile]] = []
    for idx, profile in enumerate(results):
        name = (profile.canonical_name or "").strip()
        name_lower = name.lower()
        name_normalized = normalize_name(name)

        if raw_lower and name_lower == raw_lower:
            match_type = "exact"
            rank = 0
        elif normalized and name_normalized == normalized:
            match_type = "normalized"
            rank = 1
        elif raw_lower and raw_lower in name_lower:
            match_type = "substring"
            rank = 2
        elif normalized and normalized in name_normalized:
            match_type = "substring"
            rank = 2
        else:
            match_type = "other"
            rank = 3

        metadata = dict(profile.metadata or {})
        metadata["normalized_query"] = normalized
        metadata["raw_query"] = raw
        metadata["match_type"] = match_type
        profile.metadata = metadata

        ranked.append(
            (
                rank,
                -float(profile.confidence or 0.0),
                len(name),
                idx,
                profile,
            )
        )

    ranked.sort(key=lambda item: (item[0], item[1], item[2], item[3]))
    return [item[-1] for item in ranked]


def _score_candidates_with_match_gateway(
    name: str,
    candidates: List[CompanyProfile],
) -> Optional[List[Tuple[CompanyProfile, float]]]:
    try:
        from fmatch.saas.services.match_gateway import MatchGateway
    except ImportError:
        return None

    try:
        import pandas as pd
    except ImportError:
        return None

    if not candidates:
        return []

    async def _score() -> List[float]:
        gateway = await MatchGateway.create()
        source_df = pd.DataFrame([{"name": name}])
        reference_df = pd.DataFrame(
            [{"name": candidate.canonical_name or ""} for candidate in candidates]
        )
        mappings = [{"source": "name", "reference": "name", "weight": 1.0}]
        result = await gateway.match(
            source_df,
            reference_df,
            source_col="name",
            target_col="name",
            threshold=0.0,
            mappings=mappings,
        )

        scores = [0.0] * len(candidates)
        if result and not result.matches.empty:
            for _, row in result.matches.iterrows():
                ref_id = row.get("reference_id", row.get("__reference_id"))
                try:
                    ref_index = int(ref_id)
                except (TypeError, ValueError):
                    continue
                score = row.get("score", row.get("match_score", 0.0)) or 0.0
                try:
                    score_value = float(score)
                except (TypeError, ValueError):
                    score_value = 0.0
                if score_value > 1.0:
                    score_value = score_value / 100.0
                if 0 <= ref_index < len(scores):
                    scores[ref_index] = score_value
        return scores

    try:
        import asyncio

        try:
            asyncio.get_running_loop()
        except RuntimeError:
            scores = asyncio.run(_score())
        else:
            return None
    except Exception:
        return None

    return list(zip(candidates, scores))


def search_by_name_fuzzy(
    name: str,
    limit: int = 10,
    threshold: float = 0.8,
    use_engine: bool = True,
) -> List[Tuple[CompanyProfile, float]]:
    """
    Search with fuzzy matching and confidence scores.
    """
    if limit <= 0:
        return []

    try:
        threshold_value = float(threshold)
    except (TypeError, ValueError):
        threshold_value = 0.8
    if threshold_value > 1.0:
        threshold_value = threshold_value / 100.0
    threshold_value = max(0.0, min(1.0, threshold_value))

    candidates = search_by_name(name, limit=max(limit * 3, limit))
    if not candidates:
        return []

    scored: Optional[List[Tuple[CompanyProfile, float]]] = None
    if use_engine:
        scored = _score_candidates_with_match_gateway(name, candidates)

    if scored is None:
        from difflib import SequenceMatcher

        norm_input = normalize_name(name)
        scored = [
            (
                candidate,
                SequenceMatcher(
                    None,
                    norm_input,
                    normalize_name(candidate.canonical_name or ""),
                ).ratio(),
            )
            for candidate in candidates
        ]

    results = [(candidate, score) for candidate, score in scored if score >= threshold_value]
    results.sort(key=lambda item: item[1], reverse=True)
    return results[:limit]


def _looks_like_domain(query: str) -> bool:
    """Check if query looks like a domain, URL, or email."""
    q = (query or "").strip().lower()
    if not q or " " in q:
        return False
    if "@" in q or q.startswith("http://") or q.startswith("https://"):
        return True
    if "." not in q:
        return False
    return q.endswith((".com", ".io", ".org", ".net", ".co", ".ai"))


def resolve_company(
    query: str,
    *,
    prefer_domain: bool = True,
    fuzzy_threshold: float = 0.8,
) -> Tuple[Optional[CompanyProfile], str, float]:
    """
    Smart company resolution with cascade strategy.

    Tries in order:
    1. Domain lookup (if query looks like domain/URL/email)
    2. Exact name match
    3. Fuzzy name match

    Returns:
        (profile, match_type, confidence)
        match_type: "domain", "exact", "fuzzy", "none"
    """
    query = query or ""
    if prefer_domain and _looks_like_domain(query):
        domain = normalize_domain(query)
        if domain:
            result = lookup_by_domain(domain)
            if result:
                return (result, "domain", 1.0)

    exact = search_by_name(query, limit=1)
    if exact and normalize_name(exact[0].canonical_name) == normalize_name(query):
        return (exact[0], "exact", 0.95)

    fuzzy = search_by_name_fuzzy(query, limit=1, threshold=fuzzy_threshold)
    if fuzzy:
        return (fuzzy[0][0], "fuzzy", fuzzy[0][1])

    return (None, "none", 0.0)


def resolve_company_name(name: str) -> Tuple[Optional[str], Dict[str, Any]]:
    """
    Resolve company name to domain using the full resolver.

    This uses the production CompanyDomainResolver which checks:
    - Manual overrides
    - Domain family registry
    - Hardcoded dictionary
    - Wikidata
    - Heuristics + DNS

    Args:
        name: Company name to resolve

    Returns:
        Tuple of (domain, metadata)
    """
    try:
        import sys
        sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
        from fmatch.core import resolve_company_domain

        result = resolve_company_domain(name, mode="balanced")
        value = result.get("value", {})
        return value.get("domain"), value
    except ImportError:
        # Fallback to simple search
        profiles = search_by_name(name, limit=1)
        if profiles and profiles[0].domain:
            return profiles[0].domain, profiles[0].to_dict()
        return None, {}


def resolve_many(
    names: List[str],
    mode: str = "balanced",
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
    """
    Resolve multiple company names to domains.

    Args:
        names: List of company names
        mode: Resolution mode ('fast', 'balanced', 'strict')

    Returns:
        Tuple of (results list, metrics dict)
    """
    try:
        import sys
        sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
        from fmatch.core import resolve_company_domains

        return resolve_company_domains(names, mode=mode)
    except ImportError as exc:
        raise FoundryGraphError(f"Cannot import resolver: {exc}") from exc


def get_ultimate_parent(domain: str) -> Optional[Dict[str, Any]]:
    """
    Get ultimate parent company for a domain.

    Parent lookup uses the domain lookup response when parent fields are present.
    A dedicated parent lookup endpoint can be added later if needed.

    Args:
        domain: Domain to lookup

    Returns:
        Dict with parent info if found, or None if not available
    """
    # Return the profile's parent fields if available from domain lookup
    profile = lookup_by_domain(domain)
    if not profile:
        return None

    # Check if the profile already has parent info from the remote lookup
    if profile.ultimate_parent or profile.parent_domain:
        return {
            "child_domain": domain,
            "child_name": profile.canonical_name,
            "parent_name": profile.ultimate_parent or "",
            "parent_domain": profile.parent_domain or "",
            "confidence": 0.8,
        }

    return None


def enrich_row(
    row: Dict[str, Any],
    domain_col: Optional[str] = None,
    name_col: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Enrich a single row with FoundryGraph data.

    Args:
        row: Input row dict
        domain_col: Column containing domain (if any)
        name_col: Column containing company name (if any)

    Returns:
        Dict with enrichment fields
    """
    result = {
        "fg_wikidata_id": "",
        "fg_canonical_name": "",
        "fg_domain": "",
        "fg_industries": "",
        "fg_headquarters": "",
        "fg_stock_ticker": "",
        "fg_ultimate_parent": "",
        "fg_parent_domain": "",
        "fg_confidence": 0.0,
        "fg_source": "",
        "fg_error": "",
    }

    profile = None

    # Try domain lookup first
    if domain_col and row.get(domain_col):
        domain = row[domain_col].strip()
        if domain:
            profile = lookup_by_domain(domain)

    # Fallback to name resolution
    if not profile and name_col and row.get(name_col):
        name = row[name_col].strip()
        if name:
            domain, metadata = resolve_company_name(name)
            if domain:
                result["fg_domain"] = domain
                result["fg_confidence"] = metadata.get("confidence_score", 0.0) or (
                    0.9 if metadata.get("confidence") == "high" else
                    0.6 if metadata.get("confidence") == "medium" else 0.3
                )
                result["fg_source"] = metadata.get("source", "resolver")
                # Try to get full profile
                profile = lookup_by_domain(domain)

    if profile:
        result["fg_wikidata_id"] = profile.wikidata_id or ""
        result["fg_canonical_name"] = profile.canonical_name
        result["fg_domain"] = profile.domain
        result["fg_industries"] = ", ".join(profile.industries) if profile.industries else ""
        result["fg_headquarters"] = profile.headquarters or ""
        result["fg_stock_ticker"] = profile.stock_ticker or ""
        result["fg_confidence"] = profile.confidence
        result["fg_source"] = profile.source

        # Get parent info
        parent_info = get_ultimate_parent(profile.domain)
        if parent_info:
            result["fg_ultimate_parent"] = parent_info.get("parent_name", "")
            result["fg_parent_domain"] = parent_info.get("parent_domain", "")

    return result


def get_db_stats() -> Dict[str, Any]:
    """
    Get statistics about FoundryGraph.

    NOTE: Local DuckDB stats are deprecated. FoundryGraph now uses BigQuery
    as the backend. Use the remote API's /health endpoint or BigQuery console
    to check database statistics.

    Returns:
        Dict with deprecation notice
    """
    return {
        "error": "Local DuckDB stats are deprecated. FoundryGraph uses BigQuery.",
        "message": (
            "The 'fg stats' command showed local DuckDB statistics which are no longer available. "
            "FoundryGraph now queries BigQuery via the remote API. "
            "Use 'fg lookup' or 'fg search' to verify connectivity."
        ),
        "api_base": DEFAULT_API_BASE,
    }
