from __future__ import annotations

import argparse
import json
import sys
from typing import Any, Callable, TextIO

from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
from ..public_surface import cli_public_tool_spec_from_namespace
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
from ..version import get_cli_version, get_cli_version_info
from .context import CliContext, build_cli_context
from .formatters import emit_json_result, emit_text_result
from .commands import register_all_commands


Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]


class _CliArgumentError(Exception):
    def __init__(self, *, prog: str, message: str, usage: str) -> None:
        super().__init__(message)
        self.prog = prog
        self.message = message
        self.usage = usage


class _QingflowArgumentParser(argparse.ArgumentParser):
    def error(self, message: str) -> None:
        raise _CliArgumentError(prog=self.prog, message=message, usage=self.format_usage())


def build_parser() -> argparse.ArgumentParser:
    parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
    parser.add_argument("--profile", default="default", help="会话 profile，默认 default")
    parser.add_argument("--json", action="store_true", help="输出 JSON")
    parser.add_argument("--version", action="store_true", help="输出 Qingflow CLI 版本")
    subparsers = parser.add_subparsers(dest="command", required=True)
    version_parser = subparsers.add_parser("version", help="输出 Qingflow CLI 版本")
    version_parser.set_defaults(handler=_handle_version, format_hint="version")
    register_all_commands(subparsers)
    return parser


def main(argv: list[str] | None = None) -> None:
    raise SystemExit(run(argv))


def run(
    argv: list[str] | None = None,
    *,
    context_factory: Callable[[], CliContext] = build_cli_context,
    stdout: TextIO | None = None,
    stderr: TextIO | None = None,
) -> int:
    out = stdout or sys.stdout
    err = stderr or sys.stderr
    parser = build_parser()
    normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
    if "--version" in normalized_argv:
        return _emit_version(json_mode=_should_force_json_output_argv_for_version(normalized_argv), stdout=out)
    try:
        args = parser.parse_args(normalized_argv)
    except _CliArgumentError as exc:
        if _should_force_json_output_argv(normalized_argv):
            payload = {
                "category": "config",
                "status": "failed",
                "error_code": "ARGUMENT_ERROR",
                "message": exc.message,
                "details": {"usage": exc.usage.strip(), "prog": exc.prog},
            }
            payload = _maybe_attach_builder_apply_error_envelope_from_argv(normalized_argv, payload)
            emit_json_result(payload, stream=out)
            return 2
        err.write(exc.usage)
        err.write(f"{exc.prog}: error: {exc.message}\n")
        return 2
    except SystemExit as exc:
        return int(exc.code or 0)
    try:
        _validate_conditional_args(args)
    except _CliArgumentError as exc:
        if _should_force_json_output_argv(normalized_argv):
            payload = {
                "category": "config",
                "status": "failed",
                "error_code": "ARGUMENT_ERROR",
                "message": exc.message,
                "details": {"usage": exc.usage.strip(), "prog": exc.prog},
            }
            payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
            emit_json_result(payload, stream=out)
            return 2
        err.write(exc.usage)
        err.write(f"{exc.prog}: error: {exc.message}\n")
        return 2
    setattr(args, "_stdin", sys.stdin)
    setattr(args, "_stdout_stream", out)
    setattr(args, "_stderr_stream", err)
    if getattr(args, "command", "") == "version":
        return _emit_version(json_mode=bool(args.json), stdout=out)
    handler = getattr(args, "handler", None)
    if handler is None:
        parser.print_help(out)
        return 2
    try:
        if _should_force_json_output(args):
            setattr(args, "json", True)
        context = context_factory()
        if not bool(args.json):
            _emit_cli_effective_context_notice(args, context, stream=err)
        result = handler(args, context)
    except SystemExit as exc:
        return int(exc.code or 0)
    except RuntimeError as exc:
        payload = trim_error_response(_parse_error_payload(exc))
        payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
        return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
    except QingflowApiError as exc:
        payload = trim_error_response(exc.to_dict())
        payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
        return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
    finally:
        if "context" in locals():
            context.close()

    exit_code = _result_exit_code(result)
    trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
    stream = out if bool(args.json) or exit_code == 0 else err
    if bool(args.json):
        emit_json_result(trimmed_result, stream=stream)
    else:
        emit_text_result(trimmed_result, hint=getattr(args, "format_hint", ""), stream=stream)
    return exit_code


def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
    info = get_cli_version_info()
    return {
        "ok": True,
        "status": "success",
        **info,
    }


def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
    version = get_cli_version()
    if json_mode:
        emit_json_result(
            {
                "ok": True,
                "status": "success",
                **get_cli_version_info(),
            },
            stream=stdout,
        )
    else:
        stdout.write(f"{version}\n")
    return 0


def _validate_conditional_args(args: argparse.Namespace) -> None:
    if (
        getattr(args, "command", "") in {"builder", "build"}
        and getattr(args, "builder_command", "") == "layout"
        and getattr(args, "builder_layout_command", "") == "apply"
        and not getattr(args, "apps_file", None)
        and not getattr(args, "sections_file", None)
    ):
        raise _CliArgumentError(
            prog="qingflow builder layout apply",
            message="--sections-file is required",
            usage="usage: qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json\n",
        )


