"""OCI (OpenCodeIntel) API client for saar --index integration.

Handles the one-command bootstrap flow:
    saar extract ./repo --index

Flow:
    1. Check ~/.saar/config.yaml for OCI API key
    2. If no key: prompt user to get one, show URL
    3. POST /api/v1/repos -- add repo by git URL
    4. POST /api/v1/repos/{id}/index -- trigger indexing
    5. Poll status until done, show progress
    6. Save repo_id to .saar/config.json for future saar diff / MCP use

Why this matters:
    Current setup path took 2+ hours of debugging (OPE-166).
    This reduces it to one flag on a command the user already runs.
    Every saar user becomes an OCI user automatically.
"""
from __future__ import annotations

import json
import logging
import subprocess
import time
from pathlib import Path
from typing import Optional
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin

logger = logging.getLogger(__name__)

_DEFAULT_OCI_BASE = "https://api.opencodeintel.com"
_CONFIG_PATH = Path.home() / ".saar" / "config.yaml"
_POLL_INTERVAL_SEC = 3
_POLL_TIMEOUT_SEC = 300  # 5 minutes max


class OCIAuthError(Exception):
    """Raised when API key is missing or invalid."""


class OCIAPIError(Exception):
    """Raised when OCI API returns an error."""
    def __init__(self, message: str, status_code: int = 0):
        super().__init__(message)
        self.status_code = status_code


# ---------------------------------------------------------------------------
# Config -- read ~/.saar/config.yaml
# ---------------------------------------------------------------------------

def load_oci_config() -> dict:
    """Load OCI config from ~/.saar/config.yaml.

    Returns dict with keys: oci_api_key, oci_base_url (optional).
    Returns empty dict if file doesn't exist.
    """
    if not _CONFIG_PATH.exists():
        return {}
    try:
        # Simple YAML subset -- we only need key: value pairs
        config: dict = {}
        for line in _CONFIG_PATH.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("#") or ":" not in line:
                continue
            key, _, value = line.partition(":")
            config[key.strip()] = value.strip()
        return config
    except Exception as e:
        logger.debug("Failed to read OCI config: %s", e)
        return {}


def save_oci_config(api_key: str, base_url: str = _DEFAULT_OCI_BASE) -> None:
    """Save OCI API key to ~/.saar/config.yaml."""
    config_dir = _CONFIG_PATH.parent
    config_dir.mkdir(parents=True, exist_ok=True)
    content = (
        "# saar configuration\n"
        "# Generated by saar -- edit manually if needed\n"
        f"oci_api_key: {api_key}\n"
        f"oci_base_url: {base_url}\n"
    )
    _CONFIG_PATH.write_text(content, encoding="utf-8")
    logger.debug("Saved OCI config to %s", _CONFIG_PATH)


def get_api_key() -> Optional[str]:
    """Return OCI API key from config, or None if not configured."""
    config = load_oci_config()
    return config.get("oci_api_key") or None


def get_base_url() -> str:
    """Return OCI base URL from config, falling back to default."""
    config = load_oci_config()
    return config.get("oci_base_url") or _DEFAULT_OCI_BASE


# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------

def _api_request(
    method: str,
    path: str,
    api_key: str,
    base_url: str,
    body: Optional[dict] = None,
) -> dict:
    """Make an authenticated request to the OCI API.

    Returns parsed JSON response.
    Raises OCIAPIError on HTTP errors.
    Raises OCIAuthError on 401.
    """
    url = urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    data = json.dumps(body).encode("utf-8") if body else None
    req = Request(url, data=data, headers=headers, method=method)

    try:
        with urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except HTTPError as e:
        if e.code == 401:
            raise OCIAuthError("Invalid or expired API key. Get a new key at opencodeintel.com/dashboard/api-keys")
        body_text = ""
        try:
            body_text = e.read().decode("utf-8")
        except Exception:
            pass
        raise OCIAPIError(f"API error {e.code}: {body_text}", status_code=e.code)
    except URLError as e:
        raise OCIAPIError(f"Could not reach OCI API ({base_url}): {e.reason}")


