"""Optional link to a hosted/self-hosted Abrade Console workspace.

Strictly opt-in and separate from the core `run` flow, which never needs
it — self-hosted mode is offline by default (hard constraint #4); `abrade
run` still only ever talks to the agent-under-test and Anthropic. Linking
a workspace (`abrade login`) just proves you hold a valid Console API key
and remembers it locally. `abrade push` (see `push_run` below) is the one
command that actually sends anything to Console, and only when you
explicitly run it against an already-completed local run — never
automatically, never as a side effect of `abrade run`.

Uses the standard library's `urllib` rather than adding an HTTP client
dependency for what is currently a handful of small JSON requests.
"""

import json
import os
import urllib.error
import urllib.request
from pathlib import Path
from typing import TYPE_CHECKING

from abrade_cli.safeio import write_secret_text

if TYPE_CHECKING:
    from abrade_cli.models import RunRecord

# The hosted console. This defaulted to http://localhost:8123, which is
# where the API runs under docker-compose — correct for someone developing
# Abrade, wrong for everyone who installs it. `abrade login` from a clean
# machine tried to reach a port on the user's own laptop and failed with a
# connection error that named localhost, which reads like the CLI is
# broken rather than misconfigured.
#
# ABRADE_API_URL still overrides, which is how local development points it
# back at compose.
DEFAULT_API_URL = "https://abrade.ai"


class AbradeApiError(Exception):
    """Any failure talking to a Abrade Console API (login, push, ...)."""


class LoginError(AbradeApiError):
    pass


class PushError(AbradeApiError):
    pass


def _credentials_path(home: Path | None = None) -> Path:
    base = home or Path.home()
    return base / ".abrade" / "credentials.json"


def verify_key(api_key: str, api_url: str) -> dict:
    """Calls GET {api_url}/v1/orgs/me. Raises LoginError on any failure."""
    request = urllib.request.Request(
        f"{api_url.rstrip('/')}/v1/orgs/me",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    try:
        with urllib.request.urlopen(request, timeout=10) as response:  # noqa: S310 — fixed https(s)/http URL built from --api-url, not user-controlled data
            return json.loads(response.read())
    except urllib.error.HTTPError as exc:
        if exc.code == 401:
            raise LoginError("Invalid or revoked API key.") from exc
        raise LoginError(f"Abrade API returned HTTP {exc.code}.") from exc
    except urllib.error.URLError as exc:
        raise LoginError(f"Could not reach {api_url}: {exc.reason}") from exc


def save_credentials(
    *, api_key: str, api_url: str, org: dict, home: Path | None = None
) -> None:
    path = _credentials_path(home)
    # Was write_text() followed by chmod(0o600). Between those two calls
    # the file holding a live API key sat at whatever the umask allowed —
    # 0644 on a typical workstation — and that window is the whole
    # exposure. Creating it 0600 has no window. ~/.abrade itself is
    # tightened to 0700 for the same reason: it previously inherited the
    # umask too.
    write_secret_text(
        path,
        json.dumps(
            {"api_key": api_key, "api_url": api_url, "org_name": org.get("name")},
            indent=2,
        ),
    )


def load_credentials(home: Path | None = None) -> dict | None:
    path = _credentials_path(home)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text())
    except (json.JSONDecodeError, OSError):
        return None


def clear_credentials(home: Path | None = None) -> None:
    _credentials_path(home).unlink(missing_ok=True)


def resolve_api_key(home: Path | None = None) -> str | None:
    """ABRADE_API_KEY, if set, always wins over a stored `abrade login` —
    keeps CI (which sets env vars, not dotfiles) unsurprising."""
    env_key = os.environ.get("ABRADE_API_KEY")
    if env_key:
        return env_key
    creds = load_credentials(home)
    return creds["api_key"] if creds else None


def push_run(
    run: "RunRecord",
    *,
    api_key: str,
    api_url: str,
    agent_system_prompt: str | None,
) -> dict:
    """POSTs an already-completed local run to POST {api_url}/v1/runs/import.

    Never called automatically — only from the explicit `abrade push`
    command. No model_endpoint/model_key are ever included: the AUT's real
    endpoint/credential never leaves this machine, whether the run is
    still running or long finished (see api's RunImportCreate docstring).
    """
    body = {
        "agent_name": run.agent_name,
        "agent_system_prompt": agent_system_prompt,
        "agent_posture": run.posture.value,
        "suite_mode": run.suite_mode.value,
        "posture": run.posture.value,
        "model": run.model,
        "containment": run.containment,
        "started_at": run.started_at,
        "completed_at": run.completed_at,
        "results": [
            {
                "injection_name": r.injection_name or r.injection_id,
                "injection_vector": r.injection_vector or "unknown",
                "injection_severity": r.injection_severity or "medium",
                "injection_telemetry": r.injected_telemetry,
                "status": r.status,
                "verdict": r.verdict.value,
                "reason": r.reason,
                "tell": r.tell,
                "agent_reply": r.agent_reply,
                # Deterministic leak evidence — see canary.py. Sent so the
                # proof survives the trip to Console, not just the verdict
                # it produced.
                "canary_token": r.canary_token,
                "canary_exfiltrated": r.canary_exfiltrated,
                # Without the type, Console shows a generic "value"
                # leaked rather than naming an SSN or a card number.
                "canary_type": r.canary_type,
            }
            for r in run.results
        ],
    }
    request = urllib.request.Request(
        f"{api_url.rstrip('/')}/v1/runs/import",
        data=json.dumps(body).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:  # noqa: S310 — see verify_key
            return json.loads(response.read())
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode(errors="replace")
        if exc.code == 401:
            raise PushError("Invalid or revoked API key.") from exc
        if exc.code == 403:
            raise PushError(f"Not authorized to push: {detail}") from exc
        raise PushError(f"Abrade API returned HTTP {exc.code}: {detail}") from exc
    except urllib.error.URLError as exc:
        raise PushError(f"Could not reach {api_url}: {exc.reason}") from exc
