"""Interactive CLI for the /api/v2/agent/ws endpoint."""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import signal
import subprocess
import sys
import time
from typing import Any, Optional
from uuid import uuid4

from dotenv import load_dotenv
from rich.console import Console
from rich.prompt import Prompt

from cli.file_api_client import LocalAttachment, upload_attachments
from cli.renderer import AskPromptRequest, StreamRenderer
from cli.ws_client import AgentWsV3Client, WsClientConfig


DEFAULT_WS_URL = "ws://localhost:8334/api/v2/agent/ws"
DEFAULT_FILE_API_URL = "http://localhost:38888"
DEFAULT_USER_ID = "luckee_evaluation_user"
DEFAULT_WHEEL_SERVER_URL = "http://localhost:11113"
PACKAGE_NAME = "luckee_cli"


def _is_truthy_env(value: Optional[str]) -> bool:
    if value is None:
        return False
    return value.strip().lower() in {"1", "true", "yes", "on"}


def _config_indicates_openclaw_feishu(config_payload: Any) -> bool:
    if config_payload is None:
        return False

    if isinstance(config_payload, str):
        lowered = config_payload.lower()
        return (
            "openclaw" in lowered
            and ("feishu" in lowered or "lark" in lowered or "channel" in lowered)
        ) or "channel:feishu" in lowered

    if isinstance(config_payload, dict):
        for key, value in config_payload.items():
            key_l = str(key).lower()
            if key_l in {"channel", "source_channel", "platform", "source_platform"}:
                value_l = str(value).lower()
                if value_l in {"feishu", "lark", "openclaw_feishu"}:
                    return True
            if key_l in {"source", "client", "platform_name"} and str(value).lower() == "openclaw":
                return True
            if _config_indicates_openclaw_feishu(value):
                return True
        return False

    if isinstance(config_payload, (list, tuple)):
        return any(_config_indicates_openclaw_feishu(item) for item in config_payload)

    return False


def _should_hide_session_details(args: argparse.Namespace, config_payload: Any) -> bool:
    if args.hide_session_details:
        return True

    # Manual env override keeps behavior explicit when needed.
    if _is_truthy_env(os.getenv("AGENT_CLI_HIDE_SESSION_DETAILS")):
        return True

    # Auto-hide for OpenClaw Feishu/Lark channel if passed via config payload.
    if _config_indicates_openclaw_feishu(config_payload):
        return True

    # Auto-hide in Feishu/Lark environments when related env vars exist.
    known_markers = (
        "RUNNING_IN_FEISHU",
        "RUNNING_IN_LARK",
        "FEISHU_APP_ID",
        "LARK_APP_ID",
        "FEISHU_WEBHOOK",
        "LARK_WEBHOOK",
    )
    if any(os.getenv(key) for key in known_markers):
        return True

    return any(
        key.startswith(("FEISHU_", "LARK_")) and bool(os.getenv(key))
        for key in os.environ
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Agent Loop CLI for websocket streaming messages.",
    )
    parser.add_argument("--url", default=None, help="WebSocket endpoint URL.")
    parser.add_argument(
        "--token",
        default=None,
        help="Auth token. If omitted, LUCKEE_AUTH_TOKEN is used.",
    )
    parser.add_argument("--user-id", default=None, help="User ID.")
    parser.add_argument(
        "--thread-id",
        default=None,
        help="Thread ID. If omitted, a new websocket session is created.",
    )
    parser.add_argument(
        "--language",
        default=None,
        help="Language code (e.g. en, zh-CN, ja).",
    )
    parser.add_argument(
        "--config",
        default=None,
        help='JSON string sent in query payload config, e.g. \'{"lingxing_account":"123"}\'.',
    )
    parser.add_argument(
        "--system-prompt",
        default=None,
        help="Optional system_prompt sent in query payload.",
    )
    parser.add_argument(
        "--query",
        default=None,
        help="One-shot mode: send one query and exit after complete.",
    )
    parser.add_argument(
        "--scenario",
        default=None,
        help='Optional scenario sent in query payload (defaults to "keyword").',
    )
    parser.add_argument(
        "--file-api-url",
        default=None,
        help="File API base URL used to upload local attachments.",
    )
    parser.add_argument(
        "--attachment",
        action="append",
        default=None,
        help="Local file path to upload and attach (repeat for multiple files).",
    )
    parser.add_argument(
        "--attachments",
        default=None,
        help="Comma-separated local file paths, e.g. --attachments file1,file2",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=180,
        help="One-shot mode timeout in seconds (default: 180).",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Print raw websocket messages for both send and receive.",
    )
    parser.add_argument(
        "--hide-session-details",
        action="store_true",
        help="Hide endpoint/user_id/thread_id in the session banner.",
    )
    parser.add_argument(
        "--wheel-server-url",
        default=None,
        help="Base URL for wheel server used by self-upgrade.",
    )
    parser.add_argument(
        "--upgrade",
        action="store_true",
        help="Upgrade CLI from wheel server and exit.",
    )
    return parser.parse_args()