# ---------------------------------------------------------------------------
# Core operations
# ---------------------------------------------------------------------------

def add_repository(
    name: str,
    git_url: str,
    branch: str,
    api_key: str,
    base_url: str,
) -> dict:
    """Add a repository to OCI. Returns the repo dict with 'id'."""
    return _api_request(
        "POST",
        "/api/v1/repos",
        api_key=api_key,
        base_url=base_url,
        body={"name": name, "git_url": git_url, "branch": branch},
    )


def trigger_indexing(
    repo_id: str,
    api_key: str,
    base_url: str,
) -> dict:
    """Trigger synchronous indexing for a repo. Returns indexing result."""
    return _api_request(
        "POST",
        f"/api/v1/repos/{repo_id}/index",
        api_key=api_key,
        base_url=base_url,
    )


def get_repo_status(
    repo_id: str,
    api_key: str,
    base_url: str,
) -> dict:
    """Get current status of a repository."""
    return _api_request(
        "GET",
        f"/api/v1/repos/{repo_id}",
        api_key=api_key,
        base_url=base_url,
    )


def poll_until_indexed(
    repo_id: str,
    api_key: str,
    base_url: str,
    on_tick=None,
    timeout: int = _POLL_TIMEOUT_SEC,
) -> dict:
    """Poll repo status until indexed or error. Returns final repo dict.

    on_tick: optional callback(elapsed_sec, status_str) for progress display.
    """
    start = time.time()
    while True:
        elapsed = time.time() - start
        if elapsed > timeout:
            raise OCIAPIError(f"Indexing timed out after {timeout}s")

        repo = get_repo_status(repo_id, api_key, base_url)
        status = repo.get("status", "unknown")

        if on_tick:
            on_tick(int(elapsed), status)

        if status == "indexed":
            return repo
        if status in ("error", "failed"):
            raise OCIAPIError(f"Indexing failed with status: {status}")

        time.sleep(_POLL_INTERVAL_SEC)


# ---------------------------------------------------------------------------
# Local repo helpers
# ---------------------------------------------------------------------------

def detect_git_url(repo_path: Path) -> Optional[str]:
    """Detect the git remote URL for a local repo."""
    try:
        result = subprocess.run(
            ["git", "remote", "get-url", "origin"],
            cwd=str(repo_path),
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            url = result.stdout.strip()
            # Convert SSH to HTTPS for OCI
            if url.startswith("git@github.com:"):
                url = url.replace("git@github.com:", "https://github.com/").removesuffix(".git") + ".git"
            return url
    except Exception as e:
        logger.debug("Could not detect git URL: %s", e)
    return None


def detect_default_branch(repo_path: Path) -> str:
    """Detect the current branch of a local repo."""
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            cwd=str(repo_path),
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return result.stdout.strip() or "main"
    except Exception:
        pass
    return "main"


# ---------------------------------------------------------------------------
# Repo ID persistence -- .saar/config.json
# ---------------------------------------------------------------------------

def save_repo_id(repo_path: Path, repo_id: str) -> None:
    """Save OCI repo_id to .saar/config.json for future use by saar diff / MCP."""
    cache_file = repo_path / ".saar" / "config.json"
    cache_dir = cache_file.parent
    cache_dir.mkdir(exist_ok=True)

    data: dict = {}
    if cache_file.exists():
        try:
            data = json.loads(cache_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    data["oci_repo_id"] = repo_id
    cache_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
    logger.debug("Saved OCI repo_id %s to %s", repo_id, cache_file)


def load_repo_id(repo_path: Path) -> Optional[str]:
    """Load a previously saved OCI repo_id for this repo."""
    cache_file = repo_path / ".saar" / "config.json"
    if not cache_file.exists():
        return None
    try:
        data = json.loads(cache_file.read_text(encoding="utf-8"))
        return data.get("oci_repo_id")
    except Exception:
        return None
