"""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 _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 adjust",
    "basket send", "basket search", "basket why",
    "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..."),
    ],
}

_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.",
}


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)

    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)
        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}")
    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}"

            # 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()
