"""Interactive terminal client for the NemlAI CLI API.

Run with ``nemlai`` after ``pip install nemlai``, or directly:
``python -m nemlai``
"""

from __future__ import annotations

import getpass
import itertools
import os
import re as _re
try:
    import readline  # noqa: F401 -- imported for side-effect (line editing)
except ImportError:
    pass  # Windows: readline not available
import shutil
import sys
import threading
import time
from pathlib import Path

import httpx

from nemlai.client import parse_response


def _check_for_update() -> str | None:
    """Check PyPI for a newer version. Returns update message or None."""
    try:
        from nemlai import __version__
        resp = httpx.get("https://pypi.org/pypi/nemlai/json", timeout=3)
        resp.raise_for_status()
        latest = resp.json()["info"]["version"]
        if latest != __version__:
            return (
                f"Update available: {__version__} -> {latest}\n"
                f"  pip install --upgrade nemlai"
            )
    except Exception:
        pass
    return None


_update_message: str | None = None


def _start_update_check() -> None:
    """Run update check in background thread so it doesn't block startup."""
    def _check():
        global _update_message
        _update_message = _check_for_update()
    thread = threading.Thread(target=_check, daemon=True)
    thread.start()


def _sanitize_output(text: str) -> str:
    """Strip ANSI/terminal escape sequences from server response."""
    text = _re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', text)           # CSI sequences
    text = _re.sub(r'\x1b\].*?(?:\x07|\x1b\\)', '', text)        # OSC sequences
    text = _re.sub(r'\x1b[P_^].*?(?:\x1b\\|\x9c)', '', text)     # DCS/APC/PM sequences
    text = _re.sub(r'\x1b[\x20-\x2f]*[\x30-\x7e]', '', text)     # Fe sequences
    text = _re.sub(r'[\x80-\x9f]', '', text)                      # C1 control codes
    text = _re.sub(r'[\r\x08\x0b\x0c]', '', text)                # CR, backspace, VT, FF
    return text

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DEFAULT_BASE_URL = "https://nemlai.fly.dev"
CONFIG_DIR = Path.home() / ".config" / "nemlai"
TOKEN_FILE = CONFIG_DIR / "token"
BASE_URL_FILE = CONFIG_DIR / "base_url"
HISTORY_FILE = CONFIG_DIR / "history"

BANNER = r"""
  _   _                _    _    ___    ____ _     ___
 | \ | | ___ _ __ ___ | |  / \  |_ _|  / ___| |   |_ _|
 |  \| |/ _ \ '_ ` _ \| | / _ \  | |  | |   | |    | |
 | |\  |  __/ | | | | | |/ ___ \ | |  | |___| |___ | |
 |_| \_|\___|_| |_| |_|_/_/   \_\___|  \____|_____|___|
"""

COMMANDS = [
    "help", "login", "signup", "logout", "whoami",
    "basket status", "basket generate", "basket items",
    "basket add", "basket add-many", "basket remove", "basket clear", "basket adjust",
    "basket send", "basket search", "basket why",
    "yolosearch",
    "dish", "dish add", "dish add all", "dish info", "dish recipe",
    "why",
    "orders refresh", "orders refresh-member",
    "household info",
    "settings show", "settings update",
    "preferences show", "preferences update",
    "profile update",
    "password change", "password reset-nemlig",
    "account delete",
    "notifications status", "notifications test",
    "achievements", "version", "guide",
    "categories list", "categories",
]

# ANSI colours
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
CYAN = "\033[1;36m"
DIM = "\033[90m"
BOLD = "\033[1m"
RESET = "\033[0m"


# ---------------------------------------------------------------------------
# Config helpers
# ---------------------------------------------------------------------------


def _ensure_config_dir() -> None:
    CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
    # Re-enforce permissions on existing directory
    if CONFIG_DIR.stat().st_mode & 0o077:
        CONFIG_DIR.chmod(0o700)


def _save_token(token: str) -> None:
    _ensure_config_dir()
    import os as _os
    fd = _os.open(str(TOKEN_FILE), _os.O_WRONLY | _os.O_CREAT | _os.O_TRUNC, 0o600)
    try:
        _os.write(fd, token.encode())
    finally:
        _os.close(fd)


def _load_token() -> str:
    if TOKEN_FILE.exists():
        return TOKEN_FILE.read_text().strip()
    return os.environ.get("NEMLAI_TOKEN", "")


def _clear_token() -> None:
    if TOKEN_FILE.exists():
        TOKEN_FILE.unlink()