def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
    return "--json" in argv


def _normalize_global_args(argv: list[str]) -> list[str]:
    global_args: list[str] = []
    remaining: list[str] = []
    index = 0
    while index < len(argv):
        token = argv[index]
        if token == "--json":
            global_args.append(token)
            index += 1
            continue
        if token == "--version":
            global_args.append(token)
            index += 1
            continue
        if token == "--profile":
            global_args.append(token)
            if index + 1 >= len(argv):
                global_args.append("")
                break
            global_args.append(argv[index + 1])
            index += 2
            continue
        if token.startswith("--profile="):
            global_args.append(token)
            index += 1
            continue
        remaining.append(token)
        index += 1
    return global_args + remaining


def _should_force_json_output(args: argparse.Namespace) -> bool:
    if bool(getattr(args, "force_json_output", False)):
        return True
    if (
        getattr(args, "command", "") == "builder"
        and getattr(args, "builder_app_command", "") == "repair-code-blocks"
        and bool(getattr(args, "apply", False))
    ):
        return True
    return False


def _should_force_json_output_argv(argv: list[str]) -> bool:
    tokens = _strip_global_args(argv)
    if not tokens or tokens[0] not in {"builder", "build"}:
        return False
    if len(tokens) < 3:
        return False
    section = tokens[1]
    action = tokens[2]
    if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
        return action == "apply"
    if section == "publish":
        return action == "verify"
    if section == "app":
        if action == "release-edit-lock-if-mine":
            return True
        if action == "repair-code-blocks":
            return "--apply" in tokens
    return False


def _maybe_attach_builder_apply_error_envelope_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> dict[str, Any]:
    operation = _builder_apply_operation_from_args(args)
    if not operation:
        return payload
    enriched = dict(payload)
    enriched.setdefault("status", "failed")
    enriched.setdefault("ok", False)
    enriched.setdefault("write_executed", False)
    enriched.setdefault("safe_to_retry", False)
    _copy_arg_identity(enriched, args)
    return _attach_builder_apply_envelope(operation, enriched)


def _maybe_attach_builder_apply_error_envelope_from_argv(argv: list[str], payload: dict[str, Any]) -> dict[str, Any]:
    operation = _builder_apply_operation_from_argv(argv)
    if not operation:
        return payload
    enriched = dict(payload)
    enriched.setdefault("status", "failed")
    enriched.setdefault("ok", False)
    enriched.setdefault("write_executed", False)
    enriched.setdefault("safe_to_retry", False)
    _copy_argv_identity(enriched, argv)
    return _attach_builder_apply_envelope(operation, enriched)


def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
    if getattr(args, "command", "") not in {"builder", "build"}:
        return None
    section = str(getattr(args, "builder_command", "") or "")
    if section == "package" and getattr(args, "builder_package_command", "") == "apply":
        return "package_apply"
    if section == "button" and getattr(args, "builder_button_command", "") == "apply":
        return "app_custom_buttons_apply"
    if section in {"associated-resource", "associated-resources"} and getattr(args, "builder_associated_resource_command", "") == "apply":
        return "app_associated_resources_apply"
    if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
        return "portal_apply"
    if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
        return "portal_delete"
    if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
        return "app_schema_apply"
    if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
        return "app_layout_apply"
    if section == "views" and getattr(args, "builder_views_command", "") == "apply":
        return "app_views_apply"
    if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
        return "app_flow_apply"
    if section == "charts" and getattr(args, "builder_charts_command", "") == "apply":
        return "app_charts_apply"
    if section == "publish" and getattr(args, "builder_publish_command", "") == "verify":
        return "app_publish_verify"
    return None


def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
    tokens = _strip_global_args(argv)
    if not tokens or tokens[0] not in {"builder", "build"} or len(tokens) < 3:
        return None
    section = tokens[1]
    action = tokens[2]
    if action != "apply" and not (section == "publish" and action == "verify") and not (section == "portal" and action == "delete"):
        return None
    mapping = {
        "package": "package_apply",
        "button": "app_custom_buttons_apply",
        "associated-resource": "app_associated_resources_apply",
        "associated-resources": "app_associated_resources_apply",
        "portal": "portal_apply",
        "schema": "app_schema_apply",
        "layout": "app_layout_apply",
        "views": "app_views_apply",
        "flow": "app_flow_apply",
        "charts": "app_charts_apply",
        "publish": "app_publish_verify",
    }
    if section == "portal" and action == "delete":
        return "portal_delete"
    return mapping.get(section)


def _copy_arg_identity(payload: dict[str, Any], args: argparse.Namespace) -> None:
    for attr, key in (
        ("app_key", "app_key"),
        ("app_name", "app_name"),
        ("app_title", "app_title"),
        ("package_id", "package_id"),
        ("dash_key", "dash_key"),
        ("dash_name", "dash_name"),
    ):
        value = getattr(args, attr, None)
        if value not in (None, ""):
            payload.setdefault(key, value)


