"""CLI and gateway entrypoints."""

from __future__ import annotations

import argparse
import getpass
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Callable

from memnixa.app import build_app
from memnixa.channels.manager import ChannelManager
from memnixa.config import (
    AppConfig,
    ProviderConfig,
    add_provider_config,
    ensure_config_file,
    get_default_config_path,
    get_memnixa_home,
    load_config,
    sync_default_config,
)
from memnixa.gateway.client import GatewayClient
from memnixa.gateway.messages import InboundMessageContext
from memnixa.gateway.run_queue import InMemoryRunQueue
from memnixa.gateway.server import GatewayApplication, run_gateway_server
from memnixa.gateway.ui import render_single_turn, run_terminal_loop
from memnixa.identity import OWNER_USER_ID
from memnixa.i18n import t
from memnixa.providers.registry import get_provider_spec, list_provider_specs
from memnixa.runtime_env import (
    resolve_entrypoint_path,
    resolve_launch_cwd,
    resolve_python_executable,
)
from memnixa.session import SessionStore

RuntimeEventHandler = Callable[[dict], None]


def build_parser() -> argparse.ArgumentParser:
    """Build CLI parser."""
    parser = argparse.ArgumentParser(
        prog="memnixa",
        description=t("cli.description"),
    )
    _add_agent_arguments(parser)
    subparsers = parser.add_subparsers(dest="command", required=False)

    agent_parser = subparsers.add_parser(
        "agent",
        help=t("cli.help.agent"),
    )
    _add_agent_arguments(agent_parser)

    chat_parser = subparsers.add_parser(
        "chat",
        help=t("cli.help.chat"),
    )
    _add_agent_arguments(chat_parser)

    gateway_parser = subparsers.add_parser("gateway", help=t("cli.help.gateway"))
    gateway_parser.add_argument(
        "--config",
        type=Path,
        help=t("cli.help.gateway_config"),
    )
    gateway_parser.add_argument("--host", help=t("cli.help.gateway_host"))
    gateway_parser.add_argument("--port", type=int, help=t("cli.help.gateway_port"))

    config_parser = subparsers.add_parser(
        "config",
        help=t("cli.help.config"),
    )
    config_subparsers = config_parser.add_subparsers(
        dest="config_command",
        required=True,
    )
    sync_parser = config_subparsers.add_parser(
        "sync",
        help=t("cli.help.config.sync"),
    )
    sync_parser.add_argument(
        "--path",
        action="store_true",
        help=t("cli.help.config.path_flag"),
    )
    config_subparsers.add_parser(
        "path",
        help=t("cli.help.config.path"),
    )
    config_subparsers.add_parser(
        "open",
        help=t("cli.help.config.open"),
    )
    add_model_parser = config_subparsers.add_parser(
        "add-model",
        help=t("cli.help.config.add_model"),
    )
    add_model_parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )
    add_model_parser.add_argument(
        "--provider",
        help=t("cli.help.config.add_model.provider"),
    )
    add_model_parser.add_argument(
        "--id",
        dest="provider_id",
        help=t("cli.help.config.add_model.id"),
    )
    add_model_parser.add_argument(
        "--label",
        help=t("cli.help.config.add_model.label"),
    )
    add_model_parser.add_argument(
        "--api-key",
        help=t("cli.help.config.add_model.api_key"),
    )
    add_model_parser.add_argument(
        "--model",
        help=t("cli.help.config.add_model.model"),
    )
    add_model_parser.add_argument(
        "--api-base",
        help=t("cli.help.config.add_model.api_base"),
    )
    add_model_parser.add_argument(
        "--set-default",
        action="store_true",
        help=t("cli.help.config.add_model.set_default"),
    )

    data_parser = subparsers.add_parser(
        "data",
        help=t("cli.help.data"),
    )
    data_subparsers = data_parser.add_subparsers(
        dest="data_command",
        required=True,
    )
    dump_parser = data_subparsers.add_parser(
        "dump",
        help=t("cli.help.data.dump"),
    )
    dump_parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )
    dump_parser.add_argument(
        "--session-id",
        help=t("cli.help.data.session_id"),
    )
    dump_parser.add_argument(
        "--limit",
        type=int,
        help=t("cli.help.data.limit"),
    )
    export_memory_parser = data_subparsers.add_parser(
        "export-memory",
        help="Export all stored long-term memory items as JSON.",
    )
    export_memory_parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )

    identity_parser = subparsers.add_parser(
        "identity",
        help=t("cli.help.identity"),
    )
    identity_subparsers = identity_parser.add_subparsers(
        dest="identity_command",
        required=True,
    )
    bind_owner_parser = identity_subparsers.add_parser(
        "bind-owner",
        help=t("cli.help.identity.bind_owner"),
    )
    bind_owner_parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )
    bind_owner_parser.add_argument(
        "--channel",
        required=True,
        help=t("cli.help.identity.channel"),
    )
    bind_owner_parser.add_argument(
        "--external-id",
        required=True,
        help=t("cli.help.identity.external_id"),
    )
    list_parser = identity_subparsers.add_parser(
        "list",
        help=t("cli.help.identity.list"),
    )
    list_parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )

    return parser