def _get_base_url() -> str:
    if BASE_URL_FILE.exists():
        return BASE_URL_FILE.read_text().strip()
    return os.environ.get("NEMLAI_URL", DEFAULT_BASE_URL)


def _save_base_url(url: str) -> None:
    _ensure_config_dir()
    import os as _os
    fd = _os.open(str(BASE_URL_FILE), _os.O_WRONLY | _os.O_CREAT | _os.O_TRUNC, 0o600)
    try:
        _os.write(fd, url.encode())
    finally:
        _os.close(fd)


# ---------------------------------------------------------------------------
# Tab completion
# ---------------------------------------------------------------------------


_has_readline = "readline" in sys.modules


def _completer(text: str, state: int) -> str | None:
    line = readline.get_line_buffer()
    matches = [c for c in COMMANDS if c.startswith(line)]
    if state < len(matches):
        # Return the part after what's already typed
        return matches[state][len(line) - len(text) :]  # noqa: E203
    return None


def _setup_readline() -> None:
    if not _has_readline:
        return
    readline.set_completer(_completer)
    readline.parse_and_bind("tab: complete")
    readline.set_completer_delims("")
    # Load history
    _ensure_config_dir()
    if HISTORY_FILE.exists():
        try:
            readline.read_history_file(str(HISTORY_FILE))
        except Exception:
            pass
    readline.set_history_length(500)


def _save_readline_history() -> None:
    if not _has_readline:
        return
    try:
        # Filter out commands containing sensitive data
        _sensitive_patterns = ["--password", "--current", "--new-password", "--token"]
        for i in range(readline.get_current_history_length(), 0, -1):
            item = readline.get_history_item(i)
            if item and any(p in item for p in _sensitive_patterns):
                readline.remove_history_item(i - 1)
        readline.write_history_file(str(HISTORY_FILE))
        HISTORY_FILE.chmod(0o600)
    except Exception:
        pass


# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------


def _make_client(token: str, base_url: str) -> httpx.Client:
    from urllib.parse import urlparse

    if not base_url.startswith("https://"):
        parsed = urlparse(base_url)
        if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
            print(f"{RED}Error: Refusing to connect over non-HTTPS to {base_url}{RESET}", file=sys.stderr)
            print("Use https:// or connect to localhost for development.", file=sys.stderr)
            sys.exit(1)
    headers = {}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    # 10 min timeout — orders refresh uses Selenium and can be slow
    return httpx.Client(base_url=base_url, headers=headers, timeout=600.0)


def _send_command(client: httpx.Client, command: str, extra_json: dict | None = None) -> str:
    """Send a command to /cli/api/command and return the raw response text."""
    payload: dict = {"command": command}
    if extra_json:
        payload.update(extra_json)
    try:
        resp = client.post("/cli/api/command", json=payload)
        return resp.text
    except httpx.ConnectError:
        return f"STATUS: error\nERROR Cannot connect to {client.base_url}\nHINT Check your internet connection or try: nemlai --url <base_url>"
    except httpx.TimeoutException:
        return "STATUS: error\nERROR Request timed out"
    except Exception:
        return "STATUS: error\nERROR Connection error"


# ---------------------------------------------------------------------------
# Progress spinner for slow commands
# ---------------------------------------------------------------------------

# Phases shown to the user while waiting (elapsed_seconds, message)
_SLOW_PHASES: dict[str, list[tuple[int, str]]] = {
    "orders refresh": [
        (0, "Logging into Nemlig.com..."),
        (8, "Fetching order history..."),
        (25, "Checking for new orders..."),
        (40, "Uploading to database..."),
        (90, "Still working (Selenium can be slow)..."),
    ],
    "orders refresh-member": [
        (0, "Logging into Nemlig.com..."),
        (8, "Fetching order history..."),
        (25, "Checking for new orders..."),
        (40, "Uploading to database..."),
        (90, "Still working (Selenium can be slow)..."),
    ],
    "basket generate": [
        (0, "Refreshing orders..."),
        (10, "Running ML prediction..."),
        (30, "Building basket..."),
        (50, "Resolving product prices..."),
        (90, "Still working..."),
    ],
    "basket send": [
        (0, "Building basket..."),
        (5, "Adding items to nemlig.com..."),
        (30, "Verifying basket..."),
        (60, "Almost done..."),
    ],
    "search": [
        (0, "Searching..."),
    ],
    "dish": [
        (0, "Sending dish to AI chef..."),
        (10, "Extracting ingredients..."),
        (30, "Searching Nemlig catalogue..."),
        (60, "Matching products..."),
        (90, "Still working..."),
    ],
    "signup": [
        (0, "Creating account..."),
        (10, "Logging into Nemlig.com..."),
        (40, "Fetching order history..."),
        (90, "Almost done..."),
    ],
}