def _copy_argv_identity(payload: dict[str, Any], argv: list[str]) -> None:
    tokens = _strip_global_args(argv)
    option_to_key = {
        "--app-key": "app_key",
        "--app-name": "app_name",
        "--app-title": "app_title",
        "--package-id": "package_id",
        "--dash-key": "dash_key",
        "--dash-name": "dash_name",
    }
    index = 0
    while index < len(tokens):
        token = tokens[index]
        if token in option_to_key and index + 1 < len(tokens):
            payload.setdefault(option_to_key[token], tokens[index + 1])
            index += 2
            continue
        for option, key in option_to_key.items():
            prefix = f"{option}="
            if token.startswith(prefix):
                payload.setdefault(key, token[len(prefix) :])
                break
        index += 1


def _strip_global_args(argv: list[str]) -> list[str]:
    stripped: list[str] = []
    index = 0
    while index < len(argv):
        token = argv[index]
        if token == "--json":
            index += 1
            continue
        if token == "--profile":
            index += 2
            continue
        if token.startswith("--profile="):
            index += 1
            continue
        stripped.append(token)
        index += 1
    return stripped


def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
    raw = str(exc)
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError:
        return {"category": "runtime", "message": raw}
    return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}


def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
    exit_code = _error_exit_code(payload)
    if json_mode:
        emit_json_result(payload, stream=stdout)
        return exit_code
    lines = [
        f"Category: {payload.get('category') or 'error'}",
        f"Message: {payload.get('message') or 'Unknown error'}",
    ]
    if payload.get("error_code"):
        lines.append(f"Error Code: {payload.get('error_code')}")
    if payload.get("backend_code") is not None:
        lines.append(f"Backend Code: {payload.get('backend_code')}")
    if payload.get("request_id"):
        lines.append(f"Request ID: {payload.get('request_id')}")
    details = payload.get("details")
    if isinstance(details, dict):
        for key, value in details.items():
            if isinstance(value, (str, int, float, bool)) or value is None:
                lines.append(f"{key}: {value}")
    stderr.write("\n".join(lines) + "\n")
    return exit_code


def _error_exit_code(payload: dict[str, Any]) -> int:
    if str(payload.get("error_code") or "").upper() == "ARGUMENT_ERROR":
        return 2
    if _is_auth_or_workspace_payload(payload):
        return 3
    return 4


def _is_auth_or_workspace_payload(payload: dict[str, Any]) -> bool:
    category = str(payload.get("category") or "").lower()
    error_code = str(payload.get("error_code") or "").upper()
    http_status = backend_code_value_int(payload.get("http_status"))
    if category in {"auth", "workspace"} or error_code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
        return True
    if http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
        return True
    return False


def _result_exit_code(result: dict[str, Any]) -> int:
    if not isinstance(result, dict):
        return 0
    if _is_executed_nonfatal_result(result):
        return 0
    if _is_auth_or_workspace_payload(result):
        return 3
    if result.get("ok") is False:
        return 4
    status = str(result.get("status") or "").lower()
    if status == "partial_success" and result.get("ok") is not True and not _has_readback_unavailable_verification(result):
        return 4
    if status in {"failed", "blocked"}:
        return 4
    return 0


def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
    status = str(result.get("status") or "").lower()
    executed = bool(
        result.get("write_executed")
        or result.get("write_may_have_succeeded")
        or result.get("delete_executed")
        or result.get("action_executed")
        or result.get("export_executed")
    )
    return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}


def _has_readback_unavailable_verification(result: dict[str, Any]) -> bool:
    verification = result.get("verification")
    if not isinstance(verification, dict):
        return False
    return any(
        bool(verification.get(key))
        for key in (
            "readback_unavailable",
            "readback_pending",
            "metadata_unverified",
            "views_read_unavailable",
            "readback_before_retry",
        )
    )


def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
    spec = cli_public_tool_spec_from_namespace(args)
    if spec is None or not spec.cli_show_effective_context:
        return
    hide_context_line = bool(getattr(args, "hide_effective_context_line", False))
    sessions = getattr(context, "sessions", None)
    if sessions is None or not hasattr(sessions, "get_profile"):
        return
    profile_name = str(getattr(args, "profile", "default") or "default")
    try:
        session_profile = sessions.get_profile(profile_name)
    except Exception:
        session_profile = None
    workspace_id = getattr(session_profile, "selected_ws_id", None) if session_profile is not None else None
    workspace_name = getattr(session_profile, "selected_ws_name", None) if session_profile is not None else None
    if workspace_id is None:
        workspace_label = "(not selected)"
    elif workspace_name:
        workspace_label = f"{workspace_name} ({workspace_id})"
    else:
        workspace_label = str(workspace_id)
    lines: list[str] = []
    if not hide_context_line:
        lines.append(f"Context: profile={profile_name} workspace={workspace_label}")
    if spec.cli_context_write and profile_name == "default":
        lines.append("Warning: using default profile for a workspace-sensitive write command")
    if not lines:
        return
    stream.write("\n".join(lines) + "\n")


if __name__ == "__main__":
    main()
