"""
auth.py — Jira Cloud authentication for the QA Jira Manager MCP Agent.

Uses Jira OAuth 2.0 (3LO) with a persistent file-based token cache —
same cache-first / browser-fallback pattern as the ADO MSAL auth module.

Flow:
  1. Check ~/.jira-cache/jira-token.json for a cached access token.
     If a valid (non-expired) token exists, it is returned immediately —
     no browser prompt appears.
  2. If the access token is expired but a refresh token is present,
     it is silently exchanged for a new access token and persisted.
  3. Only if no usable token exists: open the browser once for the Jira
     OAuth 2.0 authorization URL. A local loopback server on port 8765
     captures the authorization code and exchanges it for tokens.
     Tokens are written to cache. The browser never opens again until the
     refresh token expires.

Required environment variables (in .env or shell):
  JIRA_CLIENT_ID          Atlassian OAuth 2.0 app client ID
  JIRA_CLIENT_SECRET      Atlassian OAuth 2.0 app client secret
  JIRA_REDIRECT_URI       Must match the app's registered redirect URI
                          (default: http://localhost:8765/callback)
  JIRA_CLOUD_ID           Atlassian cloud ID for the target site
                          (from https://api.atlassian.com/oauth/token/accessible-resources)

Optional:
  JIRA_SCOPES             Space-separated OAuth scopes
                          (default covers all scopes needed by this agent)
"""
from __future__ import annotations

import json
import os
import threading
import time
import urllib.parse
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import Dict, Optional

from dotenv import load_dotenv

load_dotenv()

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_TOKEN_URL = "https://auth.atlassian.com/oauth/token"
_AUTH_URL = "https://auth.atlassian.com/authorize"

_DEFAULT_REDIRECT_URI = "http://localhost:8765/callback"
_CACHE_FILE = Path.home() / ".jira-cache" / "jira-token.json"

_DEFAULT_SCOPES = (
    "read:jira-user "
    "read:jira-work "
    "write:jira-work "
    "offline_access"
)

# ---------------------------------------------------------------------------
# Token cache
# ---------------------------------------------------------------------------

def _load_cache() -> dict:
    try:
        if _CACHE_FILE.exists():
            return json.loads(_CACHE_FILE.read_text(encoding="utf-8"))
    except Exception:
        pass
    return {}


def _save_cache(data: dict) -> None:
    _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    _CACHE_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")


# ---------------------------------------------------------------------------
# OAuth helpers
# ---------------------------------------------------------------------------

