"""CLI auth commands — login, logout, and status.

Provides the ``sika auth`` command group for managing authentication
credentials via OAuth2 PKCE browser flow, ``--device-code`` for
headless environments, or ``--token-stdin`` for CI/CD pipelines.
"""

from __future__ import annotations

import os
import re
import socket
import sys
import webbrowser
from typing import Any

import typer

from sika._mask import mask_api_key
from sika.auth import CredentialFile
from sika_cli.auth import (
    AuthFlowError,
    CallbackServer,
    build_authorize_url,
    create_api_token,
    exchange_code_for_tokens,
    extract_email_from_id_token,
    generate_pkce_params,
    get_cognito_config,
)
from sika_cli.device_code import (
    DeviceCodeDeniedError,
    DeviceCodeExpiredError,
    poll_for_tokens,
    request_device_code,
)
from sika_cli.errors import handle_errors
from sika_cli.output import get_console, get_error_console, print_json

auth_app = typer.Typer(name="auth", help="Manage authentication credentials.")

# ---------------------------------------------------------------------------
# sika auth login
# ---------------------------------------------------------------------------

_NON_TTY_MESSAGE = (
    "Cannot authenticate in non-interactive mode.\n"
    "Use SIKA_API_KEY environment variable, --token-stdin, "
    "or --device-code."
)

# Token format: ti_<product>_<uuid>.st_<product>_<random>
_TOKEN_PATTERN = re.compile(
    r"^ti_[a-z]+_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
    r"\.st_[a-z]+_[a-zA-Z0-9]+$"
)

_SSH_ENV_VARS = ("SSH_CLIENT", "SSH_TTY", "SSH_CONNECTION")

_DEVICE_CODE_TIMEOUT = 300.0  # 5 minutes


def _validate_token_format(token: str) -> bool:
    """Check whether a token matches the expected format.

    Expected format: ``ti_<product>_<uuid>.st_<product>_<random>``

    Example: ``ti_recon_a1b2c3d4-e5f6-7890-abcd-ef1234567890.st_recon_xYz...``
    """
    return _TOKEN_PATTERN.match(token) is not None


def _is_ssh_session() -> bool:
    """Detect whether the current session is an SSH connection."""
    return any(os.environ.get(var) for var in _SSH_ENV_VARS)


def _handle_token_stdin(
    ctx: typer.Context,
    *,
    json_mode: bool,
    profile: str,
) -> None:
    """Read an API token from stdin and store it in the credential file.

    Validates the token format, strips whitespace, and writes to
    ``~/.sika/credentials`` with ``0600`` permissions.

    Raises:
        typer.Exit: With code 5 if stdin is empty or token is invalid.
    """
    console = get_console(ctx)
    err_console = get_error_console(ctx)

    raw = sys.stdin.read().strip()

    if not raw:
        msg = "No token provided on stdin."
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=5) from None

    if not _validate_token_format(raw):
        msg = (
            "Invalid token format. "
            "Expected: ti_<product>_<uuid>.st_<product>_<random>"
        )
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=5) from None

    cred_file = CredentialFile()
    cred_file.write_api_key(raw, profile=profile)

    masked = mask_api_key(raw)

    if json_mode:
        print_json({"stored": True, "profile": profile, "api_key": masked})
    else:
        console.print(f"Token stored ({masked}).")
        console.print(f"Credentials saved to {cred_file.path}")


