from __future__ import annotations

import io
import json
import os
from typing import Any, Dict
from urllib.parse import urlparse

import pandas as pd

from ..services.engine_client import EngineClient
from ..api.intelligence_client import IntelligenceClient
from ..repositories.job_repo import JobRepository
from ..storage import get_storage_backend, resolve_storage_key
from ..db import get_session
from ..repos.api_repo import ApiRepo
import hmac
import hashlib
import httpx
from collections import defaultdict


class ApiMatchWorker:
    def __init__(self, db, storage=None):
        self.db = db
        self.storage = storage or get_storage_backend()
        self.engine = EngineClient()
        self.intel = IntelligenceClient()
        self.job_repo = JobRepository(db)

    async def _load_df_from_url(self, url: str) -> pd.DataFrame:
        u = urlparse(url)
        scheme = (u.scheme or "").lower()
        if scheme in ("http", "https"):
            import httpx

            async with httpx.AsyncClient(timeout=30.0) as client:
                resp = await client.get(url)
                resp.raise_for_status()
                content = resp.content
            if url.lower().endswith(".csv"):
                return pd.read_csv(io.BytesIO(content))
            return pd.DataFrame(json.loads(content.decode("utf-8")))
        if scheme == "s3":
            bucket_env = os.getenv("S3_BUCKET") or os.getenv("S3_BUCKET_NAME") or ""
            bucket = (u.netloc or "").strip()
            key = (u.path or "").lstrip("/")
            if not bucket or not key:
                raise ValueError("Invalid s3 URL")
            if bucket_env and bucket != bucket_env:
                raise ValueError(
                    f"Cross-bucket S3 read not supported in v1 (got {bucket}, expected {bucket_env})"
                )
            blob = await self.storage.read(key)
            if key.lower().endswith(".csv"):
                return pd.read_csv(io.BytesIO(blob))
            return pd.DataFrame(json.loads(blob.decode("utf-8")))
        raise ValueError(f"Unsupported URL scheme for v1: {scheme}")

    async def run_job(
        self, job_id: str, account_id: str, profile: str, options: Dict[str, Any]
    ):
        # Load artifact created by PublicMatchService
        try:
            # Fetch job config for artifact location
            job = await self.job_repo.get_job(job_id)
            cfg = (job.config or {}) if job else {}
            artifact_key = cfg.get("artifact_key")
            if not artifact_key:
                await self.job_repo.update_job_status(
                    job_id, status="failed", error="Missing artifact key"
                )
                return
            raw = await self.storage.read(artifact_key)
            blob = json.loads(raw.decode())
        except Exception as e:
            await self.job_repo.update_job_status(
                job_id, status="failed", error=f"Artifact load failed: {e}"
            )
            return

        body: Dict[str, Any] = blob.get("body") or {}
        profile = blob.get("profile") or profile

        # Determine data sources (inline JSON or URLs)
        src = body.get("source")
        cands = body.get("candidates") or []
        src_url = body.get("source_url")
        ref_url = body.get("reference_url")
        src_key = body.get("source_storage_key")
        ref_key = body.get("reference_storage_key")

        if src_url:
            try:
                df_src = await self._load_df_from_url(src_url)
            except Exception as e:
                await self.job_repo.update_job_status(
                    job_id, status="failed", error=f"source_url error: {e}"
                )
                return
        elif src_key:
            try:
                path = resolve_storage_key(src_key)
                blob = path.read_bytes()
                if str(path).lower().endswith(".csv"):
                    df_src = pd.read_csv(io.BytesIO(blob))
                else:
                    df_src = pd.DataFrame(json.loads(blob.decode("utf-8")))
            except Exception as e:
                await self.job_repo.update_job_status(
                    job_id, status="failed", error=f"{type(e).__name__}: {e}"
                )
                return
        else:
            df_src = pd.DataFrame([src]) if src else pd.DataFrame()

        if ref_url:
            try:
                df_ref = await self._load_df_from_url(ref_url)
            except Exception as e:
                await self.job_repo.update_job_status(
                    job_id, status="failed", error=f"reference_url error: {e}"
                )
                return
        elif ref_key:
            try:
                path = resolve_storage_key(ref_key)
                blob = path.read_bytes()
                if str(path).lower().endswith(".csv"):
                    df_ref = pd.read_csv(io.BytesIO(blob))
                else:
                    df_ref = pd.DataFrame(json.loads(blob.decode("utf-8")))
            except Exception as e:
                await self.job_repo.update_job_status(
                    job_id, status="failed", error=f"{type(e).__name__}: {e}"
                )
                return
        else:
            df_ref = pd.DataFrame(cands) if cands else pd.DataFrame()

        # Policy + config
        goal = (cfg or {}).get("goal") or profile
        policy = await self.intel.get_policy(account_id=account_id, segment=goal)
        cfg_auto = await self.intel.auto_configure(
            df_src, df_ref if not df_ref.empty else df_src, goal=goal
        )

        # Merge preset mappings if profile has one
        from ..services.public_match_service import PROFILE_CONFIGS

        preset = (PROFILE_CONFIGS.get(profile, {}) or {}).get("preset_mappings", [])
        auto_maps = cfg_auto.get("field_mappings") or cfg_auto.get("mappings") or []

        def _key(m):
            return (
                (m.get("source") or m.get("source_field") or "").lower(),
                (
                    m.get("reference") or m.get("ref") or m.get("reference_field") or ""
                ).lower(),
                (m.get("algorithm") or "").lower(),
            )

        seen, merged = set(), []
        for m in preset + auto_maps:
            k = _key(m)
            if k in seen:
                continue
            seen.add(k)
            merged.append(m)
        mappings = merged

        try:
            await self.job_repo.update_job_status(
                job_id,
                status="processing",
                results={"status": "running", "pct_complete": 5},
            )
        except Exception:
            pass

        # Chunking not needed for tiny payloads; add when URLs are present
        try:
            res = await self.engine.match_records(
                df_src,
                df_ref,
                mappings=mappings,
                threshold=float(policy.get("suggest", 0.70)),
                job_id=job_id,
            )
        except Exception as e:
            await self.job_repo.update_job_status(job_id, status="failed", error=str(e))
            return

        # Shape results (simple inline JSON)
        out_rows = []
        if res.matches is not None and len(res.matches) > 0:
            # Convert pandas rows to dicts
            out_rows = res.matches.to_dict(orient="records")

            # Robust stringification helpers for stable match_id
            def _safe_str(v):
                try:
                    if v is None:
                        return ""
                    if isinstance(v, (bytes, bytearray)):
                        return v.decode("utf-8", "ignore")
                    if hasattr(v, "item"):
                        v = v.item()
                    return str(v)
                except Exception:
                    return ""

            def _row_to_dict(r):
                if isinstance(r, dict):
                    return r
                try:
                    return r.to_dict()  # type: ignore[attr-defined]
                except Exception:
                    try:
                        return dict(r)
                    except Exception:
                        return {}

            # Make match_id stable per (job_id, src, tgt), disambiguate duplicates
            _seen = defaultdict(int)
            for i, r in enumerate(out_rows):
                rd = _row_to_dict(r)
                src = _safe_str(rd.get("source_id") or rd.get("src_id") or i)
                tgt = _safe_str(rd.get("target_id") or rd.get("ref_id") or "")
                base = f"{job_id}|{src}|{tgt}"
                n = _seen[base]
                _seen[base] += 1
                key = base if n == 0 else f"{base}|{n}"
                rd["match_id"] = hashlib.sha1(
                    key.encode("utf-8", "ignore")
                ).hexdigest()[:16]
                out_rows[i] = rd

        summary = {
            "status": "completed",
            "pct_complete": 100,
            "count": len(out_rows),
            "engine_version": getattr(res, "engine_version", ""),
        }
        await self.job_repo.update_job_status(
            job_id,
            status="completed",
            results={"rows": out_rows},
            matches_found=len(out_rows),
        )
        await self.job_repo.update_job_status(
            job_id, status="completed", results={"rows": out_rows, **summary}
        )

        # Async usage metering on completion (best-effort)
        try:
            # Prefer key prefix from job config
            key_prefix = None
            try:
                job = await self.job_repo.get_job(job_id)
                if job and isinstance(job.config, dict):
                    key_prefix = job.config.get("api_key_prefix")
            except Exception:
                key_prefix = None
            if key_prefix:
                async for session in get_session():
                    repo = ApiRepo(session)
                    await repo.bump_usage(
                        api_key_prefix=key_prefix,
                        calls=1,
                        matches=len(out_rows),
                        bytes_count=0,
                    )
                    break
        except Exception:
            pass

        # Optional HMAC-signed webhook callback
        cb = (options or {}).get("callback_url") if isinstance(options, dict) else None
        secret = os.getenv("WEBHOOK_HMAC_SECRET", "")
        if cb and secret:
            payload = {"job_id": job_id, "status": "completed", "summary": summary}
            body = json.dumps(payload).encode()
            sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
            try:
                async with httpx.AsyncClient(timeout=5.0) as client:
                    await client.post(
                        cb,
                        headers={
                            "Content-Type": "application/json",
                            "X-Foundry-Signature": f"sha256={sig}",
                        },
                        content=body,
                    )
            except Exception:
                # Silent failure on webhook
                pass

        # Usage metering: defer to ApiRepo at completion in service or higher layer