def main() -> None:
    """CLI entrypoint."""
    parser = build_parser()
    args = parser.parse_args()
    if args.command in {None, "agent", "chat"}:
        run_agent_command(
            config_path=args.config,
            first_message=args.message,
            via=args.via,
            gateway_url=args.gateway_url,
            gateway_host=args.gateway_host,
            gateway_port=args.gateway_port,
        )
        return
    if args.command == "gateway":
        run_gateway_command(
            config_path=args.config,
            host=args.host,
            port=args.port,
        )
        return
    if args.command == "config":
        if args.config_command == "sync":
            run_config_sync_command(show_path=args.path)
            return
        if args.config_command == "path":
            run_config_path_command()
            return
        if args.config_command == "open":
            run_config_open_command()
            return
        if args.config_command == "add-model":
            run_config_add_model_command(
                config_path=args.config,
                provider=args.provider,
                provider_id=args.provider_id,
                label=args.label,
                api_key=args.api_key,
                model=args.model,
                api_base=args.api_base,
                set_default=args.set_default,
            )
            return
    if args.command == "data" and args.data_command == "dump":
        run_data_dump_command(
            config_path=args.config,
            session_id=args.session_id,
            limit=args.limit,
        )
        return
    if args.command == "data" and args.data_command == "export-memory":
        run_data_export_memory_command(config_path=args.config)
        return
    if args.command == "identity":
        if args.identity_command == "bind-owner":
            run_identity_bind_owner_command(
                config_path=args.config,
                channel=args.channel,
                external_id=args.external_id,
            )
            return
        if args.identity_command == "list":
            run_identity_list_command(config_path=args.config)


def _config_help_text() -> str:
    return t("cli.help.config_path", path=get_default_config_path())


def _add_agent_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--config",
        type=Path,
        help=_config_help_text(),
    )
    parser.add_argument("--message", "-m", help=t("cli.help.message"))
    parser.add_argument(
        "--via",
        choices=("local", "gateway", "auto"),
        default=None,
        help=t("cli.help.via"),
    )
    parser.add_argument(
        "--gateway-url",
        help=t("cli.help.gateway_url"),
    )
    parser.add_argument("--gateway-host", help=t("cli.help.gateway_host"))
    parser.add_argument("--gateway-port", type=int, help=t("cli.help.gateway_port"))


