"""Periodic skills puller — fetches skills from platform and writes to disk."""

from __future__ import annotations

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

from qc_trace.daemon.config import DaemonConfig

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


class SkillsPuller:
    """Pulls skills from platform API and writes skill files to disk."""

    def __init__(self, config: DaemonConfig) -> None:
        self.config = config
        self._last_pull: float = 0.0
        self._content_hash: str | None = None

    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.skills_pull_interval:
            return
        self._last_pull = now
        try:
            data = self._fetch_skills()
            if data is None:
                return
            content_hash = data.get("content_hash")
            if content_hash and content_hash == self._content_hash:
                return
            self._sync_skills(data)
            self._content_hash = content_hash
        except Exception:
            logger.warning("Failed to pull skills", exc_info=True)

    def reset(self) -> None:
        """Clear interval and hash cache to force next pull."""
        self._last_pull = 0.0
        self._content_hash = None

    def _fetch_skills(self) -> dict | None:
        """GET /api/cli/skills from platform. Returns parsed JSON or None."""
        url = f"{self.config.platform_url.rstrip('/')}/api/cli/skills"
        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 skills from %s: %s", url, e)
            return None

    def _sync_skills(self, data: dict) -> None:
        """Write skill files and delete skills not in the response."""
        skills = data.get("skills", {})
        skills_dir = self.config.base_dir / "skills"

        # Safety: only operate inside a .quickcall-trace directory
        base = self.config.base_dir.resolve()
        if base.name != ".quickcall-trace" and "tmp" not in str(base).lower():
            logger.warning("Refusing _sync_skills: base_dir %s is not a .quickcall-trace directory", base)
            return

        # Write all skills from response
        for skill_name, skill_data in skills.items():
            skill_dir = skills_dir / skill_name
            skill_dir.mkdir(parents=True, exist_ok=True)
            files = skill_data.get("files", {})
            for filename, content in files.items():
                self._write_skill_file(skill_dir, filename, content)

        # Delete skill dirs not in response
        if skills_dir.exists():
            for entry in skills_dir.iterdir():
                if entry.is_dir() and not entry.is_symlink() and entry.name not in skills:
                    shutil.rmtree(entry)
                    logger.info("Removed skill %s (no longer on platform)", entry.name)

    @staticmethod
    def _write_skill_file(skill_dir, filename: str, content: str) -> None:
        """Atomic write: tempfile + os.replace. Makes .sh files executable."""
        target = skill_dir / filename
        target.parent.mkdir(parents=True, exist_ok=True)
        fd, tmp_path = tempfile.mkstemp(dir=str(target.parent), suffix=".tmp")
        try:
            with os.fdopen(fd, "w") as f:
                f.write(content)
            os.replace(tmp_path, str(target))
            if filename.endswith(".sh"):
                os.chmod(str(target), 0o755)
        except BaseException:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
            raise
