from __future__ import annotations

import hashlib
import json
import logging
import os
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict

try:
    import duckdb  # type: ignore
except Exception:  # pragma: no cover - duckdb may be unavailable in some envs
    duckdb = None  # type: ignore

from .schemas import ColumnProfile, MappingSuggestion, SmartMapRequest, SmartMapResponse
from fmatch.saas.providers import get_llm_provider

log = logging.getLogger(__name__)

_SYSTEM_PROMPT = (
    "You are a B2B account schema matcher. Prefer identity signals (domain/email/id/name) over geography. "
    "Output STRICT JSON only per the schema. Do not invent columns."
)

_FEWSHOT = [
    {
        "input": {
            "source": [{"name": "Website"}],
            "reference": [{"name": "DHC Provider Website"}],
        },
        "output": {
            "pairs": [
                {
                    "source": "Website",
                    "reference": "DHC Provider Website",
                    "relation": "identity",
                    "algo": "Exact",
                    "transforms": ["to_domain", "lower"],
                    "weight": 1.0,
                    "why": ["both are domains"],
                }
            ]
        },
    },
    {
        "input": {
            "source": [{"name": "Company Name"}],
            "reference": [{"name": "Account Name"}],
        },
        "output": {
            "pairs": [
                {
                    "source": "Company Name",
                    "reference": "Account Name",
                    "relation": "supporting",
                    "algo": "TokenSet",
                    "transforms": ["normalize_company_name"],
                    "weight": 0.8,
                    "why": ["name tokens align"],
                }
            ]
        },
    },
]

_CACHE_TTL = timedelta(days=7)
_CACHE_INITIALIZED = False
_CACHE_DISABLED = False


def _default_cache_path() -> Path:
    if os.name == "nt":
        base = Path(
            os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
        )
    else:
        base = Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache")))
    return base / "FoundryMatch" / "ai_mapper_cache.duckdb"


_CACHE_PATH = Path(os.getenv("FM_AI_CACHE_PATH", str(_default_cache_path())))
try:
    _CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
except Exception:
    pass


def is_ai_mapping_enabled() -> bool:
    return os.getenv("ENABLE_AI_MAPPING", "false").strip().lower() in {
        "1",
        "true",
        "yes",
        "on",
    }


def get_ai_sample_rows(default: int = 400) -> int:
    try:
        return max(10, int(os.getenv("AI_SAMPLE_ROWS", str(default))))
    except (TypeError, ValueError):
        return default