_SPINNER_CHARS = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"

_SLOW_HINTS: dict[str, str] = {
    "orders refresh": "Fetching orders. This typically takes 1-2 minutes.",
    "orders refresh-member": "Fetching orders. This typically takes 1-2 minutes.",
    "basket generate": "Generating basket. This typically takes 1-3 minutes.",
    "basket send": "Sending basket to nemlig.com. Typically takes about 1 minute.",
    "dish": "Searching for dish ingredients. This typically takes 1-2 minutes.",
}


def _is_slow_command(cmd: str) -> bool:
    """Check if a command should show a progress spinner."""
    return any(cmd.startswith(prefix) for prefix in _SLOW_PHASES)


def _get_phase(cmd: str, elapsed: float) -> str:
    """Get the current phase message based on elapsed time."""
    for prefix, phases in _SLOW_PHASES.items():
        if cmd.startswith(prefix):
            msg = phases[0][1]
            for threshold, phase_msg in phases:
                if elapsed >= threshold:
                    msg = phase_msg
            return msg
    return "Working..."


def _run_with_spinner(
    func: callable,
    phase_key: str,
    hint: str | None = None,
) -> str:
    """Run *func* in a background thread with an animated progress spinner.

    *func* must be a zero-arg callable that returns a ``str``.
    *phase_key* selects spinner phases from ``_SLOW_PHASES``.
    """
    if hint is None:
        hint = _SLOW_HINTS.get(phase_key)
    if hint:
        print(f"{DIM}{hint}{RESET}")

    result: list[str | None] = [None]

    def _worker() -> None:
        result[0] = func()

    thread = threading.Thread(target=_worker, daemon=True)
    thread.start()

    spinner = itertools.cycle(_SPINNER_CHARS)
    start = time.monotonic()

    while thread.is_alive():
        elapsed = time.monotonic() - start
        phase = _get_phase(phase_key, elapsed)
        frame = next(spinner)
        elapsed_str = f"{int(elapsed)}s"
        line = f"\r{YELLOW}{frame} {phase}  {DIM}({elapsed_str}){RESET}"
        sys.stdout.write(f"{line}\033[K")
        sys.stdout.flush()
        thread.join(timeout=0.1)

    # Clear the spinner line
    sys.stdout.write(f"\r\033[K")
    sys.stdout.flush()

    return result[0] or "STATUS: error\nERROR No response from server"


def _send_command_with_progress(client: httpx.Client, command: str, extra_json: dict | None = None) -> str:
    """Send a command with an animated progress spinner for slow operations."""
    if not _is_slow_command(command):
        return _send_command(client, command, extra_json=extra_json)

    return _run_with_spinner(
        lambda: _send_command(client, command, extra_json=extra_json),
        phase_key=command,
    )


def _send_orders_refresh(
    client: httpx.Client,
    command: str,
    *,
    password: str,
    email: str | None = None,
) -> str:
    """Send orders refresh via its dedicated endpoint (not /cli/api/command).

    The generic ``CommandBody`` only accepts the ``command`` field — extra
    fields like ``password`` are silently dropped by Pydantic.  This helper
    routes through ``/cli/api/orders/refresh`` instead.
    """
    payload: dict = {"password": password}
    if email:
        payload["email"] = email

    def _do() -> str:
        try:
            resp = client.post("/cli/api/orders/refresh", json=payload)
            return resp.text
        except httpx.ConnectError:
            return (
                f"STATUS: error\nERROR Cannot connect to {client.base_url}\n"
                "HINT Check your internet connection or try: nemlai --url <base_url>"
            )
        except httpx.TimeoutException:
            return "STATUS: error\nERROR Request timed out"
        except Exception:
            return "STATUS: error\nERROR Connection error"

    return _run_with_spinner(_do, phase_key=command)


# ---------------------------------------------------------------------------
# Command argument parsing
# ---------------------------------------------------------------------------


def _parse_cmd_kwargs(cmd: str) -> dict[str, str]:
    """Extract ``--key value`` pairs from a command string."""
    import shlex
    result: dict[str, str] = {}
    try:
        parts = shlex.split(cmd)
    except ValueError:
        return result
    i = 0
    while i < len(parts):
        if parts[i].startswith("--") and i + 1 < len(parts) and not parts[i + 1].startswith("--"):
            key = parts[i][2:]  # strip leading --
            result[key] = parts[i + 1]
            i += 2
        else:
            i += 1
    return result


