from __future__ import annotations

import argparse
import asyncio
import inspect
import json
import sys
from pathlib import Path
from typing import Any

from .config import ConfigManager, init_project_layout
from .core.runtime import OpenClawLiteRuntime
from .interfaces.onboarding import run_terminal_onboarding
from .interfaces.web_ui import run_web_ui


def _add_config_argument(parser: argparse.ArgumentParser, *, suppress_default: bool = False) -> None:
    kwargs = {"help": "Path to YAML config"}
    if suppress_default:
        kwargs["default"] = argparse.SUPPRESS
    else:
        kwargs["default"] = "config.yaml"
    parser.add_argument("--config", **kwargs)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="liteclaw")
    _add_config_argument(parser)
    subparsers = parser.add_subparsers(dest="command", required=True)

    init_parser = subparsers.add_parser("init", help="Create config and project directories")
    _add_config_argument(init_parser, suppress_default=True)

    doctor_parser = subparsers.add_parser("doctor", help="Print runtime diagnostics")
    doctor_parser.add_argument("--json", action="store_true", help="Always emit JSON")
    _add_config_argument(doctor_parser, suppress_default=True)

    run_parser = subparsers.add_parser("run", help="Run the Telegram bot")
    _add_config_argument(run_parser, suppress_default=True)

    ask_parser = subparsers.add_parser(
        "ask",
        aliases=["agent"],
        help="Run or continue a local agent session without Telegram",
    )
    ask_parser.add_argument("message", help="Message to send to the agent")
    ask_parser.add_argument("--session-id", default="cli-main", help="Reuse this to continue the same agent session")
    ask_parser.add_argument("--chat-id", default="cli-main")
    ask_parser.add_argument("--user-id", type=int, default=0)
    ask_parser.add_argument("--json", action="store_true", help="Emit the full response payload as JSON")
    _add_config_argument(ask_parser, suppress_default=True)

    web_parser = subparsers.add_parser("web", help="Run local web UI for editing tokens and provider keys")
    web_parser.add_argument("--host", default="127.0.0.1")
    web_parser.add_argument("--port", type=int, default=8787)
    _add_config_argument(web_parser, suppress_default=True)

    onboard_parser = subparsers.add_parser("onboard", help="Run the interactive terminal quickstart wizard and launch LiteClaw")
    _add_config_argument(onboard_parser, suppress_default=True)

    config_parser = subparsers.add_parser("config", help="Inspect the active YAML config")
    _add_config_argument(config_parser, suppress_default=True)
    config_subparsers = config_parser.add_subparsers(dest="config_command", required=True)

    config_show_parser = config_subparsers.add_parser("show", help="Print the full resolved config")
    config_show_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(config_show_parser, suppress_default=True)

    config_get_parser = config_subparsers.add_parser("get", help="Read one dotted config key")
    config_get_parser.add_argument("path", help="Dotted config path, e.g. providers.default")
    config_get_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(config_get_parser, suppress_default=True)

    exec_parser = subparsers.add_parser("exec", help="Run a terminal command through LiteClaw shell executor")
    exec_parser.add_argument("command", help="Shell command to execute")
    exec_parser.add_argument("--mode", choices=["safe", "yolo", "god"], help="Explicit shell mode")
    exec_parser.add_argument("--cwd", help="Optional working directory")
    exec_parser.add_argument("--timeout", type=int, default=None, help="Optional timeout seconds; 0 disables if mode allows it")
    _add_config_argument(exec_parser, suppress_default=True)

    inbox_parser = subparsers.add_parser("inbox", help="Inspect saved Telegram attachments inbox")
    inbox_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(inbox_parser, suppress_default=True)

    sessions_parser = subparsers.add_parser("sessions", help="Inspect saved local agent sessions")
    sessions_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(sessions_parser, suppress_default=True)

    pair_parser = subparsers.add_parser("pair", help="Generate or inspect pairing/auth access state")
    _add_config_argument(pair_parser, suppress_default=True)
    pair_subparsers = pair_parser.add_subparsers(dest="pair_command", required=True)

    pair_create_parser = pair_subparsers.add_parser("create", help="Generate a short-lived pairing code")
    pair_create_parser.add_argument("--owner-user-id", type=int, help="Optional owner id to attribute the code to")
    pair_create_parser.add_argument("--ttl-seconds", type=int, default=600, help="Pairing code lifetime in seconds")
    pair_create_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(pair_create_parser, suppress_default=True)

    pair_show_parser = pair_subparsers.add_parser("show", help="Inspect auth/pairing state if supported by runtime")
    pair_show_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    _add_config_argument(pair_show_parser, suppress_default=True)

    return parser