def _exchange_code(code: str, client_id: str, client_secret: str, redirect_uri: str) -> dict:
    import requests
    resp = requests.post(
        _TOKEN_URL,
        json={
            "grant_type": "authorization_code",
            "client_id": client_id,
            "client_secret": client_secret,
            "code": code,
            "redirect_uri": redirect_uri,
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


def _refresh_token(refresh: str, client_id: str, client_secret: str) -> dict:
    import requests
    resp = requests.post(
        _TOKEN_URL,
        json={
            "grant_type": "refresh_token",
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": refresh,
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


# ---------------------------------------------------------------------------
# Loopback server — captures the OAuth callback from the browser
# ---------------------------------------------------------------------------

class _CallbackHandler(BaseHTTPRequestHandler):
    auth_code: Optional[str] = None
    error: Optional[str] = None

    def do_GET(self):  # noqa: N802
        parsed = urllib.parse.urlparse(self.path)
        params = urllib.parse.parse_qs(parsed.query)
        if "code" in params:
            _CallbackHandler.auth_code = params["code"][0]
            self._respond("Authentication successful. You can close this tab.")
        else:
            _CallbackHandler.error = params.get("error", ["unknown"])[0]
            self._respond("Authentication failed. Check the terminal for details.")

    def _respond(self, msg: str):
        body = f"<html><body><h2>{msg}</h2></body></html>".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *_):  # suppress access log noise
        pass


def _interactive_login(client_id: str, client_secret: str, redirect_uri: str, scopes: str) -> dict:
    """Open browser for Jira OAuth 2.0 authorization, capture code via loopback."""
    _CallbackHandler.auth_code = None
    _CallbackHandler.error = None

    port = int(urllib.parse.urlparse(redirect_uri).port or 8765)
    server = HTTPServer(("localhost", port), _CallbackHandler)

    # Build the authorization URL
    params = {
        "audience": "api.atlassian.com",
        "client_id": client_id,
        "scope": scopes,
        "redirect_uri": redirect_uri,
        "state": "qa-jira-agent",
        "response_type": "code",
        "prompt": "consent",
    }
    auth_url = f"{_AUTH_URL}?{urllib.parse.urlencode(params)}"

    print(
        f"\n[qa-jira-manager] Opening browser for Jira login...\n"
        f"  If the browser does not open, navigate to:\n  {auth_url}\n",
        flush=True,
    )
    webbrowser.open(auth_url)

    # Block until callback received (or 120 s timeout)
    server.timeout = 120
    deadline = time.time() + 120
    while _CallbackHandler.auth_code is None and _CallbackHandler.error is None:
        server.handle_request()
        if time.time() > deadline:
            raise RuntimeError("[qa-jira-manager] OAuth login timed out after 120 seconds.")
    server.server_close()

    if _CallbackHandler.error:
        raise RuntimeError(f"[qa-jira-manager] OAuth error: {_CallbackHandler.error}")

    return _exchange_code(_CallbackHandler.auth_code, client_id, client_secret, redirect_uri)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def get_auth_headers(content_type: str = "application/json") -> Dict[str, str]:
    """Return HTTP headers with a valid Jira Bearer token."""
    token = _acquire_token()
    return {
        "Authorization": f"Bearer {token}",
        "Content-Type": content_type,
        "Accept": "application/json",
    }


def get_cloud_id() -> str:
    """Return the Jira Cloud ID from env or raise."""
    cloud_id = os.getenv("JIRA_CLOUD_ID", "").strip()
    if not cloud_id:
        raise RuntimeError(
            "JIRA_CLOUD_ID is not set. "
            "Run: curl -H 'Authorization: Bearer <token>' "
            "https://api.atlassian.com/oauth/token/accessible-resources "
            "to find your cloud ID."
        )
    return cloud_id


def get_signed_in_user() -> str:
    """Return the display name of the cached Jira account, or empty string."""
    cache = _load_cache()
    return cache.get("display_name", cache.get("email", ""))


def _acquire_token() -> str:
    """
    1. Return cached non-expired access token.
    2. Refresh silently if a refresh token exists.
    3. Fall back to interactive browser login.
    """
    client_id = os.getenv("JIRA_CLIENT_ID", "").strip()
    client_secret = os.getenv("JIRA_CLIENT_SECRET", "").strip()
    redirect_uri = os.getenv("JIRA_REDIRECT_URI", _DEFAULT_REDIRECT_URI).strip()
    scopes = os.getenv("JIRA_SCOPES", _DEFAULT_SCOPES).strip()

    if not client_id or not client_secret:
        raise RuntimeError(
            "JIRA_CLIENT_ID and JIRA_CLIENT_SECRET must be set in your .env file. "
            "Create an OAuth 2.0 (3LO) app at https://developer.atlassian.com/console/myapps/ "
            "and add the callback URL: " + _DEFAULT_REDIRECT_URI
        )

    cache = _load_cache()
    now = time.time()

    # 1. Valid cached token
    if cache.get("access_token") and cache.get("expires_at", 0) > now + 30:
        return cache["access_token"]

    # 2. Refresh token available
    if cache.get("refresh_token"):
        try:
            tokens = _refresh_token(cache["refresh_token"], client_id, client_secret)
            _persist_tokens(tokens, cache)
            return tokens["access_token"]
        except Exception:
            pass  # fall through to interactive

    # 3. Interactive browser login
    tokens = _interactive_login(client_id, client_secret, redirect_uri, scopes)
    _persist_tokens(tokens, {})
    return tokens["access_token"]


def _persist_tokens(tokens: dict, existing_cache: dict) -> None:
    cache = dict(existing_cache)
    cache["access_token"] = tokens["access_token"]
    cache["expires_at"] = time.time() + tokens.get("expires_in", 3600)
    if "refresh_token" in tokens:
        cache["refresh_token"] = tokens["refresh_token"]
    _save_cache(cache)
