"""
goCLI  --  terminal error detection and auto-fix assistant.

Cross-platform entry point (Windows, macOS, Linux).

Uses a command-loop architecture: reads user input, executes each
command via subprocess, captures stdout and stderr, displays the
output, and checks for error patterns. No pty dependency.
"""

from __future__ import annotations

import json
import os
import platform
import shlex
import subprocess
import sys
import threading
import urllib.request
from collections import deque
from typing import Deque

from .config import (
    CORAL,
    OFF_WHITE,
    MUTED,
    RESET,
    LABEL,
    CONTEXT_WINDOW,
    IS_WINDOWS,
)
from .watcher import detect_error
from .ai import get_fix
from .fix import present_suggestion, execute_fix

_CURRENT_VERSION = "0.2.2"
_PYPI_URL = "https://pypi.org/pypi/gocli-terminal/json"


def _check_for_update() -> None:
    """Check PyPI for a newer version and print a notice if one exists.

    Runs in a background thread with a 3-second timeout so startup
    is never blocked.
    """
    result: list[str] = []

    def _fetch() -> None:
        try:
            with urllib.request.urlopen(_PYPI_URL, timeout=3) as resp:
                data = json.loads(resp.read().decode())
            latest: str = data["info"]["version"]
            if latest != _CURRENT_VERSION:
                result.append(latest)
        except Exception:
            pass

    t = threading.Thread(target=_fetch, daemon=True)
    t.start()
    t.join(timeout=4)

    if result:
        latest = result[0]
        sys.stderr.write(
            f"  {LABEL}  {MUTED}update available  "
            f"{CORAL}{_CURRENT_VERSION}{MUTED} → {OFF_WHITE}{latest}{RESET}\n"
            f"         {MUTED}run  {CORAL}pip install --upgrade gocli-terminal{RESET}\n\n"
        )
        sys.stderr.flush()


def _get_shell() -> str:
    """Return the platform shell for subprocess execution."""
    if IS_WINDOWS:
        return os.environ.get("COMSPEC", "cmd.exe")
    return os.environ.get("SHELL", "/bin/bash")


def _get_prompt() -> str:
    """Build a minimal prompt showing cwd."""
    cwd = os.getcwd()
    try:
        home = os.path.expanduser("~")
        if cwd.startswith(home):
            cwd = "~" + cwd[len(home):]
    except Exception:
        pass
    return f"{MUTED}{cwd}{RESET} {CORAL}>{RESET} "


def _banner() -> None:
    """Print a one-line startup notice."""
    exit_key = "ctrl-z then enter" if IS_WINDOWS else "ctrl-d"
    sys.stderr.write(
        f"  {LABEL}  {MUTED}watching session  "
        f"{OFF_WHITE}{exit_key}{MUTED} to exit{RESET}\n\n"
    )
    sys.stderr.flush()


def _run_command(command: str) -> tuple[str, str, int]:
    """Execute *command* in a shell and return (stdout, stderr, returncode).

    Output is streamed to the real terminal in real time AND captured
    for error detection.
    """
    # Use the platform shell so builtins (cd, set, etc.) work.
    if IS_WINDOWS:
        args = ["cmd.exe", "/c", command]
    else:
        args = ["sh", "-c", command]

    try:
        proc = subprocess.Popen(
            args,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
    except OSError as exc:
        msg = f"could not execute: {exc}"
        sys.stderr.write(msg + "\n")
        return "", msg, 1

    # Read stdout and stderr in full (simple and cross-platform).
    # For very long-running commands this blocks, but it keeps the
    # implementation dependency-free and portable.
    stdout_bytes, stderr_bytes = proc.communicate()
    stdout_text = stdout_bytes.decode("utf-8", errors="replace")
    stderr_text = stderr_bytes.decode("utf-8", errors="replace")

    # Display output to the user.
    if stdout_text:
        sys.stdout.write(stdout_text)
        if not stdout_text.endswith("\n"):
            sys.stdout.write("\n")
        sys.stdout.flush()
    if stderr_text:
        sys.stderr.write(stderr_text)
        if not stderr_text.endswith("\n"):
            sys.stderr.write("\n")
        sys.stderr.flush()

    return stdout_text, stderr_text, proc.returncode


def _handle_cd(args: str) -> None:
    """Handle ``cd`` as a built-in since subprocess can't change our cwd."""
    target = args.strip()
    if not target:
        target = os.path.expanduser("~")
    # Strip surrounding quotes (common on Windows).
    if len(target) >= 2 and target[0] in ('"', "'") and target[-1] == target[0]:
        target = target[1:-1]
    target = os.path.expandvars(os.path.expanduser(target))
    try:
        os.chdir(target)
    except FileNotFoundError:
        sys.stderr.write(f"cd: no such directory: {target}\n")
    except PermissionError:
        sys.stderr.write(f"cd: permission denied: {target}\n")


def run() -> int:
    """Launch goCLI. Returns an exit code."""

    _check_for_update()
    _banner()

    # Set env flag so nested goCLI invocations can detect the wrapper.
    os.environ["GOCLI_ACTIVE"] = "1"

    recent_commands: Deque[str] = deque(maxlen=CONTEXT_WINDOW)

    while True:
        # Print the prompt and read a line.
        try:
            line = input(_get_prompt())
        except (EOFError, KeyboardInterrupt):
            sys.stderr.write("\n")
            break

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

        # Exit commands.
        if command.lower() in ("exit", "quit"):
            break

        # Handle cd in-process so the working directory actually changes.
        if command == "cd" or command.startswith("cd "):
            _handle_cd(command[2:])
            recent_commands.append(command)
            continue

        # Map 'clear' to 'cls' on Windows so it works cross-platform.
        if IS_WINDOWS and command.lower() == "clear":
            command = "cls"

        # Record the command.
        recent_commands.append(command)

        # Execute and capture output.
        stdout_text, stderr_text, returncode = _run_command(command)

        # Only check for errors when the command failed.
        if returncode != 0:
            combined = stdout_text + "\n" + stderr_text
            error = detect_error(combined)

            if error is not None:
                fix_cmd = get_fix(
                    error.matched_text,
                    list(recent_commands),
                )

                if fix_cmd:
                    accepted = present_suggestion(error.description, fix_cmd)
                    if accepted:
                        execute_fix(fix_cmd)
                else:
                    sys.stderr.write(
                        f"  {LABEL}  {MUTED}no fix available{RESET}\n"
                    )
                    sys.stderr.flush()

    return 0


def main() -> None:
    """CLI entry point."""
    raise SystemExit(run())


if __name__ == "__main__":
    main()