def _extract_token_and_save(text: str) -> str:
    """Extract TOKEN from response text, save it, return the token (or "")."""
    status, body = parse_response(text)
    if status != "ok":
        return ""
    for line in body.splitlines():
        if line.startswith("TOKEN "):
            token = line[len("TOKEN "):].strip()
            if token:
                _save_token(token)
            return token
    return ""


# ---------------------------------------------------------------------------
# Non-interactive auth (for --email/--password one-shot commands)
# ---------------------------------------------------------------------------


def _noninteractive_auth(client: httpx.Client, endpoint: str, payload: dict) -> str:
    """POST to an auth endpoint and return the raw response text."""
    try:
        resp = client.post(endpoint, json=payload)
        return resp.text
    except httpx.ConnectError:
        return "STATUS: error\nERROR Cannot connect to server."
    except Exception as e:
        return f"STATUS: error\nERROR {e}"


# ---------------------------------------------------------------------------
# Interactive login (collects email/password via prompts)
# ---------------------------------------------------------------------------


def _interactive_login(client: httpx.Client, base_url: str) -> tuple[httpx.Client, str]:
    """Prompt for email and password, log in, save token, return new client."""
    print(f"{YELLOW}Email:{RESET} ", end="", flush=True)
    email = input()
    password = getpass.getpass(f"{YELLOW}Password:{RESET} ")

    try:
        resp = client.post(
            "/cli/api/auth/login",
            json={"email": email, "password": password},
        )
    except httpx.ConnectError:
        print(f"{RED}Cannot connect to server.{RESET}")
        return client, ""
    except Exception as e:
        print(f"{RED}Error: {e}{RESET}")
        return client, ""

    text = resp.text
    status, body = parse_response(text)

    if status == "ok":
        # Extract token
        token = ""
        for line in body.splitlines():
            if line.startswith("TOKEN "):
                token = line[len("TOKEN "):].strip()
                break
        if token:
            _save_token(token)
            # Rebuild client with new token
            client.close()
            client = _make_client(token, base_url)
            print(f"{GREEN}Logged in successfully. Token saved to ~/.config/nemlai/token{RESET}")
        else:
            _print_response(text)
    else:
        _print_response(text)

    return client, _load_token()


def _interactive_signup(client: httpx.Client) -> None:
    """Prompt for signup fields and create account with progress spinner."""
    print(f"{BOLD}Create a NemlAI account{RESET}\n")
    print(f"{BOLD}This will create a new NemlAI account and link it to your Nemlig.com account.{RESET}\n")
    print(f"{BOLD}Creating a NemlAI will allow you to interact with nemlig.com through CLI and auto-generate a basket based on your order history. You or AI agent can prepare everything from here, then 'send the cart to nemlig.com', so that the cart is ready across all your devices.{RESET}\n")
    print(f"{YELLOW}Display name:{RESET} ", end="", flush=True)
    name = input()
    while True:
        print(f"{YELLOW}Email (for your NemlAI account):{RESET} ", end="", flush=True)
        email = input()
        if _re.match(r"[^@]+@[^@]+\.[^@]+", email):
            break
        print(f"{RED}Please enter a valid email address.{RESET}")
    password = getpass.getpass(f"{YELLOW}Password (for your NemlAI account):{RESET} ")
    print(f"{DIM}This is the email you use to log in to Nemlig.com.{RESET}")
    while True:
        print(f"{YELLOW}Nemlig.com email:{RESET} ", end="", flush=True)
        nemlig_email = input()
        if _re.match(r"[^@]+@[^@]+\.[^@]+", nemlig_email):
            break
        print(f"{RED}Please enter a valid email address.{RESET}")
    print(f"{DIM}Your Nemlig.com password will only be used once to fetch your orders.{RESET}")
    print(f"{DIM}It will not be saved and it will be deleted right after authentication.{RESET}")
    nemlig_password = getpass.getpass(f"{YELLOW}Nemlig.com password:{RESET} ")
    print(f"{YELLOW}Invite code (optional):{RESET} ", end="", flush=True)
    invite_code = input()

    payload = {
        "name": name,
        "email": email,
        "password": password,
        "nemlig_email": nemlig_email,
        "nemlig_password": nemlig_password,
        "invite_code": invite_code or None,
    }

    def _do() -> str:
        try:
            resp = client.post("/cli/api/auth/signup", json=payload)
            return resp.text
        except httpx.ConnectError:
            return "STATUS: error\nERROR Cannot connect to server."
        except Exception as e:
            return f"STATUS: error\nERROR {e}"

    text = _run_with_spinner(
        _do,
        phase_key="signup",
        hint="Signup takes 2-4 minutes (logging into Nemlig + fetching orders).",
    )
    _print_response(text)


