"""Domain models for TUI permission requests and decisions.

Defines the data structures used by the PermissionsScreen to display
tool permission requests with diff views and record user decisions.
"""

from __future__ import annotations

from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field

__all__ = [
    "DiffDisplayMode",
    "FileChangeType",
    "PermissionDecision",
    "PermissionRequest",
    "ToolPermissionRequest",
]


class DiffDisplayMode(StrEnum):
    """Diff display modes for the PermissionsScreen.

    Toggled with the ``d`` key in the PermissionsScreen.
    """

    UNIFIED = "unified"
    """Standard unified diff with +/- lines (default)."""

    SIDE_BY_SIDE = "side_by_side"
    """Two-column view with old content left, new content right."""

    CONTEXT = "context"
    """Shows only changed lines with surrounding context (3 lines default)."""


class FileChangeType(StrEnum):
    """Type of file change in a permission request."""

    MODIFIED = "M"
    """File was modified."""

    ADDED = "A"
    """File was added."""

    DELETED = "D"
    """File was deleted."""


class PermissionDecision(StrEnum):
    """User decision for a permission request."""

    ALLOW_ONCE = "allow_once"
    """Allow this operation once."""

    ALLOW_ALWAYS = "allow_always"
    """Allow all operations of this type for the remainder of the session."""

    REJECT_ONCE = "reject_once"
    """Reject this operation once."""

    REJECT_ALWAYS = "reject_always"
    """Reject all operations of this type for the remainder of the session."""


class PermissionRequest(BaseModel):
    """A single file change within a tool permission request."""

    path: str = Field(..., min_length=1, description="File path being changed.")
    change_type: FileChangeType = Field(
        ...,
        description="Type of change (M/A/D).",
    )
    before_content: str | None = Field(
        default=None,
        description="Content before the change (None for new files).",
    )
    after_content: str | None = Field(
        default=None,
        description="Content after the change (None for deleted files).",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    def unified_diff(self, *, context_lines: int = 3) -> str:
        """Generate a unified diff string for this file change."""
        import difflib

        before_lines = (self.before_content or "").splitlines(keepends=True)
        after_lines = (self.after_content or "").splitlines(keepends=True)
        diff_lines = difflib.unified_diff(
            before_lines,
            after_lines,
            fromfile=f"a/{self.path}",
            tofile=f"b/{self.path}",
            n=context_lines,
        )
        return "".join(diff_lines)

    def side_by_side_diff(self) -> tuple[list[str], list[str]]:
        """Return (left_lines, right_lines) for side-by-side display."""
        import difflib

        before_lines = (self.before_content or "").splitlines()
        after_lines = (self.after_content or "").splitlines()

        matcher = difflib.SequenceMatcher(None, before_lines, after_lines)
        left: list[str] = []
        right: list[str] = []

        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == "equal":
                for line in before_lines[i1:i2]:
                    left.append(f"  {line}")
                    right.append(f"  {line}")
            elif tag == "replace":
                old_chunk = before_lines[i1:i2]
                new_chunk = after_lines[j1:j2]
                max_len = max(len(old_chunk), len(new_chunk))
                for k in range(max_len):
                    left_line = f"- {old_chunk[k]}" if k < len(old_chunk) else ""
                    right_line = f"+ {new_chunk[k]}" if k < len(new_chunk) else ""
                    left.append(left_line)
                    right.append(right_line)
            elif tag == "delete":
                for line in before_lines[i1:i2]:
                    left.append(f"- {line}")
                    right.append("")
            elif tag == "insert":
                for line in after_lines[j1:j2]:
                    left.append("")
                    right.append(f"+ {line}")

        return left, right

    def context_diff(self, *, context_lines: int = 3) -> str:
        """Generate a context diff showing only changed lines with context."""
        import difflib

        before_lines = (self.before_content or "").splitlines(keepends=True)
        after_lines = (self.after_content or "").splitlines(keepends=True)
        diff_lines = difflib.context_diff(
            before_lines,
            after_lines,
            fromfile=f"a/{self.path}",
            tofile=f"b/{self.path}",
            n=context_lines,
        )
        return "".join(diff_lines)

    def render_diff(
        self,
        mode: DiffDisplayMode,
        *,
        context_lines: int = 3,
    ) -> str:
        """Render the diff in the specified display mode."""
        if mode == DiffDisplayMode.UNIFIED:
            return self.unified_diff(context_lines=context_lines)
        if mode == DiffDisplayMode.SIDE_BY_SIDE:
            left, right = self.side_by_side_diff()
            lines: list[str] = []
            for l_line, r_line in zip(left, right, strict=True):
                lines.append(f"{l_line:<40} | {r_line}")
            return "\n".join(lines)
        # CONTEXT mode
        return self.context_diff(context_lines=context_lines)


class ToolPermissionRequest(BaseModel):
    """A complete tool permission request with multiple file changes."""

    request_id: str = Field(
        ...,
        min_length=1,
        description="Unique identifier for this permission request.",
    )
    tool_name: str = Field(
        ...,
        min_length=1,
        description="Name of the tool requesting permission (e.g. 'local/file-write').",
    )
    resource_name: str = Field(
        ...,
        min_length=1,
        description="Name of the resource being modified (e.g. 'local/api-service').",
    )
    changes: list[PermissionRequest] = Field(
        default_factory=list,
        description="List of file changes in this request.",
    )
    decision: PermissionDecision | None = Field(
        default=None,
        description="User decision for this request (None if pending).",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    @property
    def is_pending(self) -> bool:
        """Return True if no decision has been made yet."""
        return self.decision is None

    @property
    def is_allowed(self) -> bool:
        """Return True if the request was allowed."""
        return self.decision in {
            PermissionDecision.ALLOW_ONCE,
            PermissionDecision.ALLOW_ALWAYS,
        }

    @property
    def is_rejected(self) -> bool:
        """Return True if the request was rejected."""
        return self.decision in {
            PermissionDecision.REJECT_ONCE,
            PermissionDecision.REJECT_ALWAYS,
        }

    def apply_decision(self, decision: PermissionDecision) -> ToolPermissionRequest:
        """Return a new request with the given decision applied."""
        return self.model_copy(update={"decision": decision})