def _handle_device_code(
    ctx: typer.Context,
    *,
    json_mode: bool,
    profile: str,
    environment: str,
) -> None:
    """Run the device code authentication flow.

    Requests a device code, displays the user code and verification URL,
    then polls until the user approves or the code expires.

    Raises:
        typer.Exit: With code 2 on auth failure, timeout, or denial.
    """
    console = get_console(ctx)
    err_console = get_error_console(ctx)
    config = get_cognito_config(environment)

    # 1. Request device code
    try:
        device_resp = request_device_code(
            cognito_domain=config["domain"],
            client_id=config["client_id"],
        )
    except AuthFlowError as exc:
        msg = str(exc)
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=2) from None

    # 2. Display user code and verification URL
    if json_mode:
        print_json({
            "action": "user_code",
            "user_code": device_resp.user_code,
            "verification_uri": device_resp.verification_uri,
            "expires_in": device_resp.expires_in,
        })
    else:
        console.print(
            f"\nGo to [bold]{device_resp.verification_uri}[/bold] "
            f"and enter code: [bold]{device_resp.user_code}[/bold]\n"
        )
        console.print("Waiting for authorization...", style="muted")

    # 3. Poll for tokens
    try:
        tokens = poll_for_tokens(
            cognito_domain=config["domain"],
            client_id=config["client_id"],
            device_code=device_resp.device_code,
            interval=device_resp.interval,
            timeout=_DEVICE_CODE_TIMEOUT,
        )
    except DeviceCodeExpiredError:
        msg = "Device code expired. Please try again."
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=2) from None
    except DeviceCodeDeniedError:
        msg = "Authorization denied by the user."
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=2) from None
    except AuthFlowError as exc:
        msg = str(exc)
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=2) from None

    # 4. Create API token from OAuth tokens
    try:
        email = extract_email_from_id_token(tokens.id_token) or "unknown"
        hostname = socket.gethostname()
        api_token = create_api_token(
            platform_api_url=config["platform_api_url"],
            id_token=tokens.id_token,
            label=f"sika-cli on {hostname}",
        )
    except AuthFlowError as exc:
        msg = str(exc)
        if json_mode:
            print_json({"error": msg})
        else:
            err_console.print(msg, style="error")
        raise typer.Exit(code=2) from None

    # 5. Store API token and email
    cred_file = CredentialFile()
    cred_file.write_api_key(api_token, profile=profile)
    if email != "unknown":
        cred_file.write_email(email, profile=profile)

    # 6. Print success
    if json_mode:
        print_json({
            "authenticated": True,
            "email": email,
            "profile": profile,
            "environment": environment,
        })
    else:
        console.print(f"Authenticated as [bold]{email}[/bold].")
        console.print(f"Credentials saved to {cred_file.path}")


@auth_app.command()
@handle_errors
def login(
    ctx: typer.Context,
    token_stdin: bool = typer.Option(
        False,
        "--token-stdin",
        help="Read API token from stdin (for CI/CD pipelines).",
    ),
    device_code: bool = typer.Option(
        False,
        "--device-code",
        help="Use device code flow (for SSH, Docker, headless environments).",
    ),
) -> None:
    """Authenticate via browser, device code, or stdin token.

    By default, opens the Sika login page in your default browser
    (OAuth2 PKCE).  After authenticating, a long-lived API token is
    created and stored in ``~/.sika/credentials``.

    With ``--device-code``, displays a code and URL.  Visit the URL
    on any device, enter the code, and the CLI completes login
    automatically.  Ideal for SSH, Docker, and headless environments::

        sika auth login --device-code

    With ``--token-stdin``, reads an API token directly from stdin
    instead.  This is designed for CI/CD pipelines::

        echo "$SIKA_API_KEY" | sika auth login --token-stdin

    Requires an interactive terminal for browser login.  In SSH or
    headless environments, use ``--device-code``.  In CI/CD or
    scripts, use ``--token-stdin`` or set ``SIKA_API_KEY``.
    """
    obj: dict[str, Any] = ctx.ensure_object(dict)
    json_mode = obj.get("json", False)
    profile = obj.get("profile") or "default"
    environment = obj.get("environment", "production")

    # --token-stdin: skip browser flow entirely
    if token_stdin:
        _handle_token_stdin(ctx, json_mode=json_mode, profile=profile)
        return

    # --device-code: headless flow
    if device_code:
        _handle_device_code(
            ctx,
            json_mode=json_mode,
            profile=profile,
            environment=environment,
        )
        return

    console = get_console(ctx)
    err_console = get_error_console(ctx)

    # Non-TTY detection: refuse immediately in non-interactive mode
    if not sys.stdin.isatty():
        if json_mode:
            print_json({"error": _NON_TTY_MESSAGE})
        else:
            err_console.print(_NON_TTY_MESSAGE, style="error")
        raise typer.Exit(code=2) from None

    # SSH detection: suggest --device-code before attempting PKCE
    if _is_ssh_session():
        err_console.print(
            "SSH session detected. The browser login may not work.\n"
            "Consider: [bold]sika auth login --device-code[/bold]",
            style="warning",
        )

    config = get_cognito_config(environment)

    # 1. Generate PKCE parameters
    pkce = generate_pkce_params()

    # 2. Start local callback server
    server = CallbackServer()
    server.start()

    try:
        # 3. Build authorization URL and open browser
        auth_url = build_authorize_url(
            cognito_domain=config["domain"],
            client_id=config["client_id"],
            redirect_uri=server.redirect_uri,
            pkce=pkce,
        )

        console.print("Opening browser for authentication...")
        if not webbrowser.open(auth_url):
            err_console.print(
                "Could not open browser. Try: "
                "[bold]sika auth login --device-code[/bold]",
                style="warning",
            )
            console.print(f"Or visit this URL manually: {auth_url}")

        # 4. Wait for callback
        console.print("Waiting for authentication callback...", style="muted")
        if not server.wait(timeout=300):
            if json_mode:
                print_json({"error": "Authentication timed out."})
            else:
                err_console.print("Authentication timed out.", style="error")
            raise typer.Exit(code=2) from None

        result = server.result

        # Check for OAuth errors
        if result.error:
            msg = f"Authentication failed: {result.error}"
            if json_mode:
                print_json({"error": msg})
            else:
                err_console.print(msg, style="error")
            raise typer.Exit(code=2) from None

        # Validate state (CSRF protection)
        if result.state != pkce.state:
            msg = "Authentication failed: state mismatch (possible CSRF)."
            if json_mode:
                print_json({"error": msg})
            else:
                err_console.print(msg, style="error")
            raise typer.Exit(code=2) from None

        if not result.code:
            msg = "Authentication failed: no authorization code received."
            if json_mode:
                print_json({"error": msg})
            else:
                err_console.print(msg, style="error")
            raise typer.Exit(code=2) from None

        # 5. Exchange authorization code for tokens
        try:
            tokens = exchange_code_for_tokens(
                cognito_domain=config["domain"],
                client_id=config["client_id"],
                redirect_uri=server.redirect_uri,
                code=result.code,
                code_verifier=pkce.code_verifier,
            )

            # 6. Extract email from ID token
            email = extract_email_from_id_token(tokens.id_token) or "unknown"

            # 7. Create API token via platform endpoint
            hostname = socket.gethostname()
            api_token = create_api_token(
                platform_api_url=config["platform_api_url"],
                id_token=tokens.id_token,
                label=f"sika-cli on {hostname}",
            )
        except AuthFlowError as exc:
            msg = str(exc)
            if json_mode:
                print_json({"error": msg})
            else:
                err_console.print(msg, style="error")
            raise typer.Exit(code=2) from None

        # 8. Store API token and email
        cred_file = CredentialFile()
        cred_file.write_api_key(api_token, profile=profile)
        if email != "unknown":
            cred_file.write_email(email, profile=profile)

        # 9. Print success
        if json_mode:
            print_json({
                "authenticated": True,
                "email": email,
                "profile": profile,
                "environment": environment,
            })
        else:
            console.print(f"Authenticated as [bold]{email}[/bold].")
            console.print(f"Credentials saved to {cred_file.path}")

    finally:
        server.stop()


