"""
auth.py — Authentication for the QA Test Case Manager MCP Agent.

Uses MSAL with a persistent file-based token cache — identical approach to
the official microsoft/azure-devops-mcp server and the ado-qa-mcp-agent.

Flow:
  1. Check ~/.msal-cache/msal-cache.json for a cached token.
     If the browser is already signed into a Microsoft account (via VS Code,
     az login, or a previous run), the token is loaded silently — no browser
     prompt appears.
  2. Only if no valid cached token exists: open browser once for interactive
     login. Token is written to cache. Browser never opens again until the
     refresh token expires (~90 days).

The cache path is shared with microsoft/azure-devops-mcp and ado-qa-mcp-agent,
so signing in via any of those tools means this agent is already authenticated.

Optional env vars:
  AZURE_TENANT_ID     Pin to a specific Entra tenant (default: "organizations")
  AZURE_CLIENT_ID     Custom app registration (default: Azure CLI client ID)
"""
from __future__ import annotations

import os
from pathlib import Path
from typing import Dict

# ADO OAuth scope — same as microsoft/azure-devops-mcp
_ADO_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default"

# Azure CLI public client ID — works without a custom app registration
_DEFAULT_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"

# Shared cache location with microsoft/azure-devops-mcp
_CACHE_FILE = Path.home() / ".msal-cache" / "msal-cache.json"

_msal_app = None


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


def get_signed_in_user() -> str:
    """Return the display name / UPN of the cached account, or empty string."""
    try:
        app, _ = _get_msal_app()
        accounts = app.get_accounts()
        if accounts:
            return accounts[0].get("name") or accounts[0].get("username") or ""
    except Exception:
        pass
    return ""


def _get_msal_app():
    global _msal_app
    if _msal_app is None:
        _msal_app = _create_msal_app()
    return _msal_app


def _create_msal_app():
    import msal  # type: ignore

    client_id = os.getenv("AZURE_CLIENT_ID", _DEFAULT_CLIENT_ID)
    tenant_id = os.getenv("AZURE_TENANT_ID", "organizations")
    authority = f"https://login.microsoftonline.com/{tenant_id}"

    cache = msal.SerializableTokenCache()
    _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    if _CACHE_FILE.exists():
        cache.deserialize(_CACHE_FILE.read_text(encoding="utf-8"))

    app = msal.PublicClientApplication(
        client_id=client_id,
        authority=authority,
        token_cache=cache,
    )
    return app, cache


def _acquire_token() -> str:
    """
    1. Try silent acquisition from the shared cache.
    2. Fall back to acquire_token_interactive (opens browser once).
    """
    app, cache = _get_msal_app()
    scopes = [_ADO_SCOPE]

    accounts = app.get_accounts()
    if accounts:
        result = app.acquire_token_silent(scopes, account=accounts[0])
        if result and "access_token" in result:
            _persist_cache(cache)
            return result["access_token"]

    # No cached token — open browser
    result = app.acquire_token_interactive(scopes=scopes)

    if "access_token" not in result:
        err = result.get("error_description") or result.get("error") or str(result)
        raise RuntimeError(f"MSAL authentication failed: {err}")

    _persist_cache(cache)
    return result["access_token"]


def _persist_cache(cache) -> None:
    if cache.has_state_changed:
        _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        _CACHE_FILE.write_text(cache.serialize(), encoding="utf-8")
