"""Edit file tool using hashline references.

Hashline format from read: {line_num}:{hash}|{content}

Edit operations:
- line="2:f1b" + new="..." → replace single line
- range="2:f1b-5:a3c" + new="..." → replace range
- after="3:0e2" + new="..." → insert after line
- before="3:0e2" + new="..." → insert before line
- delete="2:f1b" or delete="2:f1b-5:a3c" → delete line(s)

Hash verification prevents editing stale content.
"""

from __future__ import annotations

import os
import re

from krim.tools.base import Tool
from krim.tools.hashline import hash_line, format_hashlines


def _parse_ref(ref: str) -> tuple[int, str] | None:
    """Parse a line reference like '2:f1b' into (line_num, hash)."""
    match = re.match(r'^(\d+):([0-9a-f]{3})$', ref.strip())
    if match:
        return int(match.group(1)), match.group(2)
    return None


def _parse_range(ref: str) -> tuple[int, str, int, str] | None:
    """Parse a range reference like '2:f1b-5:a3c' into (start_line, start_hash, end_line, end_hash)."""
    match = re.match(r'^(\d+):([0-9a-f]{3})-(\d+):([0-9a-f]{3})$', ref.strip())
    if match:
        return int(match.group(1)), match.group(2), int(match.group(3)), match.group(4)
    return None