# ---------------------------------------------------------------------------
# Single-command dispatch
# ---------------------------------------------------------------------------


def _dispatch_single_command(client: httpx.Client, cmd: str, json_mode: bool = False) -> str:
    """Route a single CLI command to the correct endpoint.

    Handles login, signup, and orders refresh specially so that their
    payload reaches the dedicated endpoints (not the generic /cli/api/command).

    When *json_mode* is True and a v2 endpoint exists for the command,
    calls the v2 endpoint directly and returns structured JSON.
    """
    kw = _parse_cmd_kwargs(cmd)

    # In JSON mode, route supported commands to v2 endpoints
    if json_mode:
        v2_result = _try_v2_dispatch(client, cmd, kw)
        if v2_result is not None:
            return v2_result

    # login --email x --password y
    if cmd.startswith("login"):
        if kw.get("email") and kw.get("password"):
            text = _noninteractive_auth(client, "/cli/api/auth/login", {
                "email": kw["email"],
                "password": kw["password"],
            })
            _extract_token_and_save(text)
            return text
        return _send_command(client, cmd)

    # signup --name n --email e --password p --nemlig-email ne --nemlig-password np
    if cmd.startswith("signup"):
        if kw.get("email") and kw.get("password") and kw.get("nemlig-email") and kw.get("nemlig-password"):
            payload = {
                "name": kw.get("name", ""),
                "email": kw["email"],
                "password": kw["password"],
                "nemlig_email": kw["nemlig-email"],
                "nemlig_password": kw["nemlig-password"],
                "invite_code": kw.get("invite-code"),
            }
            text = _noninteractive_auth(client, "/cli/api/auth/signup", payload)
            return text
        return _send_command(client, cmd)

    # orders refresh --password p / orders refresh-member --email e --password p
    if cmd.startswith("orders refresh"):
        pw = kw.get("password") or os.environ.get("NEMLIG_PASSWORD")
        if pw:
            email = kw.get("email")
            phase_key = "orders refresh-member" if "refresh-member" in cmd else "orders refresh"
            return _send_orders_refresh(client, phase_key, password=pw, email=email)
        return _send_command(client, cmd)

    # Everything else — use the standard path
    return _send_command_with_progress(client, cmd)


def _try_v2_dispatch(client: httpx.Client, cmd: str, kw: dict[str, str]) -> str | None:
    """Try to dispatch a command via v2 JSON endpoints.

    Returns a JSON string if a v2 endpoint handled the command, or None
    to fall through to the v1 path.
    """
    import json

    def _v2_request(method: str, path: str, payload: dict | None = None) -> str:
        """Send a v2 request and return the JSON result string."""
        try:
            if method == "GET":
                resp = client.get(path)
            else:
                resp = client.post(path, json=payload)

            if resp.status_code == 429:
                retry_after = resp.headers.get("retry-after")
                delay = min(float(retry_after), 30.0) if retry_after else 2.0
                print(f"Rate limited, retrying in {delay:.0f}s...", file=sys.stderr)
                time.sleep(delay)
                if method == "GET":
                    resp = client.get(path)
                else:
                    resp = client.post(path, json=payload)

            data = resp.json()
            if resp.status_code >= 400:
                return json.dumps({"status": "error", "error": data.get("detail", str(data))}, ensure_ascii=False)
            data["status"] = "ok"
            return json.dumps(data, ensure_ascii=False)
        except httpx.ConnectError:
            return json.dumps({"status": "error", "error": f"Cannot connect to {client.base_url}"})
        except Exception as e:
            return json.dumps({"status": "error", "error": str(e)})

    # search <query>
    if cmd.startswith("search") and not cmd.startswith("search info"):
        query = kw.get("query") or cmd.replace("search", "", 1).replace("basket search", "", 1).strip()
        query = query.lstrip("-").strip()
        if query:
            return _v2_request("POST", "/cli/api/v2/search", {"query": query, "take": int(kw.get("limit", "10"))})

    # basket search <query>
    if cmd.startswith("basket search"):
        query = kw.get("query") or cmd.replace("basket search", "", 1).strip()
        query = query.lstrip("-").strip()
        if query:
            return _v2_request("POST", "/cli/api/v2/search", {"query": query, "take": int(kw.get("limit", "10"))})

    # basket items
    if cmd.strip() == "basket items":
        return _v2_request("GET", "/cli/api/v2/basket/items")

    # basket add --query <q> or basket add --product-number <pn>
    if cmd.startswith("basket add") and "add-many" not in cmd:
        pn = kw.get("product-number")
        query = kw.get("query") or cmd.replace("basket add", "", 1).strip()
        qty = int(kw.get("quantity", "1"))
        payload: dict = {"quantity": qty}
        if pn:
            payload["product_number"] = pn
        elif query:
            payload["query"] = query
        else:
            return None
        return _v2_request("POST", "/cli/api/v2/basket/add", payload)

    # basket remove --product-number <pn> or basket remove --item <idx>
    if cmd.startswith("basket remove"):
        pn = kw.get("product-number")
        if pn:
            return _v2_request("POST", "/cli/api/v2/basket/remove", {"product_number": pn})
        # Fall through to v1 for index-based removal
        return None

    # basket adjust --product-number <pn> --quantity <q>
    if cmd.startswith("basket adjust"):
        pn = kw.get("product-number")
        qty_str = kw.get("quantity")
        if pn and qty_str:
            return _v2_request("POST", "/cli/api/v2/basket/adjust", {"product_number": pn, "quantity": int(qty_str)})
        return None

    # basket add-many --items <json>
    if cmd.startswith("basket add-many"):
        items_json = kw.get("items")
        items_file = kw.get("file")
        if items_file:
            try:
                items_data = json.loads(Path(items_file).read_text())
            except Exception as e:
                return json.dumps({"status": "error", "error": f"Failed to read {items_file}: {e}"})
        elif items_json:
            try:
                items_data = json.loads(items_json)
            except json.JSONDecodeError as e:
                return json.dumps({"status": "error", "error": f"Invalid JSON: {e}"})
        else:
            return json.dumps({"status": "error", "error": "Provide --items or --file"})
        return _v2_request("POST", "/cli/api/v2/basket/add-batch", {"items": items_data})

    # categories list
    if cmd.strip() in ("categories list", "categories"):
        return _v2_request("GET", "/cli/api/v2/categories")

    return None


# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------


def _print_json_response(text: str) -> None:
    """Print a response as structured JSON.

    If the text is already valid JSON (from a v2 endpoint), print as-is.
    Otherwise, parse the v1 STATUS format and wrap it.
    """
    import json

    # Check if this is already structured JSON from a v2 endpoint
    stripped = text.strip()
    if stripped.startswith("{"):
        try:
            data = json.loads(stripped)
            # Already structured — print directly
            print(json.dumps(data, ensure_ascii=False))
            return
        except json.JSONDecodeError:
            pass  # Fall through to v1 parsing

    status, body = parse_response(text)
    result: dict = {"status": status}
    for line in body.splitlines():
        if line.startswith("ERROR "):
            result["error"] = line[len("ERROR "):]
        elif line.startswith("HINT "):
            result["hint"] = line[len("HINT "):]
        elif line.startswith("TOKEN "):
            result["token"] = line[len("TOKEN "):]
    if "error" not in result and "token" not in result:
        result["message"] = body.strip()
    print(json.dumps(result, ensure_ascii=False))


def _print_response(text: str) -> None:
    """Print a response with colour-coded STATUS line."""
    text = _sanitize_output(text)
    lines = text.split("\n")
    for line in lines:
        if line.startswith("STATUS: ok"):
            print(f"{GREEN}{line}{RESET}")
        elif line.startswith("STATUS: error"):
            print(f"{RED}{line}{RESET}")
        elif line.startswith("STATUS: pending"):
            print(f"{YELLOW}{line}{RESET}")
        elif line.startswith("ERROR "):
            print(f"{RED}{line}{RESET}")
        elif line.startswith("HINT "):
            print(f"{DIM}{line}{RESET}")
        else:
            print(line)


# ---------------------------------------------------------------------------
# Main REPL
# ---------------------------------------------------------------------------


