"""
Copyright (C) 2024-2026 Mo Zhou <lumin@debian.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

import difflib
import os
import pty
import re
import select
import shlex
import subprocess
import tempfile
from collections import deque

from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text


def render_diff(old_text: str, new_text: str, path: str, max_chars: int = 4000) -> str:
    """Return a unified diff between old_text and new_text, syntax
    highlighted with Pygments when available, ready to print raw to a
    terminal.  Returns "(no changes)" when the two are identical."""
    try:
        from pygments import highlight as _pyg_highlight
        from pygments.formatters import TerminalTrueColorFormatter
        from pygments.lexers import DiffLexer

        _has_pygments = True
    except ImportError:
        _has_pygments = False

    base = os.path.basename(path)
    diff_lines = list(
        difflib.unified_diff(
            old_text.splitlines(),
            new_text.splitlines(),
            fromfile=f"a/{base}",
            tofile=f"b/{base}",
            lineterm="",
        )
    )
    if not diff_lines:
        return "(no changes)"

    diff_text = "\n".join(diff_lines)
    if len(diff_text) > max_chars:
        diff_text = diff_text[:max_chars] + "\n... (diff truncated)"

    if _has_pygments:
        return _pyg_highlight(
            diff_text,
            DiffLexer(),
            TerminalTrueColorFormatter(style="monokai"),
        ).rstrip()
    return diff_text


def _short(value, limit: int = 50) -> str:
    """Render a value on a single line, truncating if it is too long.

    Used to keep tool-call summaries concise: newlines are collapsed and
    long blobs (e.g. file contents or edit strings) are cut with an
    ellipsis so the summary stays on one tidy line.
    """
    s = " ".join(str(value).split())
    if len(s) > limit:
        s = s[: limit - 3] + "..."
    return s


def _diff_stat(old: str, new: str) -> tuple:
    """Return (added, removed) line counts between two strings, git --stat style."""
    added = removed = 0
    for line in difflib.ndiff(old.splitlines(), new.splitlines()):
        if line.startswith("+ "):
            added += 1
        elif line.startswith("- "):
            removed += 1
    return added, removed


class Tool:
    """Base class for all tools."""

    name = ""
    description = ""

    def __call__(self, *args, **kwargs):
        raise NotImplementedError

    parameters = {}

    def to_openai_tool(self) -> dict:
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters,
            },
        }

    def needs_confirmation(self, args_dict: dict) -> tuple:
        """Return (needs_confirm: bool, reason: str) for the given call args.

        Subclasses should override this to require user confirmation for
        dangerous or destructive operations.  The default is no confirmation.
        """
        return False, ""

    def confirmation_context(self, args_dict: dict) -> str:
        """Return extra context to display when asking for confirmation.

        Subclasses can override this to show, e.g., the existing content
        that will be overwritten.  Return an empty string for no extra context.
        """
        return ""

    def summary(self, args_dict: dict) -> str:
        """Return a concise, one-line summary of this tool call for display.

        This is what the user sees in non-verbose mode instead of the full
        JSON arguments (which may include large blobs such as file contents
        or edit strings).  The generic default renders the arguments
        compactly, collapsing whitespace and truncating long values;
        subclasses override it for a prettier, tool-specific rendering.
        """
        parts = [f"{k}={_short(v)}" for k, v in args_dict.items() if v is not None]
        return f"{self.name}({', '.join(parts)})"