def _print_json(payload: Any) -> None:
    print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))


def _serialize_session(session: Any) -> dict[str, Any]:
    history = getattr(session, "history", None)
    payload = {
        "session_id": getattr(session, "session_id", None),
        "chat_id": getattr(session, "chat_id", None),
        "user_id": getattr(session, "user_id", None),
        "provider": getattr(session, "provider", None),
        "model": getattr(session, "model", None),
        "security_mode": getattr(session, "security_mode", None),
        "history_messages": len(history) if history is not None else None,
        "enabled_skills": getattr(session, "enabled_skills", None),
    }
    return {key: value for key, value in payload.items() if value is not None}


def _require_auth_manager(runtime: OpenClawLiteRuntime):
    auth_manager = getattr(runtime, "auth_manager", None)
    if auth_manager is None:
        raise RuntimeError("Pairing/auth manager is not available in this build yet.")
    return auth_manager


def _call_supported(fn: Any, **kwargs: Any) -> Any:
    signature = inspect.signature(fn)
    accepted = {
        name: value
        for name, value in kwargs.items()
        if name in signature.parameters and value is not None
    }
    return fn(**accepted)


def _find_method(target: Any, names: list[str]):
    for name in names:
        candidate = getattr(target, name, None)
        if callable(candidate):
            return candidate
    return None


def _normalise_pair_payload(result: Any, *, fallback_ttl: int | None = None) -> dict[str, Any]:
    if isinstance(result, dict):
        return result
    if isinstance(result, str):
        return {"code": result, "ttl_seconds": fallback_ttl}
    payload = {}
    for attr in ["code", "expires_at", "ttl_seconds", "owner_user_id", "owner_chat_id", "created_at"]:
        value = getattr(result, attr, None)
        if value is not None:
            payload[attr] = value
    if payload:
        payload.setdefault("ttl_seconds", fallback_ttl)
        return payload
    return {"value": str(result), "ttl_seconds": fallback_ttl}


def command_init(config_path: str) -> int:
    manager = ConfigManager(config_path)
    init_project_layout(manager)
    config_example = Path(config_path)
    print(f"Initialized project layout using {config_example}")
    return 0


