"""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 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",
]

# 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..."),
    ],
}

_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 _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)

    # Print time estimate before starting
    for prefix, hint in _SLOW_HINTS.items():
        if command.startswith(prefix):
            print(f"{DIM}{hint}{RESET}")
            break

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

    def do_request() -> None:
        result[0] = _send_command(client, command, extra_json=extra_json)

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

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

    while thread.is_alive():
        elapsed = time.monotonic() - start
        phase = _get_phase(command, 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"


# ---------------------------------------------------------------------------
# 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."""
    print(f"{BOLD}Create a NemlAI account{RESET}\n")
    print(f"{YELLOW}Display name:{RESET} ", end="", flush=True)
    name = input()
    print(f"{YELLOW}Email:{RESET} ", end="", flush=True)
    email = input()
    password = getpass.getpass(f"{YELLOW}Password:{RESET} ")
    print(f"{YELLOW}Nemlig.com email:{RESET} ", end="", flush=True)
    nemlig_email = input()
    nemlig_password = getpass.getpass(f"{YELLOW}Nemlig.com password:{RESET} ")
    print(f"{YELLOW}Invite code (optional):{RESET} ", end="", flush=True)
    invite_code = input()

    try:
        resp = client.post(
            "/cli/api/auth/signup",
            json={
                "name": name,
                "email": email,
                "password": password,
                "nemlig_email": nemlig_email,
                "nemlig_password": nemlig_password,
                "invite_code": invite_code or None,
            },
        )
        _print_response(resp.text)
    except httpx.ConnectError:
        print(f"{RED}Cannot connect to server.{RESET}")
    except Exception as e:
        print(f"{RED}Error: {e}{RESET}")


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


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 NemlAI 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(
        "command",
        nargs="*",
        help="Run a single command and exit (e.g., nemlai help)",
    )
    parsed = parser.parse_args(args)
    _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 = _send_command(client, cmd)
        _print_response(text)
        if _update_message:
            print(f"\n{DIM}{_update_message}{RESET}", file=sys.stderr)
        client.close()
        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"{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

            # Interactive login — collects password securely
            if cmd == "login":
                client, token = _interactive_login(client, base_url)
                continue

            # Interactive signup
            if cmd == "signup":
                _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 = getpass.getpass(f"{YELLOW}Nemlig password:{RESET} ")
                # Send password in structured JSON body, not in command string
                text = _send_command_with_progress(
                    client, cmd,
                    extra_json={"password": pw},
                )
                _print_response(text)
                continue

            # Interactive password for orders refresh-member
            if cmd.startswith("orders refresh-member") and "--password" not in cmd:
                if "--email" not in cmd:
                    print(f"{YELLOW}Nemlig email:{RESET} ", end="", flush=True)
                    email = input()
                    cmd = f"{cmd} --email {email}"
                pw = getpass.getpass(f"{YELLOW}Nemlig password:{RESET} ")
                # Send password in structured JSON body, not in command string
                text = _send_command_with_progress(
                    client, cmd,
                    extra_json={"password": pw},
                )
                _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()
