from __future__ import annotations

import os
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pathspec

from prompt_toolkit import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.containers import HSplit, Window, ConditionalContainer
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.filters import Condition, has_completions
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.menus import CompletionsMenuControl
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
from prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings
from prompt_toolkit.styles import Style

STYLE = Style.from_dict(
    {
        "": "fg:#d7d7d7 bg:default",
        "completion-menu": "noinherit fg:#787d82 bg:default",
        "completion-menu.completion": "noinherit fg:#787d82 bg:default",
        "completion-menu.completion.current": "noinherit bold fg:#d7d7d7 bg:default",
        "completion-menu.meta": "noinherit fg:#787d82 bg:default",
        "completion-menu.meta.completion": "noinherit fg:#787d82 bg:default",
        "completion-menu.meta.completion.current": "noinherit fg:#d7d7d7 bg:default",
        "scrollbar.background": "bg:default",
        "scrollbar.button": "bg:default",
    }
)

_DISPLAY_MIN_WIDTH = 25
_DISPLAY_META_RESERVE = 32


def _fmt(text: str) -> str:
    """Pad text to min width and truncate with ellipsis if needed."""
    cols = shutil.get_terminal_size().columns
    max_width = max(_DISPLAY_MIN_WIDTH, cols - _DISPLAY_META_RESERVE)
    if len(text) > max_width:
        return text[: max_width - 1] + "…"
    return text.ljust(_DISPLAY_MIN_WIDTH)


REPL_COMMANDS: dict[str, str] = {
    "help": "show this help",
    "quit": "quit the REPL",
    "exit": "quit the REPL",
    "clear": "clear conversation history",
    "session": "show current session info",
    "cost": "show accumulated cost and token usage",
    "model": "show or switch current model",
}

_MAX_FILE_COMPLETIONS = 20
_gitignore_cache: tuple[Path, "pathspec.PathSpec"] | None = None  # noqa: F821


def _load_gitspec(cwd: Path) -> "pathspec.PathSpec":  # noqa: F821
    """Return a pathspec.PathSpec for the gitignore at cwd (cached)."""
    import pathspec  # noqa: PLC0415

    global _gitignore_cache
    if _gitignore_cache and _gitignore_cache[0] == cwd:
        return _gitignore_cache[1]

    lines: list[str] = []
    gitignore = cwd / ".gitignore"
    if gitignore.exists():
        lines = gitignore.read_text(errors="replace").splitlines()
    spec: pathspec.PathSpec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
    _gitignore_cache = (cwd, spec)
    return spec


def _is_hidden(path: Path) -> bool:
    return any(part.startswith(".") for part in path.parts)


def _file_completions(partial: str):
    """Yield Completion entries for @<partial> filesystem matches.

    - No '/' in partial → recursive search for paths containing the string.
    - Partial ends with '/' → list that directory.
    - Otherwise → list parent dir filtered by last component prefix.
    Dotfiles and gitignored paths are always excluded.
    """

    cwd = Path.cwd()
    spec = _load_gitspec(cwd)

    def _ignored(p: Path) -> bool:
        rel = p.relative_to(cwd)
        rel_str = str(rel) + ("/" if p.is_dir() else "")
        return _is_hidden(rel) or bool(spec.match_file(rel_str))

    # ── recursive name search ─────────────────────────────────────────────
    if partial and "/" not in partial:
        needle = partial.lower()
        count = 0
        try:
            for dirpath, dirnames, filenames in os.walk(cwd):
                root = Path(dirpath)
                rel_root = root.relative_to(cwd)

                # prune ignored/hidden dirs in-place so os.walk won't descend
                dirnames[:] = sorted(
                    d
                    for d in dirnames
                    if not d.startswith(".")
                    and not spec.match_file(str(rel_root / d) + "/")
                )

                for name in sorted(filenames):
                    if name.startswith("."):
                        continue
                    rel = rel_root / name
                    rel_str = str(rel)
                    if spec.match_file(rel_str):
                        continue
                    if needle not in rel_str.lower():
                        continue
                    yield Completion(
                        rel_str, start_position=-len(partial), display=_fmt(rel_str)
                    )
                    count += 1
                    if count >= _MAX_FILE_COMPLETIONS:
                        return
        except (PermissionError, FileNotFoundError):
            pass
        return

    # ── directory listing ─────────────────────────────────────────────────
    base = Path(partial)
    if partial.endswith("/"):
        search_dir = cwd / base
        prefix = partial
        name_filter = ""
    else:
        search_dir = cwd / base.parent
        prefix = str(base.parent) + "/" if base.parent != Path(".") else ""
        name_filter = base.name

    try:
        entries = sorted(search_dir.iterdir(), key=lambda p: (p.is_file(), p.name))
    except (PermissionError, FileNotFoundError):
        return

    count = 0
    for entry in entries:
        if _ignored(entry):
            continue
        if name_filter and not entry.name.startswith(name_filter):
            continue
        display = prefix + entry.name + ("/" if entry.is_dir() else "")
        raw = f"+ {display}"
        yield Completion(raw, start_position=-len(partial), display=_fmt(raw))
        count += 1
        if count >= _MAX_FILE_COMPLETIONS:
            break


