"""Pure functions for loading and filtering local recommendation rules.

Rules are stored per-repo in QC_TRACE_CONFIG_DIR/recs/<slug>/rules.json,
written by the daemon's RecsPuller. This module reads them for the /api/recs
endpoint so skills can fetch relevant rules without direct filesystem access.
"""

from __future__ import annotations

import json
import logging
import os
import re
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path

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

DEFAULT_BASE_DIR = Path(os.environ.get("QC_TRACE_CONFIG_DIR", str(Path.home() / ".quickcall-trace")))


def load_rules(base_dir: Path, repo_slug: str) -> dict | None:
    """Read rules.json for a repo slug. Returns parsed dict or None."""
    rules_path = base_dir / "recs" / repo_slug / "rules.json"
    if not rules_path.exists():
        return None
    try:
        return json.loads(rules_path.read_text())
    except (json.JSONDecodeError, OSError):
        logger.warning("Failed to read rules from %s", rules_path)
        return None


def get_pending_rules(rules_data: dict) -> list[dict]:
    """Filter rules with status 'pending' or 'open'."""
    return [r for r in rules_data.get("rules", []) if r.get("status") in ("pending", "open")]


def get_accepted_rules(rules_data: dict) -> list[dict]:
    """Filter rules with status 'accepted'."""
    return [r for r in rules_data.get("rules", []) if r.get("status") == "accepted"]


def resolve_slug_from_cache(cwd: str, base_dir: Path) -> str | None:
    """Look up repo slug from repo-cache.json by matching cwd or a parent.

    Returns slug in 'org--repo' form, or None if no match.
    """
    cache_path = base_dir / "repo-cache.json"
    if not cache_path.exists():
        return None
    try:
        cache = json.loads(cache_path.read_text())
    except (json.JSONDecodeError, OSError):
        return None

    cwd_path = Path(cwd).resolve()
    # Try exact match first, then walk parents
    for check in [cwd_path, *cwd_path.parents]:
        key = str(check)
        if key in cache:
            org_repo = cache[key]  # e.g. "org/repo"
            return org_repo.replace("/", "--")
    return None


def number_rules(rules_data: dict) -> list[dict]:
    """Number non-dismissed rules: pending first, then accepted.

    Returns list of dicts with: num, id, rule, reason, confidence, finding_count, status.
    """
    pending = [r for r in rules_data.get("rules", []) if r.get("status") in ("pending", "open")]
    accepted = [r for r in rules_data.get("rules", []) if r.get("status") == "accepted"]

    result = []
    for i, r in enumerate(pending + accepted, start=1):
        result.append({
            "num": i,
            "id": r.get("id"),
            "rule": r.get("rule"),
            "reason": r.get("reason"),
            "confidence": r.get("confidence"),
            "finding_count": r.get("finding_count"),
            "status": r.get("status"),
        })
    return result


def resolve_numbers_to_rules(rules_data: dict, numbers: list[int]) -> list[dict] | None:
    """Map sequential numbers to rule dicts using number_rules() ordering.

    Returns matching rule dicts, or None if any number is invalid.
    """
    numbered = number_rules(rules_data)
    by_num = {r["num"]: r for r in numbered}
    result = []
    for n in numbers:
        if n not in by_num:
            return None
        result.append(by_num[n])
    return result


def resolve_repo_slug(project_dir: str) -> str | None:
    """Extract repo slug from git remote in project_dir. Returns 'org--repo' or None."""
    try:
        result = subprocess.run(
            ["git", "-C", project_dir, "remote", "get-url", "origin"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode != 0:
            return None
        remote = result.stdout.strip()
    except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
        return None

    match = re.search(r"[:/]([^/]+/[^/]+?)(?:\.git)?$", remote)
    if not match:
        return None
    return match.group(1).replace("/", "--")


def _load_config(base_dir: Path) -> dict:
    """Read config.json from base_dir. Returns {} on failure."""
    config_path = base_dir / "config.json"
    if not config_path.exists():
        return {}
    try:
        return json.loads(config_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def send_feedback(base_dir: Path, rule_id: str, action: str) -> bool:
    """Relay feedback to platform API. Returns True on success.

    POSTs to {platform_url}/api/cli/recs/{rule_id}/feedback with Bearer auth.
    Fails silently (returns False) if no token or request fails.
    """
    import urllib.error
    import urllib.request

    config = _load_config(base_dir)
    platform_url = config.get("platform_url", "https://api.quickcall.dev")
    platform_token = config.get("platform_token")
    if not platform_token:
        logger.warning("No platform_token in config — skipping feedback relay")
        return False

    url = f"{platform_url.rstrip('/')}/api/cli/recs/{rule_id}/feedback"
    rating = "thumbs_up" if action == "accept" else "thumbs_down"
    payload = json.dumps({"action": action, "rating": rating}).encode()
    req = urllib.request.Request(url, data=payload, method="POST")
    req.add_header("Authorization", f"Bearer {platform_token}")
    req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return resp.status == 200
    except (urllib.error.URLError, OSError, TimeoutError) as e:
        logger.warning("Failed to relay feedback to %s: %s", url, e)
        return False


def update_rule_status(base_dir: Path, repo_slug: str, rule_id: str, new_status: str) -> bool:
    """Update a rule's status in rules.json. Returns True if found and updated."""
    rules_path = base_dir / "recs" / repo_slug / "rules.json"
    if not rules_path.exists():
        return False
    try:
        data = json.loads(rules_path.read_text())
    except (json.JSONDecodeError, OSError):
        return False

    found = False
    for rule in data.get("rules", []):
        if rule.get("id") == rule_id:
            rule["status"] = new_status
            if new_status == "accepted":
                rule["accepted_at"] = datetime.now(timezone.utc).isoformat()
            found = True
            break

    if not found:
        return False

    fd, tmp_path = tempfile.mkstemp(dir=str(rules_path.parent), suffix=".tmp")
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(data, f, indent=2)
            f.write("\n")
        os.replace(tmp_path, str(rules_path))
    except BaseException:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    return True