class EditTool(Tool):
    name = "edit"
    description = (
        "Edit file using hashline references from read output. "
        "Use line='N:hash' to replace, after/before='N:hash' to insert, delete='N:hash' to remove. "
        "Hash mismatch means file changed - read again."
    )
    parameters = {
        "path": {"type": "string", "description": "File path to edit"},
        "line": {"type": "string", "description": "Replace single line (e.g., '2:f1b')", "optional": True},
        "range": {"type": "string", "description": "Replace range (e.g., '2:f1b-5:a3c')", "optional": True},
        "after": {"type": "string", "description": "Insert after line (e.g., '3:0e2')", "optional": True},
        "before": {"type": "string", "description": "Insert before line (e.g., '3:0e2')", "optional": True},
        "delete": {"type": "string", "description": "Delete line or range (e.g., '2:f1b' or '2:f1b-5:a3c')", "optional": True},
        "new": {"type": "string", "description": "New content (required except for delete)", "optional": True},
    }

    def run(
        self,
        path: str,
        line: str | None = None,
        range: str | None = None,
        after: str | None = None,
        before: str | None = None,
        delete: str | None = None,
        new: str | None = None,
    ) -> str:
        path = os.path.expanduser(path)
        if not os.path.isfile(path):
            return f"error: {path} not found"

        # Count operations
        ops = sum(1 for x in [line, range, after, before, delete] if x is not None)
        if ops == 0:
            return "error: must specify one of: line, range, after, before, delete"
        if ops > 1:
            return "error: specify only one operation (line, range, after, before, or delete)"

        # Check new content
        if delete is None and new is None:
            return "error: 'new' content required for non-delete operations"

        try:
            with open(path, "r") as f:
                lines = f.read().splitlines()
        except PermissionError:
            return f"error: permission denied reading {path}"
        except UnicodeDecodeError:
            return f"error: {path} is a binary file, cannot edit"
        except OSError as e:
            return f"error: failed to read {path}: {e.strerror}"

        try:
            # Dispatch to appropriate handler
            if line:
                return self._replace_line(path, lines, line, new)
            elif range:
                return self._replace_range(path, lines, range, new)
            elif after:
                return self._insert_after(path, lines, after, new)
            elif before:
                return self._insert_before(path, lines, before, new)
            elif delete:
                return self._delete(path, lines, delete)
            return "error: no operation executed"
        except PermissionError:
            return f"error: permission denied writing to {path}"
        except OSError as e:
            return f"error: failed to write {path}: {e.strerror}"

    def _verify_hash(self, lines: list[str], line_num: int, expected_hash: str) -> str | None:
        """Verify hash matches. Returns error message or None if OK."""
        if line_num < 1 or line_num > len(lines):
            return f"error: line {line_num} out of range (file has {len(lines)} lines)"

        actual_hash = hash_line(lines[line_num - 1])
        if actual_hash != expected_hash:
            # Return context around the mismatched line
            start = max(0, line_num - 2)
            end = min(len(lines), line_num + 2)
            context = format_hashlines(lines[start:end], start + 1)
            return f"error: hash mismatch at line {line_num} (expected {expected_hash}, got {actual_hash})\nCurrent state:\n{context}"
        return None

    def _write_and_respond(self, path: str, lines: list[str], msg: str, affected_start: int, affected_end: int) -> str:
        """Write file and return success message with affected lines."""
        with open(path, "w") as f:
            f.write("\n".join(lines))
            if lines:  # Add trailing newline if file not empty
                f.write("\n")

        # Show affected lines with new hashes
        affected = lines[affected_start - 1:affected_end]
        if affected:
            context = format_hashlines(affected, affected_start)
            return f"{msg}\n{context}"
        return msg

    def _replace_line(self, path: str, lines: list[str], ref: str, new: str) -> str:
        """Replace a single line."""
        parsed = _parse_ref(ref)
        if not parsed:
            return f"error: invalid line reference '{ref}' (expected format: N:hash, e.g., '2:f1b')"

        line_num, expected_hash = parsed

        # Verify hash
        err = self._verify_hash(lines, line_num, expected_hash)
        if err:
            return err

        # Replace (new can be multiple lines)
        new_lines = new.splitlines()
        lines[line_num - 1:line_num] = new_lines

        return self._write_and_respond(
            path, lines,
            f"edited {path} (replaced line {line_num})",
            line_num, line_num + len(new_lines) - 1
        )

    def _replace_range(self, path: str, lines: list[str], ref: str, new: str) -> str:
        """Replace a range of lines."""
        parsed = _parse_range(ref)
        if not parsed:
            return f"error: invalid range reference '{ref}' (expected format: N:hash-M:hash, e.g., '2:f1b-5:a3c')"

        start_num, start_hash, end_num, end_hash = parsed

        if start_num > end_num:
            return f"error: invalid range (start {start_num} > end {end_num})"

        # Verify both hashes
        err = self._verify_hash(lines, start_num, start_hash)
        if err:
            return err
        err = self._verify_hash(lines, end_num, end_hash)
        if err:
            return err

        # Replace range
        new_lines = new.splitlines()
        lines[start_num - 1:end_num] = new_lines

        return self._write_and_respond(
            path, lines,
            f"edited {path} (replaced lines {start_num}-{end_num})",
            start_num, start_num + len(new_lines) - 1
        )

    def _insert_after(self, path: str, lines: list[str], ref: str, new: str) -> str:
        """Insert content after a line."""
        parsed = _parse_ref(ref)
        if not parsed:
            return f"error: invalid line reference '{ref}' (expected format: N:hash, e.g., '3:0e2')"

        line_num, expected_hash = parsed

        # Verify hash
        err = self._verify_hash(lines, line_num, expected_hash)
        if err:
            return err

        # Insert after
        new_lines = new.splitlines()
        lines[line_num:line_num] = new_lines

        return self._write_and_respond(
            path, lines,
            f"edited {path} (inserted {len(new_lines)} line(s) after line {line_num})",
            line_num + 1, line_num + len(new_lines)
        )

    def _insert_before(self, path: str, lines: list[str], ref: str, new: str) -> str:
        """Insert content before a line."""
        parsed = _parse_ref(ref)
        if not parsed:
            return f"error: invalid line reference '{ref}' (expected format: N:hash, e.g., '3:0e2')"

        line_num, expected_hash = parsed

        # Verify hash
        err = self._verify_hash(lines, line_num, expected_hash)
        if err:
            return err

        # Insert before
        new_lines = new.splitlines()
        lines[line_num - 1:line_num - 1] = new_lines

        return self._write_and_respond(
            path, lines,
            f"edited {path} (inserted {len(new_lines)} line(s) before line {line_num})",
            line_num, line_num + len(new_lines) - 1
        )

    def _delete(self, path: str, lines: list[str], ref: str) -> str:
        """Delete line(s)."""
        # Try range first
        parsed_range = _parse_range(ref)
        if parsed_range:
            start_num, start_hash, end_num, end_hash = parsed_range

            if start_num > end_num:
                return f"error: invalid range (start {start_num} > end {end_num})"

            # Verify both hashes
            err = self._verify_hash(lines, start_num, start_hash)
            if err:
                return err
            err = self._verify_hash(lines, end_num, end_hash)
            if err:
                return err

            # Delete range
            del lines[start_num - 1:end_num]

            # Show context around deletion
            context_start = max(1, start_num - 1)
            context_end = min(len(lines), start_num + 1)

            return self._write_and_respond(
                path, lines,
                f"edited {path} (deleted lines {start_num}-{end_num})",
                context_start, context_end
            )

        # Try single line
        parsed = _parse_ref(ref)
        if parsed:
            line_num, expected_hash = parsed

            # Verify hash
            err = self._verify_hash(lines, line_num, expected_hash)
            if err:
                return err

            # Delete line
            del lines[line_num - 1]

            # Show context around deletion
            context_start = max(1, line_num - 1)
            context_end = min(len(lines), line_num + 1)

            return self._write_and_respond(
                path, lines,
                f"edited {path} (deleted line {line_num})",
                context_start, context_end
            )

        return f"error: invalid delete reference '{ref}' (expected format: N:hash or N:hash-M:hash)"