def main(args: list[str] | None = None) -> None:
    """Entry point for the ``nemlai`` CLI."""
    import argparse

    parser = argparse.ArgumentParser(
        prog="nemlai",
        description="NemlAI CLI — interact with Nemlig.com from your terminal",
    )
    parser.add_argument(
        "--url",
        default=None,
        help=f"Base URL of the NemlAI server (default: {DEFAULT_BASE_URL})",
    )
    parser.add_argument(
        "--token",
        default=None,
        help="API token (or set NEMLAI_TOKEN env var)",
    )
    parser.add_argument(
        "--version",
        action="store_true",
        help="Show version and exit",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output structured JSON (single-command mode only)",
    )
    parser.add_argument(
        "command",
        nargs="*",
        help="Run a single command and exit (e.g., nemlai help)",
    )
    # Use parse_known_args so command-specific flags (--email, --password, etc.)
    # aren't rejected by argparse — they're passed through as part of the command.
    parsed, extra = parser.parse_known_args(args)
    if extra:
        parsed.command = (parsed.command or []) + extra
    _start_update_check()

    if parsed.token:
        print(f"{YELLOW}Warning: Passing tokens via --token is insecure (visible in process listings). "
              f"Use NEMLAI_TOKEN env var or 'nemlai login' instead.{RESET}", file=sys.stderr)

    from nemlai import __version__

    if parsed.version:
        print(f"nemlai {__version__}")
        return

    # Resolve base URL
    base_url = parsed.url or _get_base_url()
    if parsed.url:
        _save_base_url(parsed.url)

    # Resolve token
    token = parsed.token or _load_token()

    # Build HTTP client
    client = _make_client(token, base_url)

    # Non-interactive: run a single command
    if parsed.command:
        cmd = " ".join(parsed.command)
        text = _dispatch_single_command(client, cmd, json_mode=parsed.json)
        if parsed.json:
            _print_json_response(text)
        else:
            _print_response(text)
        if _update_message and not parsed.json:
            print(f"\n{DIM}{_update_message}{RESET}", file=sys.stderr)
        client.close()
        # Exit codes: ok → 0, error → 1, connection error → 2
        status, _ = parse_response(text)
        if status == "error":
            if "Cannot connect" in text or "Connection error" in text:
                sys.exit(2)
            sys.exit(1)
        return

    # Interactive REPL
    _setup_readline()

    # Banner
    term_width = shutil.get_terminal_size().columns
    if term_width >= 60:
        print(f"{CYAN}{BANNER}{RESET}")
    print(f"{BOLD}NemlAI CLI v{__version__}{RESET}")
    print(f"{BOLD}Interact with nemlig.com through CLI.{RESET}")
    print(f"{BOLD}Bespoke feature of this tool: use the 'basket generate' command{RESET}")
    print(f"{BOLD}to generate a basket based on your order history.{RESET}")
    print(f"{DIM}Server: {base_url}{RESET}")
    if token:
        print(f"{DIM}Authenticated (token loaded from ~/.config/nemlai/token){RESET}")
    else:
        print(f"{DIM}Not logged in. Type 'login' to authenticate.{RESET}")
    print(f"{DIM}Type 'help' for available commands. Ctrl+D to exit.{RESET}")
    if _update_message:
        print(f"\n{YELLOW}{_update_message}{RESET}")
    print()

    _interrupt_count = 0
    try:
        while True:
            try:
                line = input(f"{GREEN}nemlai> {RESET}")
                _interrupt_count = 0
            except EOFError:
                print()
                break
            except KeyboardInterrupt:
                _interrupt_count += 1
                if _interrupt_count >= 2:
                    print("\nBye!")
                    break
                print(f"\n{DIM}Press Ctrl+C again to exit, or keep typing.{RESET}")
                continue

            cmd = line.strip()
            if not cmd:
                continue

            # Local commands
            if cmd in ("exit", "quit"):
                break

            # Login — non-interactive if --email and --password provided
            if cmd.startswith("login"):
                kw = _parse_cmd_kwargs(cmd)
                if kw.get("email") and kw.get("password"):
                    text = _noninteractive_auth(client, "/cli/api/auth/login", {
                        "email": kw["email"],
                        "password": kw["password"],
                    })
                    tok = _extract_token_and_save(text)
                    if tok:
                        client.close()
                        client = _make_client(tok, base_url)
                        token = tok
                        print(f"{GREEN}Logged in successfully. Token saved to ~/.config/nemlai/token{RESET}")
                    else:
                        _print_response(text)
                else:
                    client, token = _interactive_login(client, base_url)
                continue

            # Signup — non-interactive if required args provided
            if cmd.startswith("signup"):
                kw = _parse_cmd_kwargs(cmd)
                if kw.get("email") and kw.get("password") and kw.get("nemlig-email") and kw.get("nemlig-password"):
                    payload = {
                        "name": kw.get("name", ""),
                        "email": kw["email"],
                        "password": kw["password"],
                        "nemlig_email": kw["nemlig-email"],
                        "nemlig_password": kw["nemlig-password"],
                        "invite_code": kw.get("invite-code"),
                    }
                    def _do_signup() -> str:
                        try:
                            resp = client.post("/cli/api/auth/signup", json=payload)
                            return resp.text
                        except httpx.ConnectError:
                            return "STATUS: error\nERROR Cannot connect to server."
                        except Exception as e:
                            return f"STATUS: error\nERROR {e}"
                    text = _run_with_spinner(
                        _do_signup,
                        phase_key="signup",
                        hint="Signup takes 2-4 minutes (logging into Nemlig + fetching orders).",
                    )
                    _print_response(text)
                else:
                    _interactive_signup(client)
                continue

            # Interactive prompts for basket generate
            if (cmd == "basket generate" or cmd.startswith("basket generate ")) and "--size" not in cmd:
                print(f"{YELLOW}Basket size{RESET} {DIM}(small ~800kr, medium ~1500kr, large ~2500kr, xl ~3500kr, full){RESET}")
                print(f"{YELLOW}Size [medium]:{RESET} ", end="", flush=True)
                size = input().strip() or "medium"
                cmd = f"{cmd} --size {size}"

            # Interactive prompt for basket search
            if cmd == "basket search" and "--query" not in cmd:
                print(f"{YELLOW}Search query:{RESET} ", end="", flush=True)
                query = input().strip()
                if query:
                    cmd = f"basket search --query {query}"

            # Interactive prompt for basket add
            if cmd == "basket add" and "--query" not in cmd:
                print(f"{YELLOW}Product to add:{RESET} ", end="", flush=True)
                query = input().strip()
                if query:
                    cmd = f"basket add --query {query}"

            # Confirmation prompt for basket clear
            if cmd == "basket clear":
                print(f"{YELLOW}Are you sure you want to clear the basket? [y/N]:{RESET} ", end="", flush=True)
                confirm = input().strip().lower()
                if confirm not in ("y", "yes"):
                    print(f"{DIM}Cancelled.{RESET}")
                    continue
                print(f"{DIM}Clearing basket...{RESET}")
                text = _send_command(client, cmd)
                _print_response(text)
                continue

            # Interactive prompt for dish
            if cmd == "dish":
                print(f"{YELLOW}Dish name:{RESET} ", end="", flush=True)
                query = input().strip()
                if query:
                    cmd = f"dish {query}"

            # Interactive prompt for dish add
            if cmd == "dish add":
                print(f"{YELLOW}Item number (or 'all'):{RESET} ", end="", flush=True)
                num = input().strip()
                if num:
                    cmd = f"dish add {num}"

            # Interactive prompt for why
            if cmd == "why" or cmd == "basket why":
                print(f"{YELLOW}Item number:{RESET} ", end="", flush=True)
                num = input().strip()
                if num:
                    cmd = f"why {num}"

            # Interactive password for orders refresh
            if (cmd == "orders refresh" or cmd.startswith("orders refresh ")) and "--password" not in cmd:
                pw = os.environ.get("NEMLIG_PASSWORD") or getpass.getpass(f"{YELLOW}Nemlig password:{RESET} ")
                text = _send_orders_refresh(client, "orders refresh", password=pw)
                _print_response(text)
                continue

            # Interactive password for orders refresh-member
            if cmd.startswith("orders refresh-member") and "--password" not in cmd:
                _rm_email = None
                if "--email" not in cmd:
                    print(f"{YELLOW}Nemlig email:{RESET} ", end="", flush=True)
                    _rm_email = input().strip()
                else:
                    import shlex as _shlex
                    try:
                        _rm_parts = _shlex.split(cmd)
                        for _i, _p in enumerate(_rm_parts):
                            if _p == "--email" and _i + 1 < len(_rm_parts):
                                _rm_email = _rm_parts[_i + 1]
                    except ValueError:
                        pass
                pw = os.environ.get("NEMLIG_PASSWORD") or getpass.getpass(f"{YELLOW}Nemlig password:{RESET} ")
                text = _send_orders_refresh(client, "orders refresh-member", password=pw, email=_rm_email)
                _print_response(text)
                continue

            # Interactive password for password change
            if (cmd == "password change" or cmd.startswith("password change ")) and "--current" not in cmd:
                current = getpass.getpass(f"{YELLOW}Current password:{RESET} ")
                new_pw = getpass.getpass(f"{YELLOW}New password:{RESET} ")
                # Send passwords in structured JSON body, not in command string
                text = _send_command(
                    client, "password change",
                    extra_json={"current_password": current, "new_password": new_pw},
                )
                _print_response(text)
                continue

            # Logout — also clear local token
            if cmd == "logout":
                text = _send_command(client, "logout")
                _print_response(text)
                _clear_token()
                token = ""
                client.close()
                client = _make_client("", base_url)
                continue

            # All other commands go to the server
            text = _send_command_with_progress(client, cmd)
            _print_response(text)
    finally:
        _save_readline_history()
        client.close()


if __name__ == "__main__":
    main()
