"""Authentication for Specter CLI.

Handles ``specter login`` flow:
- Browser-based device auth via specter.host (OTP code)
- Programmatic auth via SPECTER_API_KEY env var

The device auth flow:
1. POST /api/auth/cli/initiate → session_id + browser_url
2. Open browser → user clicks Authorize
3. User pastes OTP code (XXXX-XXXX) into CLI
4. POST /api/auth/cli/poll → api_key (sp_live_...)
5. Save key to ~/.specter/auth.json
"""

from __future__ import annotations

import contextlib
import logging
import os
import re
import sys
import webbrowser
from pathlib import Path

import httpx

from specter_cli.config import auth_file, clear_auth, get_token, save_auth

log = logging.getLogger(__name__)

DEFAULT_WEB_URL = "https://specter.host"


class AuthError(Exception):
    """Raised when authentication fails."""


def get_web_url() -> str:
    """Return the web app URL, reading SPECTER_WEB_URL env var if set."""
    return os.environ.get("SPECTER_WEB_URL", DEFAULT_WEB_URL)


def _normalize_otp(raw_otp: str) -> str:
    """Normalize OTP: strip non-alphanumeric chars, uppercase.

    Returns the stripped format (e.g. ``AXHK3V9M``) for the API.
    Display formatting (``AXHK-3V9M``) is the caller's responsibility.
    """
    return re.sub(r"[^A-Za-z0-9]", "", raw_otp).upper()


def _initiate_session(base_url: str) -> dict:
    """Start a CLI auth session.

    POST {base_url}/api/auth/cli/initiate

    Returns dict with session_id, browser_url, expires_in.
    Raises AuthError on HTTP errors, lets httpx.ConnectError/TimeoutException propagate.
    """
    resp = httpx.post(f"{base_url}/api/auth/cli/initiate", timeout=30.0)
    if resp.status_code != 200:
        raise AuthError(f"Failed to start login: HTTP {resp.status_code}")
    return resp.json()


def _poll_for_key(base_url: str, session_id: str, otp: str) -> dict:
    """Submit OTP and retrieve API key.

    POST {base_url}/api/auth/cli/poll with {session_id, otp}

    Returns dict with status and api_key on success.
    Raises AuthError on 401 (invalid_otp), 404 (invalid_session), 410 (expired).
    """
    resp = httpx.post(
        f"{base_url}/api/auth/cli/poll",
        json={"session_id": session_id, "otp": otp},
        timeout=30.0,
    )
    if resp.status_code == 200:
        return resp.json()

    try:
        body = resp.json()
        error = body.get("error", "unknown")
        message = body.get("message", "")
    except Exception:
        error = "unknown"
        message = resp.text

    if resp.status_code == 401:
        raise AuthError(f"invalid_otp: {message or 'Invalid code. Please check and try again.'}")
    elif resp.status_code == 404:
        raise AuthError(
            f"invalid_session: {message or 'Authorization not complete. Please click Authorize in the browser first.'}"
        )
    elif resp.status_code == 410:
        raise AuthError(
            f"expired: {message or 'Session expired. Please run `specter login` again.'}"
        )
    else:
        raise AuthError(f"Authentication failed ({resp.status_code}): {error}")


def login_with_browser(base_url: str | None = None) -> None:
    """Authenticate via browser-based device auth flow.

    1. POST /api/auth/cli/initiate → session_id + browser_url
    2. Open browser → user clicks Authorize
    3. Prompt for OTP code (XXXX-XXXX)
    4. POST /api/auth/cli/poll → api_key
    5. Save key to ~/.specter/auth.json
    """
    url = base_url or get_web_url()

    # Step 1: Initiate session.
    try:
        data = _initiate_session(url)
    except (httpx.ConnectError, httpx.TimeoutException) as e:
        raise AuthError("Could not connect to Specter. Check your internet connection.") from e

    session_id = data["session_id"]
    browser_url = data["browser_url"]

    # Step 2: Open browser.
    print("\n  Opening browser to authenticate...\n")
    with contextlib.suppress(Exception):
        webbrowser.open(browser_url)
    print(f"  If the browser didn't open, visit:\n  {browser_url}\n")

    # Step 3+4: OTP prompt with retry on wrong code.
    max_attempts = 3
    for attempt in range(max_attempts):
        try:
            otp = input("  Paste the code from the browser: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\n  Login cancelled.", file=sys.stderr)
            return
        if not otp:
            print("  No code provided. Login cancelled.", file=sys.stderr)
            return

        normalized = _normalize_otp(otp)

        try:
            result = _poll_for_key(url, session_id, normalized)
        except AuthError as e:
            err_str = str(e)
            if "invalid_otp" in err_str:
                remaining = max_attempts - attempt - 1
                if remaining > 0:
                    print(
                        f"  Invalid code. Please check and try again. ({remaining} attempts left)"
                    )
                    continue
                raise AuthError("Invalid code. Too many attempts.") from e
            raise
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise AuthError("Could not connect to Specter. Check your internet connection.") from e

        # Success.
        api_key = result["api_key"]
        if not api_key.startswith("sp_live_"):
            log.warning("API key has unexpected prefix (expected sp_live_)")
        save_auth(api_key, method="device_auth")
        print("\n  \u2713 Authenticated successfully")
        print(f"  API key saved to {auth_file()}")
        return

    raise AuthError("Too many failed attempts.")


def logout() -> None:
    """Remove stored authentication."""
    clear_auth()
    print("\u2713 Logged out")


def require_auth() -> str:
    """Get the current auth token, or exit with an error message.

    Checks in order:
    1. SPECTER_API_KEY env var (non-empty)
    2. Stored token in ~/.specter/auth.json

    Returns the auth token string.
    """
    env_key = os.environ.get("SPECTER_API_KEY", "")
    if env_key:
        return env_key

    token = get_token()
    if not token:
        print(
            "Not logged in. Run 'specter login' first.",
            file=sys.stderr,
        )
        sys.exit(1)
    return token


def read_ssh_public_key() -> str:
    """Read the user's SSH public key.

    Tries common key paths in order:
    1. ~/.ssh/id_ed25519.pub
    2. ~/.ssh/id_rsa.pub
    3. ~/.ssh/id_ecdsa.pub

    Returns the public key string.
    Raises FileNotFoundError if no SSH key is found.
    """
    ssh_dir = Path.home() / ".ssh"
    candidates = [
        ssh_dir / "id_ed25519.pub",
        ssh_dir / "id_rsa.pub",
        ssh_dir / "id_ecdsa.pub",
    ]

    for key_path in candidates:
        if key_path.is_file():
            key = key_path.read_text().strip()
            log.debug("Using SSH public key: %s", key_path)
            return key

    raise FileNotFoundError("No SSH public key found. Generate one with: ssh-keygen -t ed25519")