def run_agent_command(
    config_path: Path | None,
    first_message: str | None,
    via: str | None,
    gateway_url: str | None,
    gateway_host: str | None,
    gateway_port: int | None,
) -> None:
    """Run the agent command in local, gateway, or auto mode."""
    local_config = _load_local_config_if_needed(config_path, via)
    effective_via = _resolve_cli_via(via, local_config)
    resolved_gateway_url = _resolve_gateway_url(
        config=local_config,
        explicit_url=gateway_url,
        explicit_host=gateway_host,
        explicit_port=gateway_port,
    )

    if first_message:
        result = _run_one_turn(
            message=first_message,
            via=effective_via,
            config=local_config,
            gateway_url=resolved_gateway_url,
        )
        render_single_turn(
            via=effective_via,
            config=local_config,
            gateway_url=resolved_gateway_url,
            user_message=first_message,
            result=result,
        )
        return

    run_terminal_loop(
        via=effective_via,
        config=local_config,
        gateway_url=resolved_gateway_url,
        execute=lambda user_message, event_callback=None: _run_one_turn(
            message=user_message,
            via=effective_via,
            config=local_config,
            gateway_url=resolved_gateway_url,
            event_callback=event_callback,
        ),
    )


def run_gateway_command(
    config_path: Path | None,
    host: str | None,
    port: int | None,
) -> None:
    """Run the standalone gateway server."""
    config = load_config(config_path, require_api_key=True)
    app = build_app(config)
    run_queue = InMemoryRunQueue()
    channel_manager = ChannelManager(
        app.runtime,
        config.channels,
        run_queue=run_queue,
    )
    bind_host = host or config.gateway_host
    bind_port = port or config.gateway_port
    run_gateway_server(
        application=GatewayApplication(
            runtime=app.runtime,
            channel_manager=channel_manager,
            run_queue=run_queue,
        ),
        host=bind_host,
        port=bind_port,
    )


def run_config_sync_command(*, show_path: bool) -> None:
    """Create or incrementally update the default user-level config."""
    config_path, created, added_paths = sync_default_config()
    verb = "created" if created else "updated"
    print(t(f"config.sync.{verb}", path=config_path))
    if added_paths:
        print(t("config.sync.added_fields"))
        for path in added_paths:
            print(f"  - {path}")
    else:
        print(t("config.sync.no_changes"))
    if show_path:
        print(t("config.sync.home", path=get_memnixa_home()))


def run_config_path_command() -> None:
    """Print the default user-level config path."""
    print(get_default_config_path())


def run_config_open_command() -> None:
    """Open the default config file."""
    config_path, _, _ = sync_default_config()
    _open_path(config_path)
    print(t("config.opened", path=config_path))


def run_config_add_model_command(
    *,
    config_path: Path | None,
    provider: str | None,
    provider_id: str | None,
    label: str | None,
    api_key: str | None,
    model: str | None,
    api_base: str | None,
    set_default: bool,
) -> None:
    """Add one provider profile into the config file."""
    target_path = ensure_config_file(config_path)
    current_config = load_config(target_path, require_api_key=False)
    resolved_provider = provider or _prompt_provider_preset()
    spec = get_provider_spec(resolved_provider)
    resolved_api_key = api_key or _prompt_text(
        t("config.add_model.prompt.api_key"),
        secret=True,
    )
    resolved_model = model or _prompt_text(
        t("config.add_model.prompt.model"),
        default=spec.default_model,
    )
    existing_ids = {profile.id for profile in current_config.list_provider_configs()}
    suggested_id = _build_unique_provider_id(resolved_model, existing_ids)
    if spec.name == "custom_openai_compatible":
        resolved_id = provider_id or _prompt_text(
            t("config.add_model.prompt.id"),
            default=suggested_id,
        )
        resolved_label = label or _prompt_text(
            t("config.add_model.prompt.label"),
            default=resolved_id,
        )
        resolved_api_base = api_base or _prompt_text(
            t("config.add_model.prompt.api_base"),
        )
    else:
        resolved_id = provider_id or suggested_id
        resolved_label = label or spec.label
        resolved_api_base = api_base or spec.default_base_url
    resolved_set_default = set_default or _prompt_yes_no(
        t("config.add_model.prompt.set_default"),
        default=False,
    )

    saved_path, updated_config = add_provider_config(
        target_path,
        ProviderConfig(
            id=resolved_id,
            preset=spec.name,
            api_key=resolved_api_key,
            api_base=resolved_api_base,
            model=resolved_model,
            label=resolved_label,
        ),
        set_default=resolved_set_default,
    )

    print(t("config.add_model.saved", path=saved_path))
    print(
        t(
            "config.add_model.summary",
            id=resolved_id,
            label=resolved_label,
            preset=spec.name,
            model=resolved_model,
        )
    )
    default_profile = updated_config.get_provider_config()
    print(
        t(
            "config.add_model.default",
            id=default_profile.id,
            label=default_profile.label or default_profile.preset,
        )
    )
    print(t("config.add_model.providers"))
    for profile in updated_config.list_provider_configs():
        print(
            t(
                "config.add_model.provider_item",
                id=profile.id,
                label=profile.label or profile.preset,
                preset=profile.preset,
                model=profile.model or "(default)",
            )
        )