def command_doctor(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    _print_json(runtime.doctor())
    return 0


async def command_run(config_path: str) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    await runtime.run()
    return 0


async def command_ask(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    result = await runtime.handle_user_message(
        session_id=args.session_id,
        chat_id=args.chat_id,
        user_id=args.user_id,
        text=args.message,
        transport="cli",
    )
    if args.json:
        _print_json(result)
        return 0
    print(result["text"])
    if result.get("usage"):
        _print_json(result["usage"])
    return 0


def command_config(config_path: str, args: argparse.Namespace) -> int:
    manager = ConfigManager(config_path)
    if args.config_command == "show":
        payload = manager.data
        if getattr(args, "json", False):
            _print_json(payload)
        else:
            print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
        return 0
    if args.config_command == "get":
        payload = {"path": args.path, "value": manager.get(args.path)}
        if getattr(args, "json", False):
            _print_json(payload)
        else:
            print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
        return 0
    raise ValueError(f"Unknown config command: {args.config_command}")


def command_exec(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    mode = args.mode or runtime.config.get("shell", {}).get("default_mode", "safe")
    result = asyncio.run(runtime.shell_executor.run(args.command, mode=mode, cwd=args.cwd, timeout_seconds=args.timeout))
    print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2, default=str))
    return 0


def command_inbox(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    inbox_root = Path(runtime.config.get("data_dir", "data")) / "telegram_inbox"
    files = []
    if inbox_root.exists():
        for path in sorted(inbox_root.rglob("*")):
            if path.is_file():
                files.append({"path": str(path), "bytes": path.stat().st_size})
    payload = {"inbox_root": str(inbox_root), "files": files}
    if args.json:
        _print_json(payload)
    else:
        print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
    return 0


def command_sessions(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    sessions = runtime.session_store.list_sessions()
    payload = {"sessions": [_serialize_session(item) for item in sessions]}
    if args.json:
        _print_json(payload)
        return 0
    if not payload["sessions"]:
        print("No saved sessions.")
        return 0
    for item in payload["sessions"]:
        session_id = item.get("session_id", "unknown")
        provider = item.get("provider", "?")
        model = item.get("model", "?")
        mode = item.get("security_mode", "?")
        history_messages = item.get("history_messages", 0)
        print(f"{session_id} :: {provider}/{model} :: mode={mode} :: history={history_messages}")
    return 0


def command_pair_create(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    try:
        auth_manager = _require_auth_manager(runtime)
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    method = _find_method(auth_manager, ["create_pairing_code", "generate_pairing_code", "issue_pairing_code"])
    if method is None:
        print("Auth manager does not expose a pairing-code generator.", file=sys.stderr)
        return 2
    result = _call_supported(method, owner_user_id=args.owner_user_id, created_by=args.owner_user_id, ttl_seconds=args.ttl_seconds)
    payload = _normalise_pair_payload(result, fallback_ttl=args.ttl_seconds)
    if args.json:
        _print_json(payload)
        return 0
    print(payload.get("code", payload))
    if payload.get("expires_at"):
        print(f"expires_at={payload['expires_at']}")
    elif payload.get("ttl_seconds"):
        print(f"ttl_seconds={payload['ttl_seconds']}")
    return 0


def command_pair_show(config_path: str, args: argparse.Namespace) -> int:
    runtime = OpenClawLiteRuntime(config_path)
    try:
        auth_manager = _require_auth_manager(runtime)
    except RuntimeError as exc:
        print(str(exc), file=sys.stderr)
        return 2
    method = _find_method(auth_manager, ["export_state", "export_summary", "list_grants", "list_access", "debug_state", "status", "summary"])
    if method is None:
        print("Auth manager does not expose a readable state/export API.", file=sys.stderr)
        return 2
    result = method()
    payload = result if isinstance(result, dict) else {"value": result}
    if args.json:
        _print_json(payload)
        return 0
    print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
    return 0


def command_web(config_path: str, args: argparse.Namespace) -> int:
    run_web_ui(config_path, host=args.host, port=args.port)
    return 0


def command_onboard(config_path: str, args: argparse.Namespace) -> int:
    should_launch = run_terminal_onboarding(config_path)
    if should_launch:
        return asyncio.run(command_run(config_path))
    return 0


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    if args.command == "init":
        return command_init(args.config)
    if args.command == "doctor":
        return command_doctor(args.config, args)
    if args.command == "run":
        return asyncio.run(command_run(args.config))
    if args.command in {"ask", "agent"}:
        return asyncio.run(command_ask(args.config, args))
    if args.command == "web":
        return command_web(args.config, args)
    if args.command == "onboard":
        return command_onboard(args.config, args)
    if args.command == "config":
        return command_config(args.config, args)
    if args.command == "exec":
        return command_exec(args.config, args)
    if args.command == "inbox":
        return command_inbox(args.config, args)
    if args.command == "sessions":
        return command_sessions(args.config, args)
    if args.command == "pair" and args.pair_command == "create":
        return command_pair_create(args.config, args)
    if args.command == "pair" and args.pair_command == "show":
        return command_pair_show(args.config, args)
    parser.error(f"Unknown command: {args.command}")
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