def load_runtime_defaults(args: argparse.Namespace) -> dict[str, Any]:
    load_dotenv()
    url = args.url or os.getenv("AGENT_CLI_URL") or DEFAULT_WS_URL
    token = (args.token or os.getenv("LUCKEE_AUTH_TOKEN") or "").strip() or None
    file_api_url = (
        args.file_api_url
        or os.getenv("AGENT_CLI_FILE_API_URL")
        or DEFAULT_FILE_API_URL
    )
    user_id = args.user_id or os.getenv("AGENT_CLI_USER_ID") or DEFAULT_USER_ID
    language = args.language or os.getenv("AGENT_CLI_LANGUAGE")
    thread_id = args.thread_id or os.getenv("AGENT_CLI_THREAD_ID") or str(uuid4())
    scenario = args.scenario or os.getenv("AGENT_CLI_SCENARIO") or "keyword"
    wheel_server_url = (
        args.wheel_server_url
        or os.getenv("AGENT_CLI_WHEEL_SERVER_URL")
        or DEFAULT_WHEEL_SERVER_URL
    )

    config_payload: Optional[dict[str, Any]] = None
    if args.config:
        config_payload = json.loads(args.config)

    return {
        "url": url,
        "token": token,
        "file_api_url": file_api_url,
        "user_id": user_id,
        "language": language,
        "thread_id": thread_id,
        "scenario": scenario,
        "config_payload": config_payload,
        "wheel_server_url": wheel_server_url.rstrip("/"),
    }


def _print_purchase_credit_hint(console: Console) -> None:
    console.print("[yellow]Purchase credit at:[/yellow] https://motse.ai")


def _looks_like_auth_error(detail: str) -> bool:
    lowered = detail.lower()
    auth_markers = (
        "401",
        "invalidstatus",
        "invalid status",
        "usertoken",
        "token",
        "unauthorized",
        "未登录",
        "已失效",
    )
    return any(marker in lowered for marker in auth_markers)


def _self_upgrade(wheel_server_url: str, console: Console) -> None:
    cmd = [
        sys.executable,
        "-m",
        "pip",
        "install",
        "--upgrade",
        "--no-cache-dir",
        "--find-links",
        wheel_server_url,
        PACKAGE_NAME,
    ]
    console.print(
        f"[cyan]Upgrading {PACKAGE_NAME} from[/cyan] [bold]{wheel_server_url}[/bold]"
    )
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        stderr = (result.stderr or "").strip()
        stdout = (result.stdout or "").strip()
        detail = stderr or stdout or "pip install failed"
        raise RuntimeError(detail)
    console.print("[bold green]Upgrade complete.[/bold green]")
    console.print("[dim]Restart the CLI to use the newest version.[/dim]")


async def prompt_and_answer(
    console: Console,
    ask_request: AskPromptRequest,
    client: AgentWsV3Client,
) -> None:
    answers: dict[str, str] = {}
    console.print(f"[bold cyan]Agent asked for input[/bold cyan] id={ask_request.message_id}")
    for q in ask_request.questions:
        question_text = str(q.get("question") or "")
        options = q.get("options") or []
        labels = [str(opt.get("label")) for opt in options if opt.get("label")]
        if labels:
            console.print(f"\n[bold]{question_text}[/bold]")
            for idx, label in enumerate(labels, 1):
                console.print(f"  {idx}. {label}")

            user_input = Prompt.ask("Your answer", default="1").strip()
            if not user_input:
                user_input = "1"

            # Keep input flexible: raw text is allowed; valid option numbers map to labels.
            if user_input.isdigit():
                selected_idx = int(user_input)
                if 1 <= selected_idx <= len(labels):
                    answers[question_text] = labels[selected_idx - 1]
                else:
                    answers[question_text] = user_input
            else:
                answers[question_text] = user_input
        else:
            answers[question_text] = Prompt.ask(question_text)

    await client.send_answer(message_id=ask_request.message_id, answers=answers)


