"""Text editor for in-game content editing."""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field

from mudlib.render.highlight import highlight


@dataclass
class EditorResponse:
    """Response from editor input handling."""

    output: str  # text to send back to the player
    done: bool  # True if editor should be closed (quit/save-and-quit)
    saved: bool = False  # True if save was performed


@dataclass
class Editor:
    """Line-based text editor for in-game content.

    Doesn't know about telnet — takes string input, returns string output.
    Will be integrated into the shell loop in a later step.
    """

    buffer: list[str] = field(default_factory=list)
    cursor: int = 0  # current line (0-indexed internally)
    undo_stack: list[list[str]] = field(default_factory=list)
    save_callback: Callable[[str], Awaitable[None]] | None = None
    content_type: str = "text"  # hint for syntax highlighting later
    color_depth: str = "16"  # "16", "256", or "truecolor"
    dirty: bool = False

    def __init__(
        self,
        initial_content: str = "",
        save_callback: Callable[[str], Awaitable[None]] | None = None,
        content_type: str = "text",
        color_depth: str = "16",
    ):
        """Initialize editor with optional content."""
        self.save_callback = save_callback
        self.content_type = content_type
        self.color_depth = color_depth
        self.cursor = 0
        self.undo_stack = []
        self.dirty = False

        if initial_content:
            self.buffer = initial_content.split("\n")
        else:
            self.buffer = []

    def _save_undo_state(self) -> None:
        """Save current buffer state to undo stack."""
        self.undo_stack.append(self.buffer.copy())
        # Cap undo stack at 50 entries
        if len(self.undo_stack) > 50:
            self.undo_stack.pop(0)

    def _mark_dirty(self) -> None:
        """Mark buffer as modified."""
        self.dirty = True

    def _view_buffer(self) -> str:
        """Return formatted view of entire buffer."""
        if not self.buffer:
            return "(empty buffer)"

        # Use syntax highlighting if content_type is not "text"
        if self.content_type != "text":
            content = "\n".join(self.buffer)
            highlighted = highlight(
                content,
                language=self.content_type,
                color_depth=self.color_depth,
                line_numbers=True,
            )
            # If highlight() returned the original (unknown language), fall through
            if "\033[" in highlighted:
                return highlighted

        # Fall back to plain line-number formatting
        max_line_num = len(self.buffer)
        padding = len(str(max_line_num))

        lines = []
        for i, line in enumerate(self.buffer, 1):
            lines.append(f"{i:>{padding}}  {line}")
        return "\n".join(lines)

    def _view_line(self, line_num: int) -> str:
        """Return formatted view of specific line with context."""
        if line_num < 1 or line_num > len(self.buffer):
            return f"Error: Line {line_num} does not exist"

        # Show 3 lines of context on each side
        context = 3
        start = max(0, line_num - 1 - context)
        end = min(len(self.buffer), line_num + context)

        # Use syntax highlighting if content_type is not "text"
        if self.content_type != "text":
            # Get the context window
            context_lines = self.buffer[start:end]
            content = "\n".join(context_lines)
            highlighted = highlight(
                content,
                language=self.content_type,
                color_depth=self.color_depth,
                line_numbers=True,
            )
            # If highlight() returned highlighted content, use it
            if "\033[" in highlighted:
                return highlighted

        # Fall back to plain line-number formatting
        max_line_num = end
        padding = len(str(max_line_num))

        lines = []
        for i in range(start, end):
            display_num = i + 1
            lines.append(f"{display_num:>{padding}}  {self.buffer[i]}")
        return "\n".join(lines)

    def _insert_line(self, line_num: int, text: str) -> str:
        """Insert text at specified line number."""
        if line_num < 1:
            return "Error: Line number must be >= 1"

        self._save_undo_state()

        # Insert at end if line_num is beyond buffer
        if line_num > len(self.buffer):
            self.buffer.append(text)
            self.cursor = len(self.buffer) - 1
        else:
            self.buffer.insert(line_num - 1, text)
            self.cursor = line_num - 1

        self._mark_dirty()
        return f"Inserted at line {line_num}"

    def _delete_line(self, start: int, end: int | None = None) -> str:
        """Delete line(s) at specified range."""
        if start < 1 or start > len(self.buffer):
            return f"Error: Line {start} does not exist"

        self._save_undo_state()

        if end is None:
            # Delete single line
            self.buffer.pop(start - 1)
            self.cursor = min(start - 1, len(self.buffer) - 1)
            self._mark_dirty()
            return f"Deleted line {start}"
        else:
            # Delete range
            if end < start or end > len(self.buffer):
                return f"Error: Invalid range {start}-{end}"

            # Delete from start to end (inclusive)
            del self.buffer[start - 1 : end]
            self.cursor = min(start - 1, len(self.buffer) - 1)
            self._mark_dirty()
            return f"Deleted lines {start}-{end}"

    def _replace_line(self, line_num: int, text: str) -> str:
        """Replace line at specified number with new text."""
        if line_num < 1 or line_num > len(self.buffer):
            return f"Error: Line {line_num} does not exist"

        self._save_undo_state()
        self.buffer[line_num - 1] = text
        self._mark_dirty()
        return f"Replaced line {line_num}"

    def _search_replace(self, old: str, new: str, replace_all: bool = False) -> str:
        """Search and replace text in buffer."""
        self._save_undo_state()
        count = 0

        for i, line in enumerate(self.buffer):
            if old in line:
                if replace_all:
                    self.buffer[i] = line.replace(old, new)
                    count += line.count(old)
                else:
                    self.buffer[i] = line.replace(old, new, 1)
                    count = 1
                    self._mark_dirty()
                    return f"Replaced '{old}' with '{new}' on line {i + 1}"

        if count > 0:
            self._mark_dirty()
            return f"Replaced {count} occurrence(s) of '{old}' with '{new}'"
        else:
            # Restore undo state since nothing changed
            self.undo_stack.pop()
            return f"Not found: '{old}'"

    def _undo(self) -> str:
        """Undo last buffer change."""
        if not self.undo_stack:
            return "Nothing to undo"

        self.buffer = self.undo_stack.pop()
        # Conservative approach: if we undid something, the buffer is now different
        # from what was saved (even if we undo past a save point)
        self.dirty = True
        # Reset cursor to end of buffer after undo
        self.cursor = len(self.buffer) - 1 if self.buffer else 0
        return "Undone last change"

    async def _save(self) -> str:
        """Save buffer content via callback."""
        if self.save_callback is None:
            return "Error: Cannot save (no save callback configured)"

        content = "\n".join(self.buffer)
        await self.save_callback(content)
        self.dirty = False
        return "Saved"

    def _help(self) -> str:
        """Return help text."""
        return """Editor Commands:
:           - view entire buffer with line numbers
:N          - view line N with context
:h          - show this help
:i N text   - insert 'text' at line N
:d N        - delete line N
:d N M      - delete lines N through M
:r N text   - replace line N with 'text'
:s old new  - search and replace first occurrence
:sa old new - search and replace all occurrences
:w          - save
:wq         - save and quit
:q          - quit (warns if unsaved changes)
:q!         - force quit without saving
:u          - undo last change
.           - save and quit (MOO convention)

Any line not starting with : is appended to the buffer."""

    async def handle_input(self, line: str) -> EditorResponse:
        """Handle editor input and return response."""
        # Special case: dot on its own saves and quits
        if line == ".":
            if self.save_callback is None:
                return EditorResponse(
                    output="Error: Cannot save (no save callback configured)",
                    done=False,
                )
            save_msg = await self._save()
            return EditorResponse(output=save_msg, done=True, saved=True)

        # Check if it's a command
        if not line.startswith(":"):
            # Append to buffer
            self._save_undo_state()
            self.buffer.append(line)
            self.cursor = len(self.buffer) - 1
            self._mark_dirty()
            return EditorResponse(output="", done=False)

        # Parse command
        cmd = line[1:].strip()

        # Empty command - view buffer
        if not cmd:
            output = self._view_buffer()
            return EditorResponse(output=output, done=False)

        # Check for numeric line view
        if cmd.isdigit():
            line_num = int(cmd)
            output = self._view_line(line_num)
            return EditorResponse(output=output, done=False)

        # Parse command parts
        parts = cmd.split(None, 1)
        command = parts[0]
        args = parts[1] if len(parts) > 1 else ""

        # Help
        if command == "h":
            output = self._help()
            return EditorResponse(output=output, done=False)

        # Insert
        if command == "i":
            if not args:
                return EditorResponse(
                    output="Error: :i requires line number and text", done=False
                )
            parts = args.split(None, 1)
            if len(parts) < 2:
                return EditorResponse(
                    output="Error: :i requires line number and text", done=False
                )
            try:
                line_num = int(parts[0])
                text = parts[1]
                output = self._insert_line(line_num, text)
                return EditorResponse(output=output, done=False)
            except ValueError:
                return EditorResponse(output="Error: Invalid line number", done=False)

        # Delete
        if command == "d":
            if not args:
                return EditorResponse(
                    output="Error: :d requires line number(s)", done=False
                )
            parts = args.split()
            try:
                start = int(parts[0])
                end = int(parts[1]) if len(parts) > 1 else None
                output = self._delete_line(start, end)
                return EditorResponse(output=output, done=False)
            except (ValueError, IndexError):
                return EditorResponse(
                    output="Error: Invalid line number(s)", done=False
                )

        # Replace
        if command == "r":
            if not args:
                return EditorResponse(
                    output="Error: :r requires line number and text", done=False
                )
            parts = args.split(None, 1)
            if len(parts) < 2:
                return EditorResponse(
                    output="Error: :r requires line number and text", done=False
                )
            try:
                line_num = int(parts[0])
                text = parts[1]
                output = self._replace_line(line_num, text)
                return EditorResponse(output=output, done=False)
            except ValueError:
                return EditorResponse(output="Error: Invalid line number", done=False)

        # Search and replace
        if command == "s":
            split_args = args.split(None, 1)
            if len(split_args) < 2:
                return EditorResponse(
                    output="Error: :s requires old and new text", done=False
                )
            old = split_args[0]
            new = split_args[1]
            output = self._search_replace(old, new, replace_all=False)
            return EditorResponse(output=output, done=False)

        # Search and replace all
        if command == "sa":
            parts = args.split(None, 1)
            if len(parts) < 2:
                return EditorResponse(
                    output="Error: :sa requires old and new text", done=False
                )
            old = parts[0]
            new = parts[1]
            output = self._search_replace(old, new, replace_all=True)
            return EditorResponse(output=output, done=False)

        # Undo
        if command == "u":
            output = self._undo()
            return EditorResponse(output=output, done=False)

        # Save
        if command == "w":
            output = await self._save()
            return EditorResponse(output=output, done=False, saved=True)

        # Save and quit
        if command == "wq":
            output = await self._save()
            return EditorResponse(output=output, done=True, saved=True)

        # Quit
        if command == "q":
            if self.dirty:
                return EditorResponse(
                    output=(
                        "Unsaved changes! Use :wq to save and quit, "
                        "or :q! to force quit"
                    ),
                    done=False,
                )
            return EditorResponse(output="", done=True)

        # Force quit
        if command == "q!":
            return EditorResponse(output="", done=True)

        # Unknown command
        return EditorResponse(output=f"Unknown command: :{command}", done=False)