# ---------------------------------------------------------------------------
# sika auth logout
# ---------------------------------------------------------------------------


@auth_app.command()
@handle_errors
def logout(ctx: typer.Context) -> None:
    """Remove stored credentials.

    Deletes the API key for the current profile from the credential
    file (``~/.sika/credentials``).
    """
    obj: dict[str, Any] = ctx.ensure_object(dict)
    json_mode = obj.get("json", False)
    console = get_console(ctx)
    profile = obj.get("profile") or "default"

    cred_file = CredentialFile()
    cred_file.delete_api_key(profile=profile)

    if json_mode:
        print_json({"logged_out": True, "profile": profile})
    else:
        console.print(f"Logged out (profile: {profile}).")


# ---------------------------------------------------------------------------
# sika auth status
# ---------------------------------------------------------------------------


@auth_app.command()
@handle_errors
def status(ctx: typer.Context) -> None:
    """Show authentication status.

    Checks whether credentials are configured for the current profile
    and displays the masked API key, profile, and environment.
    """
    obj: dict[str, Any] = ctx.ensure_object(dict)
    json_mode = obj.get("json", False)
    console = get_console(ctx)
    profile = obj.get("profile") or "default"
    environment = obj.get("environment", "production")

    cred_file = CredentialFile()
    api_key = cred_file.read_api_key(profile=profile)

    authenticated = api_key is not None

    if json_mode:
        data: dict[str, Any] = {
            "authenticated": authenticated,
            "profile": profile,
            "environment": environment,
        }
        if authenticated:
            data["api_key"] = mask_api_key(api_key)  # type: ignore[arg-type]
        print_json(data)
        return

    if not authenticated:
        console.print("Not authenticated.", style="warning")
        console.print(f"Profile:     {profile}")
        console.print(f"Environment: {environment}")
        console.print(
            "\nRun [bold]sika auth login[/bold] or set "
            "[bold]SIKA_API_KEY[/bold] to authenticate."
        )
        return

    console.print("Authenticated.", style="success")
    console.print(f"API key:     {mask_api_key(api_key)}")  # type: ignore[arg-type]
    console.print(f"Profile:     {profile}")
    console.print(f"Environment: {environment}")