async def run_cli(args: argparse.Namespace) -> None:
    cfg = load_runtime_defaults(args)
    console = Console()
    if args.upgrade:
        try:
            _self_upgrade(cfg["wheel_server_url"], console)
        except Exception as exc:
            detail = str(exc) or exc.__class__.__name__
            console.print(f"[red]upgrade failed:[/red] {detail}")
            raise SystemExit(1) from exc
        raise SystemExit(0)

    if not cfg["token"]:
        console.print(
            "[red]Missing auth token.[/red] Provide [bold]--token[/bold] or set [bold]LUCKEE_AUTH_TOKEN[/bold]."
        )
        _print_purchase_credit_hint(console)
        raise SystemExit(1)

    hide_session_details = _should_hide_session_details(args, cfg.get("config_payload"))
    parsed_csv_attachments: list[str] = []
    if args.attachments:
        parsed_csv_attachments = [
            item.strip() for item in args.attachments.split(",") if item.strip()
        ]
    all_attachment_paths = [*(args.attachment or []), *parsed_csv_attachments]
    renderer = StreamRenderer(console=console)
    stop_event = asyncio.Event()
    query_complete_event = asyncio.Event()
    last_sigint_ts = 0.0
    pending_attachments: list[LocalAttachment] = [
        LocalAttachment(path=p) for p in all_attachment_paths
    ]

    client = AgentWsV3Client(
        WsClientConfig(
            base_url=cfg["url"],
            user_id=cfg["user_id"],
            user_token=cfg["token"],
            thread_id=cfg["thread_id"],
            user_language=cfg["language"],
            debug=args.debug,
        )
    )

    def _handle_sigint() -> None:
        nonlocal last_sigint_ts
        now = time.time()
        if now - last_sigint_ts < 2.0:
            console.print("\n[yellow]Exiting (double Ctrl+C)[/yellow]")
            stop_event.set()
            return
        last_sigint_ts = now
        console.print("\n[yellow]Interrupt sent. Press Ctrl+C again to exit.[/yellow]")
        asyncio.create_task(client.send_interrupt())

    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT,):
        try:
            loop.add_signal_handler(sig, _handle_sigint)
        except NotImplementedError:  # pragma: no cover - platform dependent
            pass

    try:
        await client.connect()
    except Exception as exc:
        detail = str(exc) or exc.__class__.__name__
        if _looks_like_auth_error(detail):
            console.print(
                "[red]Authentication failed: token is missing, invalid, or expired.[/red]"
            )
            _print_purchase_credit_hint(console)
        else:
            console.print(f"[red]Failed to connect:[/red] {detail}")
        raise SystemExit(1) from exc
    renderer.render_banner(
        endpoint=cfg["url"],
        user_id=cfg["user_id"],
        thread_id=client.thread_id,
        hide_session_details=hide_session_details,
    )

    async def build_query_attachments() -> list[dict[str, Any]]:
        if not pending_attachments:
            return []
        thread_id = await client.wait_for_thread_id(timeout_seconds=10.0)
        console.print(
            f"[dim]uploading {len(pending_attachments)} attachment(s) "
            f"(session_id={thread_id}) via {cfg['file_api_url']}[/dim]"
        )
        uploaded = await upload_attachments(
            base_url=cfg["file_api_url"],
            user_id=cfg["user_id"],
            thread_id=thread_id,
            attachments=pending_attachments,
            debug=args.debug,
        )
        console.print(
            f"[green]uploaded {len(uploaded)} attachment(s) (session_id={thread_id})[/green]"
        )
        return uploaded

    async def receive_loop() -> None:
        while not stop_event.is_set():
            msg = await client.incoming_queue.get()
            msg_type = msg.get("type")
            if msg_type == "metadata":
                thread_id = str(msg.get("thread_id") or "")
                if thread_id:
                    client.set_thread_id(thread_id)

            ask_request = renderer.render_message(msg)
            if ask_request is not None:
                await prompt_and_answer(console, ask_request, client)

            if msg_type in {"complete", "terminated", "_connection_error"}:
                query_complete_event.set()

            if msg_type == "_connection_error":
                detail = str(msg.get("error_message") or "connection error")
                if _looks_like_auth_error(detail):
                    console.print(
                        "[red]Authentication failed: token is missing, invalid, or expired.[/red]"
                    )
                    _print_purchase_credit_hint(console)
                else:
                    console.print(f"[red]Connection closed unexpectedly:[/red] {detail}")
                stop_event.set()
                return

    async def input_loop() -> None:
        while not stop_event.is_set():
            raw = (await asyncio.to_thread(input, "\n> ")).strip()
            if not raw:
                continue

            if raw in {"/quit", "/exit"}:
                stop_event.set()
                break

            if raw == "/help":
                renderer.render_help()
                continue

            if raw == "/upgrade":
                try:
                    await asyncio.to_thread(_self_upgrade, cfg["wheel_server_url"], console)
                except Exception as exc:
                    detail = str(exc) or exc.__class__.__name__
                    console.print(f"[red]upgrade failed:[/red] {detail}")
                continue

            if raw == "/thread":
                value = client.thread_id or "(not yet assigned)"
                console.print(f"[dim]thread_id={value}[/dim]")
                continue

            if raw == "/thinking":
                renderer.render_thinking_history()
                continue

            if raw.startswith("/thinking "):
                thinking_id = raw[len("/thinking ") :].strip()
                if not thinking_id:
                    console.print("[red]Usage: /thinking <message_id|prefix>[/red]")
                    continue
                renderer.render_thinking_history(thinking_id)
                continue

            if raw == "/clear":
                console.clear()
                renderer.render_banner(
                    endpoint=cfg["url"],
                    user_id=cfg["user_id"],
                    thread_id=client.thread_id,
                    hide_session_details=hide_session_details,
                )
                continue

            if raw == "/interrupt":
                await client.send_interrupt()
                continue

            if raw == "/new":
                await client.reconnect(new_thread=True)
                renderer.render_banner(
                    endpoint=cfg["url"],
                    user_id=cfg["user_id"],
                    thread_id=client.thread_id,
                    hide_session_details=hide_session_details,
                )
                continue

            if raw.startswith("/attach "):
                path = raw[len("/attach ") :].strip()
                if not path:
                    console.print("[red]Usage: /attach <local_file_path>[/red]")
                    continue
                pending_attachments.append(LocalAttachment(path=path))
                console.print(
                    f"[green]queued attachment[/green] #{len(pending_attachments)}: {path}"
                )
                continue

            if raw == "/attachments":
                if not pending_attachments:
                    console.print("[dim]no queued attachments[/dim]")
                    continue
                console.print("[bold cyan]Queued attachments[/bold cyan]")
                for idx, item in enumerate(pending_attachments, 1):
                    console.print(f"  {idx}. {item.path}")
                continue

            if raw.startswith("/detach "):
                token = raw[len("/detach ") :].strip().lower()
                if token == "all":
                    pending_attachments.clear()
                    console.print("[green]cleared queued attachments[/green]")
                    continue
                try:
                    remove_idx = int(token)
                except ValueError:
                    console.print("[red]Usage: /detach <index|all>[/red]")
                    continue
                if remove_idx < 1 or remove_idx > len(pending_attachments):
                    console.print("[red]attachment index out of range[/red]")
                    continue
                removed = pending_attachments.pop(remove_idx - 1)
                console.print(f"[green]removed attachment[/green] {removed.path}")
                continue

            query_complete_event.clear()
            try:
                attachments = await build_query_attachments()
            except Exception as exc:
                detail = str(exc) or exc.__class__.__name__
                console.print(f"[red]failed to upload attachments:[/red] {detail}")
                continue
            await client.send_query(
                raw,
                attachments=attachments,
                scenario=cfg["scenario"],
                config=cfg["config_payload"],
                system_prompt=args.system_prompt,
                language=cfg["language"],
            )
            pending_attachments.clear()

    recv_task = asyncio.create_task(receive_loop())

    try:
        if args.query:
            query_complete_event.clear()
            try:
                attachments = await build_query_attachments()
            except Exception as exc:
                detail = str(exc) or exc.__class__.__name__
                console.print(f"[red]failed to upload attachments:[/red] {detail}")
                stop_event.set()
                return
            await client.send_query(
                args.query,
                attachments=attachments,
                scenario=cfg["scenario"],
                config=cfg["config_payload"],
                system_prompt=args.system_prompt,
                language=cfg["language"],
            )
            try:
                await asyncio.wait_for(query_complete_event.wait(), timeout=args.timeout)
            except asyncio.TimeoutError:
                console.print(f"[red]Timed out waiting for complete ({args.timeout}s)[/red]")
            stop_event.set()
        else:
            input_task = asyncio.create_task(input_loop())
            await stop_event.wait()
            if not input_task.done():
                input_task.cancel()
                try:
                    await input_task
                except asyncio.CancelledError:
                    pass
    finally:
        if not recv_task.done():
            recv_task.cancel()
            try:
                await recv_task
            except asyncio.CancelledError:
                pass
        await client.close(send_terminate=True)


def main() -> None:
    args = parse_args()
    try:
        asyncio.run(run_cli(args))
    except KeyboardInterrupt:
        # Fallback for platforms where signal handlers aren't installed.
        pass


if __name__ == "__main__":
    main()