def _ensure_cache() -> None:
    global _CACHE_INITIALIZED, _CACHE_DISABLED
    if _CACHE_INITIALIZED or _CACHE_DISABLED:
        return
    if duckdb is None:
        _CACHE_DISABLED = True
        return
    try:
        with duckdb.connect(str(_CACHE_PATH)) as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS ai_mapper_cache (
                    fingerprint TEXT PRIMARY KEY,
                    payload TEXT NOT NULL,
                    expires_at TIMESTAMP NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    model TEXT,
                    prompt_hash TEXT
                )
                """
            )
        _CACHE_INITIALIZED = True
    except Exception as exc:  # pragma: no cover - defensive
        _CACHE_DISABLED = True
        log.warning("ai_mapper: cache disabled (%s)", exc)


def _cache_get(fingerprint: str) -> Dict[str, Any] | None:
    if duckdb is None or _CACHE_DISABLED:
        return None
    _ensure_cache()
    if _CACHE_DISABLED:
        return None
    now = datetime.utcnow()
    try:
        with duckdb.connect(str(_CACHE_PATH)) as conn:
            row = conn.execute(
                "SELECT payload, expires_at FROM ai_mapper_cache WHERE fingerprint = ?",
                [fingerprint],
            ).fetchone()
            if not row:
                return None
            payload, expires_at = row
            if expires_at is not None and expires_at < now:
                conn.execute(
                    "DELETE FROM ai_mapper_cache WHERE fingerprint = ?",
                    [fingerprint],
                )
                return None
    except Exception as exc:  # pragma: no cover - defensive
        log.debug("ai_mapper: cache read failed (%s)", exc)
        return None

    try:
        data = json.loads(payload)
        log.debug("ai_mapper: cache hit for %s", fingerprint[:8])
        return data
    except Exception as exc:  # pragma: no cover - defensive
        log.debug("ai_mapper: cache decode failed (%s)", exc)
        return None


def _cache_put(
    fingerprint: str, response: SmartMapResponse, prompt_hash: str, model: str
) -> None:
    if duckdb is None or _CACHE_DISABLED:
        return
    _ensure_cache()
    if _CACHE_DISABLED:
        return
    payload = json.dumps(response.dict())
    expires_at = datetime.utcnow() + _CACHE_TTL
    now = datetime.utcnow()
    try:
        with duckdb.connect(str(_CACHE_PATH)) as conn:
            conn.execute(
                "DELETE FROM ai_mapper_cache WHERE fingerprint = ?",
                [fingerprint],
            )
            conn.execute(
                "INSERT INTO ai_mapper_cache (fingerprint, payload, expires_at, model, prompt_hash, created_at) VALUES (?, ?, ?, ?, ?, ?)",
                [fingerprint, payload, expires_at, model, prompt_hash, now],
            )
            conn.execute(
                "DELETE FROM ai_mapper_cache WHERE expires_at <= ?",
                [now],
            )
    except Exception as exc:  # pragma: no cover - defensive
        log.debug("ai_mapper: cache write failed (%s)", exc)


def _hash_str(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8", "ignore")).hexdigest()


def _fingerprint(req: SmartMapRequest) -> str:
    headers: list[str] = [f"s:{col.name}" for col in req.source_columns]
    headers.extend(f"r:{col.name}" for col in req.reference_columns)
    header_blob = "|".join(sorted(headers))

    sample_hasher = hashlib.sha256()
    for col in list(req.source_columns) + list(req.reference_columns):
        for sample in col.samples[:20]:
            if not sample:
                continue
            sample_hasher.update(str(sample).strip().lower().encode("utf-8", "ignore"))

    combined = f"{header_blob}:{sample_hasher.hexdigest()}"
    return hashlib.sha256(combined.encode("utf-8", "ignore")).hexdigest()


def _clip_column(col: ColumnProfile) -> Dict[str, Any]:
    data = col.dict(exclude_none=True)
    data["samples"] = list(data.get("samples", []))[:40]
    return data


def _prompt(req: SmartMapRequest) -> str:
    payload = {
        "system": _SYSTEM_PROMPT,
        "fewshot": _FEWSHOT,
        "source": [_clip_column(col) for col in req.source_columns],
        "reference": [_clip_column(col) for col in req.reference_columns],
        "instructions": "Return JSON with fields: columns, pairs, threshold_suggestion, blocking_suggestion.",
    }
    return json.dumps(payload, separators=(",", ":"))


def _resolve_provider():
    """Get the configured LLM provider.

    Returns Vertex AI provider or None if not configured.
    Ollama fallback has been removed - configure Vertex AI properly.
    """
    provider = get_llm_provider()
    if provider is None:
        log.warning(
            "ai_mapper: No LLM provider available. "
            "Set VERTEX_PROJECT and ensure Vertex AI credentials are configured."
        )
    return provider


def call_ai(req: SmartMapRequest, timeout_s: float = 4.0) -> SmartMapResponse | None:
    if not is_ai_mapping_enabled():
        return None

    fingerprint = _fingerprint(req)
    cached = _cache_get(fingerprint)
    if cached:
        try:
            return SmartMapResponse(**cached)
        except Exception as exc:
            log.debug("ai_mapper: cached payload invalid (%s)", exc)

    provider = _resolve_provider()
    if provider is None:
        log.debug("ai_mapper: no provider available")
        return None

    prompt = _prompt(req)
    prompt_hash = _hash_str(prompt)

    try:
        start = time.time()
        result = provider.generate_json(prompt, num_predict=1024)
        elapsed_ms = (time.time() - start) * 1000
        log.info("ai_mapper: LLM ok in %.0fms", elapsed_ms)
    except Exception as exc:
        log.warning("ai_mapper: LLM failed (%s); falling back", exc)
        return None

    payload: Any = result.get("value") if isinstance(result, dict) else result
    if isinstance(payload, str):
        try:
            payload = json.loads(payload)
        except json.JSONDecodeError:
            log.warning("ai_mapper: non-JSON result from provider; falling back")
            return None

    if not isinstance(payload, dict):
        log.warning("ai_mapper: unexpected provider payload type %s", type(payload))
        return None

    try:
        response = SmartMapResponse(**payload)
    except Exception as exc:
        log.warning("ai_mapper: response validation failed (%s)", exc)
        return None

    model_name = getattr(provider, "model", "unknown")
    _cache_put(fingerprint, response, prompt_hash, model_name)
    return response


__all__ = [
    "call_ai",
    "get_ai_sample_rows",
    "is_ai_mapping_enabled",
    "SmartMapRequest",
    "SmartMapResponse",
    "ColumnProfile",
    "MappingSuggestion",
]
