"""Periodic recommendation puller — fetches recs from platform and writes per-repo JSON."""

from __future__ import annotations

import hashlib
import json
import logging
import os
import tempfile
import time
import urllib.error
import urllib.request

from qc_trace.daemon.config import DaemonConfig

logger = logging.getLogger("quickcall.recs")


class RecsPuller:
    """Pulls recommendations from platform API and writes per-repo rules.json files."""

    def __init__(self, config: DaemonConfig) -> None:
        self.config = config
        self._last_pull: float = 0.0
        self._content_hashes: dict[str, str] = {}  # slug → sha256 of last written content

    def pull(self) -> None:
        """Called every daemon cycle. Respects interval. Never raises."""
        if not self.config.platform_token:
            return
        now = time.time()
        if now - self._last_pull < self.config.recs_pull_interval:
            return
        self._last_pull = now
        try:
            data = self._fetch_recs()
            if data is None:
                return
            repos = data.get("repos", {})
            for repo_name, repo_data in repos.items():
                slug = self._sanitize_slug(repo_name)
                self._write_repo_rules(slug, repo_data)
        except Exception:
            logger.warning("Failed to pull recommendations", exc_info=True)

    def _fetch_recs(self) -> dict | None:
        """GET /api/cli/recs from platform. Returns parsed JSON or None."""
        url = f"{self.config.platform_url.rstrip('/')}/api/cli/recs"
        req = urllib.request.Request(url, method="GET")
        req.add_header("Authorization", f"Bearer {self.config.platform_token}")
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return json.loads(resp.read())
        except (urllib.error.URLError, OSError, json.JSONDecodeError, TimeoutError) as e:
            logger.warning("Failed to fetch recs from %s: %s", url, e)
            return None

    def _read_existing_rules(self, slug: str) -> dict | None:
        """Read current rules.json for a repo slug. Returns parsed dict or None."""
        rules_path = self.config.base_dir / "recs" / slug / "rules.json"
        if not rules_path.exists():
            return None
        try:
            return json.loads(rules_path.read_text())
        except (json.JSONDecodeError, OSError):
            return None

    def _write_repo_rules(self, slug: str, repo_data: dict) -> bool:
        """Atomic write to recs/{slug}/rules.json. Skips if unchanged. Returns True if written."""
        content = json.dumps(repo_data, indent=2, sort_keys=True)
        content_hash = hashlib.sha256(content.encode()).hexdigest()
        if self._content_hashes.get(slug) == content_hash:
            return False

        # Merge: preserve local status overrides before writing
        existing = self._read_existing_rules(slug)
        if existing:
            local_statuses = {r["id"]: r for r in existing.get("rules", [])}
            for rule in repo_data.get("rules", []):
                local = local_statuses.get(rule["id"])
                if local and local.get("status") in ("accepted", "dismissed"):
                    rule["status"] = local["status"]
                    if "accepted_at" in local:
                        rule["accepted_at"] = local["accepted_at"]

        # Recompute hash after merge so cache reflects what's actually written
        content = json.dumps(repo_data, indent=2, sort_keys=True)
        content_hash = hashlib.sha256(content.encode()).hexdigest()
        if self._content_hashes.get(slug) == content_hash:
            return False

        recs_dir = self.config.base_dir / "recs" / slug
        recs_dir.mkdir(parents=True, exist_ok=True)
        rules_path = recs_dir / "rules.json"
        # Atomic write: tempfile + rename
        fd, tmp_path = tempfile.mkstemp(dir=str(recs_dir), suffix=".tmp")
        try:
            with os.fdopen(fd, "w") as f:
                f.write(content)
                f.write("\n")
            os.replace(tmp_path, str(rules_path))
            self._content_hashes[slug] = content_hash
            logger.info("Updated rules for %s", slug)
            return True
        except BaseException:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
            raise

    @staticmethod
    def _sanitize_slug(repo_name: str) -> str:
        """Convert repo name to filesystem-safe slug: org/repo → org--repo."""
        return repo_name.replace("/", "--")