def run_data_dump_command(
    *,
    config_path: Path | None,
    session_id: str | None,
    limit: int | None,
) -> None:
    """Print stored SQLite data."""
    config = load_config(config_path, require_api_key=False)
    store = SessionStore(config.storage_path)
    payload = store.dump_data(session_id=session_id, limit=limit)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def run_data_export_memory_command(*, config_path: Path | None) -> None:
    """Print all stored long-term memory items as JSON."""
    config = load_config(config_path, require_api_key=False)
    store = SessionStore(config.storage_path)
    payload = store.export_memory_items()
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def run_identity_bind_owner_command(
    *,
    config_path: Path | None,
    channel: str,
    external_id: str,
) -> None:
    """Bind one external identity to the local owner."""
    config = load_config(config_path, require_api_key=False)
    store = SessionStore(config.storage_path)
    binding = store.bind_identity(
        channel=channel,
        external_id=external_id,
        user_id=OWNER_USER_ID,
        role="owner",
    )
    print(
        t(
            "identity.bind_owner.saved",
            channel=binding.channel,
            external_id=binding.external_id,
            user_id=binding.user_id,
        )
    )


def run_identity_list_command(*, config_path: Path | None) -> None:
    """Print stored canonical users and external identity bindings."""
    config = load_config(config_path, require_api_key=False)
    store = SessionStore(config.storage_path)
    payload = {
        "database_path": str(config.storage_path),
        "users": store.list_users(),
        "identities": store.list_identities(),
    }
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def _open_path(path: Path) -> None:
    """Open a path with the platform default application."""
    if os.name == "nt":
        os.startfile(path)  # type: ignore[attr-defined]
        return
    if sys.platform == "darwin":
        subprocess.Popen(["/usr/bin/open", str(path)])
        return
    subprocess.Popen(["/usr/bin/xdg-open", str(path)])


def _build_unique_provider_id(model_name: str, existing_ids: set[str]) -> str:
    candidate = model_name.strip() or "model"
    if candidate not in existing_ids:
        return candidate
    index = 2
    while f"{candidate}-{index}" in existing_ids:
        index += 1
    return f"{candidate}-{index}"


def _prompt_provider_preset() -> str:
    specs = list_provider_specs()
    print(t("config.add_model.supported"))
    for index, spec in enumerate(specs, start=1):
        print(
            t(
                "config.add_model.supported_item",
                index=index,
                label=spec.label,
                preset=spec.name,
                api_base=spec.default_base_url,
                model=spec.default_model,
            )
        )
    allowed = {spec.name: spec.name for spec in specs}
    while True:
        raw = _prompt_text(
            t("config.add_model.prompt.provider"),
            default=specs[0].name,
        )
        if raw in allowed:
            return allowed[raw]
        if raw.isdigit():
            index = int(raw)
            if 1 <= index <= len(specs):
                return specs[index - 1].name
        print(t("config.add_model.invalid_provider", value=raw))


def _prompt_text(
    label: str,
    *,
    default: str | None = None,
    secret: bool = False,
) -> str:
    if not sys.stdin.isatty():
        if default is not None:
            return default
        raise ValueError(t("config.add_model.non_interactive", field=label))

    while True:
        suffix = f" [{default}]" if default else ""
        prompt_text = f"{label}{suffix}: "
        value = (
            getpass.getpass(prompt_text)
            if secret
            else input(prompt_text)
        ).strip()
        if value:
            return value
        if default is not None:
            return default
        print(t("config.add_model.required", field=label))