class ModelCompleter(Completer):
    """Completes bare model names (used by set-model)."""

    def __init__(self, model_names: list[str]) -> None:
        self._model_names = model_names

    def get_completions(self, document, complete_event):  # type: ignore[override]
        partial = document.text
        for name in self._model_names:
            if name.startswith(partial):
                yield Completion(name, start_position=-len(partial))


class ReplCompleter(Completer):
    """Completes REPL commands."""

    def __init__(self, model_names: list[str]) -> None:
        self._model_names = model_names

    def get_completions(self, document, complete_event):  # type: ignore[override]
        text_before = document.text_before_cursor

        # ── @file completion ──────────────────────────────────────────────────
        at_pos = text_before.rfind("@")
        if at_pos != -1:
            partial = text_before[at_pos + 1 :]
            if " " not in partial:
                yield from _file_completions(partial)
                return

        # ── :command completion ───────────────────────────────────────────────
        text = document.text
        if not text.startswith(":"):
            return
        after_colon = text[1:]
        parts = after_colon.split(" ", 1)

        if len(parts) == 1:
            partial = parts[0].lower()
            for cmd, desc in REPL_COMMANDS.items():
                if cmd.startswith(partial):
                    yield Completion(
                        cmd,
                        start_position=-len(partial),
                        display=_fmt(cmd),
                        display_meta=desc,
                    )
        elif parts[0].lower() == "model" and self._model_names:
            partial = parts[1]
            for name in self._model_names:
                if name.startswith(partial):
                    yield Completion(
                        name, start_position=-len(partial), display=_fmt(name)
                    )


class InputSession:
    """Inline input widget: border / input / border / status — no full-screen takeover."""

    _BORDER_STYLE = "fg:#50555a"
    _PROMPT_FT = FormattedText([("fg:#50555a", "  "), ("fg:#787d82", "> ")])

    def __init__(self, completer: Completer) -> None:
        self._completer = completer
        self._history: InMemoryHistory = InMemoryHistory()

    async def prompt_async(self, get_status) -> str:  # get_status: () -> FormattedText
        submitted: list[str | None] = [None]
        show_chrome: list[bool] = [True]

        buf = Buffer(
            completer=self._completer,
            complete_while_typing=True,
            history=self._history,
        )

        kb = KeyBindings()

        @kb.add("enter", eager=True)
        def _enter(event) -> None:  # type: ignore[misc]
            b = event.current_buffer
            if b.complete_state:
                b.complete_state = None
            else:
                submitted[0] = b.text
                b.append_to_history()
                show_chrome[0] = False
                event.app.exit()

        @kb.add("tab", eager=True)
        def _tab(event) -> None:  # type: ignore[misc]
            b = event.current_buffer
            if b.complete_state:
                b.complete_next()
            else:
                b.start_completion(select_first=False)

        @kb.add("s-tab", eager=True)
        def _s_tab(event) -> None:  # type: ignore[misc]
            b = event.current_buffer
            if b.complete_state:
                b.complete_previous()

        @kb.add("c-c", eager=True)
        def _interrupt(event) -> None:  # type: ignore[misc]
            event.app.exit(exception=KeyboardInterrupt())

        @kb.add("c-d", eager=True)
        def _eof(event) -> None:  # type: ignore[misc]
            if not event.current_buffer.text:
                event.app.exit(exception=EOFError())
            else:
                event.current_buffer.delete()

        def _border() -> FormattedText:
            width = shutil.get_terminal_size().columns
            return FormattedText([(InputSession._BORDER_STYLE, "─" * width)])

        input_win = Window(
            BufferControl(buffer=buf, focusable=True),
            get_line_prefix=lambda lineno, wrap_count: (
                InputSession._PROMPT_FT
                if lineno == 0 and wrap_count == 0
                else FormattedText([("", "    ")])
            ),
            wrap_lines=True,
            dont_extend_height=True,
        )

        chrome = Condition(lambda: show_chrome[0])

        layout = Layout(
            HSplit(
                [
                    ConditionalContainer(
                        Window(
                            FormattedTextControl(_border),
                            height=1,
                            dont_extend_height=True,
                        ),
                        filter=chrome,
                    ),
                    input_win,
                    ConditionalContainer(
                        Window(
                            FormattedTextControl(_border),
                            height=1,
                            dont_extend_height=True,
                        ),
                        filter=chrome,
                    ),
                    ConditionalContainer(
                        Window(
                            CompletionsMenuControl(),
                            height=Dimension(min=1, max=5),
                            width=Dimension(weight=1),
                            dont_extend_height=True,
                        ),
                        filter=has_completions & chrome,
                    ),
                    ConditionalContainer(
                        Window(
                            FormattedTextControl(get_status),
                            height=1,
                            dont_extend_height=True,
                        ),
                        filter=~has_completions & chrome,
                    ),
                ]
            ),
            focused_element=input_win,
        )

        app: Application[None] = Application(
            layout=layout,
            key_bindings=merge_key_bindings([load_emacs_bindings(), kb]),
            style=STYLE,
            full_screen=False,
        )

        await app.run_async()

        if submitted[0] is None:
            raise EOFError
        return submitted[0]
