from __future__ import annotations

import argparse
import getpass
import sys
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional, Sequence

from autotouch_cli.exceptions import AutotouchInputError


@dataclass(frozen=True)
class AuthCommandRuntime:
    resolve_token: Callable[[Optional[str], bool], Optional[str]]
    resolve_user_session_token: Callable[[Optional[str], bool], Optional[str]]
    request_api: Callable[..., Any]
    print_json: Callable[[Any, bool], None]
    load_json_input: Callable[..., Any]
    api_url: Callable[[Optional[str]], str]
    load_config: Callable[[], Dict[str, Any]]
    save_config: Callable[[Dict[str, Any]], Any]
    config_path: Callable[[], Any]
    mask_api_key: Callable[[str], str]
    mask_secret: Callable[[str], str]
    normalize_string_value: Callable[[Any], str]
    env_api_key: Callable[[], Optional[str]]
    env_user_token: Callable[[], Optional[str]]


def cmd_auth_check(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    body = runtime.request_api(
        "GET",
        "/api/auth/check",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    output = dict(body) if isinstance(body, dict) else {"ok": True}
    output["base_url"] = runtime.api_url(args.base_url)
    output["auth_header_mode"] = "x-api-key" if args.use_x_api_key else "authorization"
    runtime.print_json(output, args.compact)


def cmd_auth_status(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    token = runtime.resolve_token(args.token, required=True)
    check_body = runtime.request_api(
        "GET",
        "/api/auth/check",
        base_url=args.base_url,
        token=token,
        use_x_api_key=args.use_x_api_key,
        timeout=args.timeout,
        verbose=args.verbose,
    )

    details_body: Any = None
    details_warning: Optional[str] = None
    try:
        details_body = runtime.request_api(
            "GET",
            "/api/auth/user-details",
            base_url=args.base_url,
            token=token,
            use_x_api_key=args.use_x_api_key,
            timeout=args.timeout,
            verbose=args.verbose,
        )
    except SystemExit as exc:
        details_warning = f"user-details lookup failed with exit code {exc.code}"
    except Exception as exc:
        details_warning = str(exc)

    output: Dict[str, Any] = {
        "ok": bool((check_body or {}).get("ok", True)) if isinstance(check_body, dict) else True,
        "base_url": runtime.api_url(args.base_url),
        "auth_header_mode": "x-api-key" if args.use_x_api_key else "authorization",
    }

    if isinstance(check_body, dict):
        for key in (
            "auth_type",
            "user_id",
            "email",
            "organization_id",
            "api_key_id",
            "api_key_name",
            "granted_scopes",
        ):
            if key in check_body:
                output[key] = check_body.get(key)

    if isinstance(details_body, dict):
        for key in (
            "first_name",
            "last_name",
            "title",
            "organization_name",
            "organization_domain",
            "organization_credits",
            "organization_credits_monthly_quota",
            "organization_next_renewal_date",
            "user_allowance",
            "user_monthly_quota",
            "credit_access",
            "permissions",
            "plan",
            "confirmed",
        ):
            if key in details_body:
                output[key] = details_body.get(key)
        if output.get("user_id") is None and details_body.get("id") is not None:
            output["user_id"] = details_body.get("id")
        if output.get("email") is None and details_body.get("email") is not None:
            output["email"] = details_body.get("email")
        if output.get("organization_id") is None and details_body.get("organization_id") is not None:
            output["organization_id"] = details_body.get("organization_id")

    if details_warning:
        output["details_warning"] = details_warning

    runtime.print_json(output, args.compact)


def cmd_auth_set_key(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    api_key = (args.api_key or "").strip() if getattr(args, "api_key", None) else None
    if not api_key:
        api_key = runtime.resolve_token(None, required=False)
    if not api_key:
        raise AutotouchInputError("missing api key. Pass --api-key or set AUTOTOUCH_API_KEY/SMARTTABLE_TOKEN.")

    cfg = runtime.load_config()
    if cfg.get("api_key") and not args.overwrite:
        raise AutotouchInputError("config already has an API key. Pass --overwrite to replace it.")

    base_url = str(args.base_url or cfg.get("base_url") or runtime.api_url(None)).rstrip("/")
    cfg["api_key"] = api_key
    cfg["base_url"] = base_url
    cfg["updated_at_epoch"] = int(time.time())
    path = runtime.save_config(cfg)

    runtime.print_json(
        {
            "saved": True,
            "config_path": str(path),
            "base_url": base_url,
            "api_key_masked": runtime.mask_api_key(api_key),
        },
        args.compact,
    )


def cmd_auth_bootstrap(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    payload = runtime.load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if payload is None:
        payload = {}
        field_map = {
            "first_name": "first_name",
            "last_name": "last_name",
            "email": "email",
            "password": "password",
            "organization_name": "organization_name",
            "utm_source": "utm_source",
            "utm_medium": "utm_medium",
            "utm_campaign": "utm_campaign",
            "utm_term": "utm_term",
            "utm_content": "utm_content",
        }
        for arg_name, payload_key in field_map.items():
            value = getattr(args, arg_name, None)
            if value is not None and str(value).strip():
                payload[payload_key] = str(value).strip()

        key_name = str(getattr(args, "key_name", "") or "").strip()
        if key_name:
            payload["keyName"] = key_name

        expires_at = str(getattr(args, "expires_at", "") or "").strip()
        if expires_at:
            payload["expiresAt"] = expires_at

        scopes_payload = runtime.load_json_input(
            inline_json=getattr(args, "scopes_json", None),
            file_path=getattr(args, "scopes_file", None),
            context="scopes",
            default=None,
        )
        if scopes_payload is not None:
            if not isinstance(scopes_payload, list):
                raise AutotouchInputError("bootstrap scopes must be a JSON array")
            payload["scopes"] = scopes_payload

    if not isinstance(payload, dict):
        raise AutotouchInputError("auth bootstrap payload must be a JSON object")

    required_keys = ["first_name", "last_name", "email", "password", "organization_name"]
    missing = [key for key in required_keys if not str(payload.get(key) or "").strip()]
    if missing:
        raise AutotouchInputError(f"auth bootstrap missing required fields: {', '.join(missing)}")

    data = runtime.request_api(
        "POST",
        "/api/auth/agent-bootstrap",
        base_url=args.base_url,
        token=None,
        use_x_api_key=False,
        payload=payload,
        timeout=args.timeout,
        verbose=args.verbose,
    )

    output: Any = data
    if getattr(args, "save_key", False) and isinstance(data, dict):
        api_key = str(data.get("apiKey") or data.get("api_key") or "").strip()
        email = str(payload.get("email") or "").strip().lower()
        password = str(payload.get("password") or "").strip()
        session_data: Optional[Dict[str, Any]] = None
        session_error: Optional[str] = None

        if email and password:
            try:
                session_data = runtime.request_api(
                    "POST",
                    "/api/auth/login",
                    base_url=args.base_url,
                    token=None,
                    use_x_api_key=False,
                    payload={"email": email, "password": password},
                    timeout=args.timeout,
                    verbose=args.verbose,
                )
            except SystemExit as exc:
                session_error = f"login failed with exit code {exc.code}"
            except Exception as exc:
                session_error = str(exc)

        if api_key:
            cfg = runtime.load_config()
            cfg["api_key"] = api_key
            access_token = runtime.normalize_string_value((session_data or {}).get("access_token"))
            refresh_token = runtime.normalize_string_value((session_data or {}).get("refresh_token"))
            if access_token:
                cfg["auth_token"] = access_token
                cfg["auth_email"] = email
                if refresh_token:
                    cfg["refresh_token"] = refresh_token
            cfg["base_url"] = str(args.base_url or runtime.api_url(None)).rstrip("/")
            cfg["updated_at_epoch"] = int(time.time())
            config_path = runtime.save_config(cfg)
            output = dict(data)
            output["saved"] = True
            output["saved_api_key"] = True
            output["saved_session"] = bool(access_token)
            output["config_path"] = str(config_path)
            output["api_key_masked"] = runtime.mask_api_key(api_key)
            if access_token:
                output["access_token_masked"] = runtime.mask_secret(access_token)
            if refresh_token:
                output["refresh_token_masked"] = runtime.mask_secret(refresh_token)
            if session_error:
                output["session_warning"] = session_error
        else:
            output = dict(data)
            output["saved"] = False

    runtime.print_json(output, args.compact)


def cmd_auth_login(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    payload = runtime.load_json_input(
        inline_json=getattr(args, "data_json", None),
        file_path=getattr(args, "data_file", None),
        context="data",
        default=None,
    )
    if payload is None:
        payload = {}
        email = runtime.normalize_string_value(getattr(args, "email", None))
        password = runtime.normalize_string_value(getattr(args, "password", None))
        if not password and sys.stdin.isatty():
            password = getpass.getpass("Password: ").strip()
        if email:
            payload["email"] = email
        if password:
            payload["password"] = password

    if not isinstance(payload, dict):
        raise AutotouchInputError("auth login payload must be a JSON object")

    required_keys = ["email", "password"]
    missing = [key for key in required_keys if not runtime.normalize_string_value(payload.get(key))]
    if missing:
        raise AutotouchInputError(f"auth login missing required fields: {', '.join(missing)}")

    data = runtime.request_api(
        "POST",
        "/api/auth/login",
        base_url=args.base_url,
        token=None,
        use_x_api_key=False,
        payload={
            "email": str(payload["email"]).strip(),
            "password": str(payload["password"]).strip(),
        },
        timeout=args.timeout,
        verbose=args.verbose,
    )

    output: Any = data
    if getattr(args, "save_session", True) and isinstance(data, dict):
        access_token = runtime.normalize_string_value(data.get("access_token"))
        refresh_token = runtime.normalize_string_value(data.get("refresh_token"))
        if access_token:
            cfg = runtime.load_config()
            cfg["auth_token"] = access_token
            if refresh_token:
                cfg["refresh_token"] = refresh_token
            cfg["auth_email"] = str(payload["email"]).strip().lower()
            cfg["base_url"] = str(args.base_url or runtime.api_url(None)).rstrip("/")
            cfg["updated_at_epoch"] = int(time.time())
            config_path = runtime.save_config(cfg)
            output = {
                "saved": True,
                "config_path": str(config_path),
                "base_url": cfg["base_url"],
                "auth_email": cfg["auth_email"],
                "token_type": data.get("token_type"),
                "expires_in": data.get("expires_in"),
                "access_token_masked": runtime.mask_secret(access_token),
                "refresh_token_masked": runtime.mask_secret(refresh_token),
            }

    runtime.print_json(output, args.compact)


def cmd_auth_show(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    cfg = runtime.load_config()
    api_key = str(cfg.get("api_key") or "").strip()
    auth_token = str(cfg.get("auth_token") or "").strip()
    refresh_token = str(cfg.get("refresh_token") or "").strip()
    base_url = str(cfg.get("base_url") or runtime.api_url(None)).rstrip("/")
    path = runtime.config_path()
    default_general_auth = "auth_token" if auth_token else ("api_key" if api_key else None)
    default_user_session_auth = "auth_token" if auth_token else ("env_token" if runtime.env_user_token() else None)
    default_developer_auth = "api_key" if api_key else ("env_api_key" if runtime.env_api_key() else None)
    output = {
        "config_path": str(path),
        "config_exists": path.exists(),
        "has_api_key": bool(api_key),
        "api_key_masked": runtime.mask_api_key(api_key) if api_key else None,
        "has_auth_token": bool(auth_token),
        "auth_token_masked": runtime.mask_secret(auth_token) if auth_token else None,
        "has_refresh_token": bool(refresh_token),
        "refresh_token_masked": runtime.mask_secret(refresh_token) if refresh_token else None,
        "auth_email": cfg.get("auth_email"),
        "base_url": base_url,
        "updated_at_epoch": cfg.get("updated_at_epoch"),
        "default_general_auth": default_general_auth,
        "default_user_session_auth": default_user_session_auth,
        "default_developer_auth": default_developer_auth,
    }
    runtime.print_json(output, args.compact)


def cmd_auth_whoami(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    token = runtime.resolve_user_session_token(args.token, required=True)
    data = runtime.request_api(
        "GET",
        "/api/auth/me",
        base_url=args.base_url,
        token=token,
        use_x_api_key=False,
        timeout=args.timeout,
        verbose=args.verbose,
    )
    runtime.print_json(data, args.compact)


def cmd_auth_clear(args: argparse.Namespace, *, runtime: AuthCommandRuntime) -> None:
    cfg = runtime.load_config()
    changed = False
    if args.all:
        cfg = {}
        changed = True
    else:
        if "api_key" in cfg:
            cfg.pop("api_key", None)
            changed = True
        for key in ("auth_token", "refresh_token", "auth_email"):
            if key in cfg:
                cfg.pop(key, None)
                changed = True
        if args.clear_base_url and "base_url" in cfg:
            cfg.pop("base_url", None)
            changed = True
        if "updated_at_epoch" in cfg:
            cfg["updated_at_epoch"] = int(time.time())
            changed = True

    path = runtime.save_config(cfg) if changed else runtime.config_path()
    runtime.print_json(
        {
            "cleared": changed,
            "config_path": str(path),
            "remaining_keys": sorted(list(cfg.keys())),
        },
        args.compact,
    )


def register_auth_subcommands(
    subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
    *,
    add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
    add_output_formatting_arguments: Callable[[argparse.ArgumentParser], None],
    handlers: Dict[str, Callable[[argparse.Namespace], None]],
    auth_schema_types: Sequence[str],
    default_api_url: str,
    default_timeout_seconds: int,
) -> None:
    auth = subparsers.add_parser("auth", help="Auth helpers")
    auth_sub = auth.add_subparsers(dest="auth_cmd", required=True)

    pa = auth_sub.add_parser(
        "check",
        help="Validate the current token/session and show granted scopes",
        description="Validate the current token/session against the API and show granted scopes for developer API keys.",
    )
    add_api_common_arguments(pa)
    pa.set_defaults(func=handlers["check"])

    pastatus = auth_sub.add_parser(
        "status",
        help="Resolve auth, identity, and credit/allowance context",
        description=(
            "Fetch the effective auth context plus organization/user credit state. "
            "This is the best one-command status check when a key is valid but pointed at the wrong org "
            "or the real blocker is allowance/quota state rather than authentication."
        ),
    )
    add_api_common_arguments(pastatus)
    pastatus.set_defaults(func=handlers["status"])

    pab = auth_sub.add_parser("bootstrap", help="Register an account/org and return a developer key")
    pab.add_argument("--first-name", help="First name")
    pab.add_argument("--last-name", help="Last name")
    pab.add_argument("--email", help="User email")
    pab.add_argument("--password", help="Password")
    pab.add_argument("--organization-name", help="Organization name")
    pab.add_argument("--key-name", help="Developer key label (default: Agent bootstrap key)")
    pab.add_argument("--expires-at", help="Optional developer key expiry ISO timestamp")
    pab.add_argument("--scopes-json", help="Optional JSON array of scopes")
    pab.add_argument("--scopes-file", help="Optional JSON file with scopes array")
    pab.add_argument("--data-json", help="Explicit agent bootstrap payload JSON")
    pab.add_argument("--data-file", help="Path to agent bootstrap payload JSON file")
    pab.add_argument("--save-key", action="store_true", help="Persist the returned apiKey and bootstrap a saved JWT session")
    pab.add_argument("--base-url", default=default_api_url, help=f"API base URL (default: {default_api_url})")
    pab.add_argument("--timeout", type=int, default=default_timeout_seconds, help="HTTP timeout in seconds")
    add_output_formatting_arguments(pab)
    pab.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")
    pab.set_defaults(func=handlers["bootstrap"])

    palogin = auth_sub.add_parser("login", help="Authenticate with email/password and persist a JWT session")
    palogin.add_argument("--email", help="User email")
    palogin.add_argument("--password", help="Password (prompted if omitted in a TTY)")
    palogin.add_argument("--data-json", help="Explicit auth login payload JSON")
    palogin.add_argument("--data-file", help="Path to auth login payload JSON file")
    palogin.set_defaults(save_session=True)
    palogin.add_argument("--no-save-session", action="store_false", dest="save_session", help="Do not persist the returned access token")
    palogin.add_argument("--base-url", default=default_api_url, help=f"API base URL (default: {default_api_url})")
    palogin.add_argument("--timeout", type=int, default=default_timeout_seconds, help="HTTP timeout in seconds")
    add_output_formatting_arguments(palogin)
    palogin.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")
    palogin.set_defaults(func=handlers["login"])

    pask = auth_sub.add_parser("set-key", help="Persist API key/base URL to user config")
    pask.add_argument("--api-key", help="Developer API key to store (defaults from env if omitted)")
    pask.add_argument("--base-url", help=f"Default API base URL (default: {default_api_url})")
    pask.add_argument("--overwrite", action="store_true", help="Replace existing stored API key")
    add_output_formatting_arguments(pask)
    pask.set_defaults(func=handlers["set_key"])

    pashow = auth_sub.add_parser(
        "show",
        aliases=["config"],
        help="Show current local auth config",
        description="Show the locally saved auth config only. This command does not contact the API.",
    )
    add_output_formatting_arguments(pashow)
    pashow.set_defaults(func=handlers["show"])

    pawho = auth_sub.add_parser("whoami", help="Show the current saved user session identity")
    pawho.add_argument("--token", help="Explicit JWT session token")
    pawho.add_argument("--base-url", default=default_api_url, help=f"API base URL (default: {default_api_url})")
    pawho.add_argument("--timeout", type=int, default=default_timeout_seconds, help="HTTP timeout in seconds")
    add_output_formatting_arguments(pawho)
    pawho.add_argument("--verbose", action="store_true", help="Print request metadata to stderr")
    pawho.set_defaults(func=handlers["whoami"])

    paclear = auth_sub.add_parser("clear", help="Clear stored auth config")
    paclear.add_argument("--all", action="store_true", help="Clear all config keys")
    paclear.add_argument("--clear-base-url", action="store_true", help="Also clear stored base_url")
    add_output_formatting_arguments(paclear)
    paclear.set_defaults(func=handlers["clear"])

    paschema = auth_sub.add_parser("schema", help="Print auth payload schemas")
    paschema.add_argument("--type", choices=["all", *auth_schema_types], default="all")
    paschema.add_argument("--out-file", help="Write schema JSON to file")
    add_output_formatting_arguments(paschema)
    paschema.set_defaults(func=handlers["schema"])