def _prompt_yes_no(label: str, *, default: bool) -> bool:
    if not sys.stdin.isatty():
        return default

    hint = "Y/n" if default else "y/N"
    while True:
        raw = input(f"{label} [{hint}]: ").strip().lower()
        if not raw:
            return default
        if raw in {"y", "yes"}:
            return True
        if raw in {"n", "no"}:
            return False
        print(t("config.add_model.invalid_yes_no"))


def _run_one_turn(
    *,
    message: str,
    via: str,
    config: AppConfig | None,
    gateway_url: str | None,
    event_callback: RuntimeEventHandler | None = None,
) -> dict:
    inbound = InboundMessageContext(
        session_id="main",
        raw_body=message,
        body_for_agent=message.strip(),
        source_mode=f"cli_{via}",
        channel="cli",
        launch_cwd=resolve_launch_cwd(),
        entrypoint_path=resolve_entrypoint_path(),
        python_executable=resolve_python_executable(),
        actor_identity_status="owner",
        actor_user_id=OWNER_USER_ID,
        actor_role="owner",
        actor_external_id=OWNER_USER_ID,
        actor_is_owner=True,
    )
    if via == "local":
        return _run_local(inbound, config, event_callback=event_callback)
    if via == "gateway":
        return _run_via_gateway(inbound, gateway_url)
    try:
        return _run_via_gateway(inbound, gateway_url)
    except Exception as exc:
        if config is None:
            raise RuntimeError(t("agent.auto_no_fallback", error=exc)) from exc
        result = _run_local(inbound, config, event_callback=event_callback)
        result["meta"] = {"fallback": "local", "gateway_error": str(exc)}
        return result


def _run_local(
    inbound: InboundMessageContext,
    config: AppConfig | None,
    *,
    event_callback: RuntimeEventHandler | None = None,
) -> dict:
    if config is None:
        raise ValueError(t("agent.local_requires_config"))
    app = build_app(config)
    result = app.runtime.run_turn(inbound, event_callback=event_callback)
    return {
        "ok": True,
        "mode": "local",
        "session_id": inbound.session_id,
        "run_id": inbound.run_id,
        "message_id": inbound.message_id,
        "reply": result.reply,
        "iterations": result.iterations,
        "tool_events": result.tool_events,
        "runtime_events": result.runtime_events,
        "provider_id": result.provider_id,
        "provider_label": result.provider_label,
        "model_name": result.model_name,
    }


def _run_via_gateway(inbound: InboundMessageContext, gateway_url: str | None) -> dict:
    if not gateway_url:
        raise ValueError(t("agent.gateway_requires_url"))
    client = GatewayClient(gateway_url)
    response = client.run_agent(inbound)
    response["mode"] = "gateway"
    return response


def _load_local_config_if_needed(
    config_path: Path | None,
    via: str | None,
) -> AppConfig | None:
    if via == "gateway" and config_path is None:
        return None
    require_api_key = via != "gateway"
    return load_config(config_path, require_api_key=require_api_key)


def _resolve_cli_via(
    requested_via: str | None,
    config: AppConfig | None,
) -> str:
    via = requested_via or (config.cli_via if config else None) or "local"
    if via not in {"local", "gateway", "auto"}:
        raise ValueError(f"unsupported cli via mode: {via}")
    return via


def _resolve_gateway_url(
    *,
    config: AppConfig | None,
    explicit_url: str | None,
    explicit_host: str | None,
    explicit_port: int | None,
) -> str | None:
    if explicit_url:
        return explicit_url
    host = explicit_host or (config.gateway_host if config else None)
    port = explicit_port or (config.gateway_port if config else None)
    if explicit_host or explicit_port:
        if host and port:
            return f"http://{host}:{port}"
        return None
    if config and config.gateway_url:
        return config.gateway_url
    if host and port:
        return f"http://{host}:{port}"
    return None
