"""Pydantic models for container diff operations."""

from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import Literal

from pydantic import BaseModel, Field


class FileType(str, Enum):
    """Type of filesystem entry."""

    FILE = "file"
    DIRECTORY = "directory"
    SYMLINK = "symlink"
    DEVICE = "device"
    SOCKET = "socket"
    FIFO = "fifo"
    UNKNOWN = "unknown"


class FileChangeType(str, Enum):
    """Type of file change between two states."""

    ADDED = "added"
    DELETED = "deleted"
    MODIFIED = "modified"
    PERMISSIONS = "permissions"
    TYPE_CHANGED = "type_changed"


class FileEntry(BaseModel):
    """Information about a file in the filesystem.

    Captures metadata about a single filesystem entry including
    path, type, size, permissions, and ownership.
    """

    path: str = Field(..., description="Absolute path within container")
    file_type: FileType = Field(default=FileType.FILE, description="Type of entry")
    size: int = Field(default=0, description="Size in bytes")
    mode: int = Field(default=0o644, description="Unix permissions")
    uid: int = Field(default=0, description="Owner user ID")
    gid: int = Field(default=0, description="Owner group ID")
    mtime: datetime | None = Field(default=None, description="Modification time")
    link_target: str | None = Field(default=None, description="Symlink target")
    checksum: str | None = Field(default=None, description="SHA256 checksum")

    @property
    def mode_str(self) -> str:
        """Human-readable permission string (e.g., 'rwxr-xr-x')."""
        mode = self.mode & 0o777
        result = ""
        for shift in (6, 3, 0):
            bits = (mode >> shift) & 0o7
            result += "r" if bits & 4 else "-"
            result += "w" if bits & 2 else "-"
            result += "x" if bits & 1 else "-"
        return result


class FileChange(BaseModel):
    """A single file change between two filesystem states.

    Represents what changed for a specific path, including the old
    and new entry information.
    """

    path: str = Field(..., description="Path that changed")
    change_type: FileChangeType = Field(..., description="Type of change")
    source_entry: FileEntry | None = Field(
        default=None, description="Entry in source state"
    )
    target_entry: FileEntry | None = Field(
        default=None, description="Entry in target state"
    )
    size_delta: int = Field(default=0, description="Size change in bytes")

    @property
    def is_content_change(self) -> bool:
        """Whether this represents a content change (not just metadata)."""
        return self.change_type in (
            FileChangeType.ADDED,
            FileChangeType.DELETED,
            FileChangeType.MODIFIED,
        )


class FilesystemManifest(BaseModel):
    """Complete manifest of a container filesystem state.

    Captures all file entries for a container at a point in time,
    indexed by path for efficient comparison.
    """

    container_name: str = Field(..., description="Container name")
    runtime: str = Field(..., description="Runtime (docker, nspawn, libvirt)")
    source_type: Literal["container", "snapshot", "image"] = Field(
        ..., description="Source type"
    )
    source_id: str = Field(
        ..., description="Source identifier (container_id, snapshot name, or image)"
    )
    created_at: datetime = Field(
        default_factory=datetime.now, description="When manifest was created"
    )
    entries: dict[str, FileEntry] = Field(
        default_factory=dict, description="File entries by path"
    )
    total_files: int = Field(default=0, description="Total file count")
    total_dirs: int = Field(default=0, description="Total directory count")
    total_size_bytes: int = Field(default=0, description="Total size in bytes")

    def add_entry(self, entry: FileEntry) -> None:
        """Add a file entry to the manifest."""
        self.entries[entry.path] = entry
        if entry.file_type == FileType.DIRECTORY:
            self.total_dirs += 1
        else:
            self.total_files += 1
            self.total_size_bytes += entry.size

    def get_entry(self, path: str) -> FileEntry | None:
        """Get entry by path."""
        return self.entries.get(path)


class PackageInfo(BaseModel):
    """Information about an installed package."""

    name: str = Field(..., description="Package name")
    version: str = Field(..., description="Package version")
    architecture: str | None = Field(default=None, description="Architecture")
    source: str | None = Field(
        default=None, description="Package source (apt, dnf, apk, pacman)"
    )


class PackageChange(BaseModel):
    """A package version change between two states."""

    package_name: str = Field(..., description="Package name")
    change_type: FileChangeType = Field(..., description="Type of change")
    source_version: str | None = Field(default=None, description="Version in source")
    target_version: str | None = Field(default=None, description="Version in target")

    @property
    def version_diff(self) -> str:
        """Human-readable version change."""
        if self.change_type == FileChangeType.ADDED:
            return f"(none) -> {self.target_version}"
        elif self.change_type == FileChangeType.DELETED:
            return f"{self.source_version} -> (removed)"
        else:
            return f"{self.source_version} -> {self.target_version}"


class PackageDiff(BaseModel):
    """Package differences between two container states."""

    added: list[PackageInfo] = Field(default_factory=list, description="New packages")
    removed: list[PackageInfo] = Field(
        default_factory=list, description="Removed packages"
    )
    upgraded: list[PackageChange] = Field(
        default_factory=list, description="Upgraded packages"
    )
    downgraded: list[PackageChange] = Field(
        default_factory=list, description="Downgraded packages"
    )

    @property
    def total_changes(self) -> int:
        """Total number of package changes."""
        return (
            len(self.added)
            + len(self.removed)
            + len(self.upgraded)
            + len(self.downgraded)
        )

    @property
    def has_changes(self) -> bool:
        """Whether there are any package changes."""
        return self.total_changes > 0


class DiffSummary(BaseModel):
    """Summary statistics for a diff operation."""

    files_added: int = Field(default=0, description="Files added")
    files_deleted: int = Field(default=0, description="Files deleted")
    files_modified: int = Field(default=0, description="Files modified")
    files_permissions_changed: int = Field(
        default=0, description="Files with permission changes only"
    )
    dirs_added: int = Field(default=0, description="Directories added")
    dirs_deleted: int = Field(default=0, description="Directories deleted")
    bytes_added: int = Field(default=0, description="Bytes added")
    bytes_deleted: int = Field(default=0, description="Bytes deleted")
    packages_added: int = Field(default=0, description="Packages added")
    packages_removed: int = Field(default=0, description="Packages removed")
    packages_upgraded: int = Field(default=0, description="Packages upgraded")
    packages_downgraded: int = Field(default=0, description="Packages downgraded")

    @property
    def total_file_changes(self) -> int:
        """Total number of file changes."""
        return (
            self.files_added
            + self.files_deleted
            + self.files_modified
            + self.files_permissions_changed
        )

    @property
    def total_package_changes(self) -> int:
        """Total number of package changes."""
        return (
            self.packages_added
            + self.packages_removed
            + self.packages_upgraded
            + self.packages_downgraded
        )


class DiffResult(BaseModel):
    """Complete result of a diff operation.

    Contains all file changes, package changes, and summary statistics
    for comparing two container filesystem states.
    """

    source_name: str = Field(..., description="Source container/snapshot name")
    source_type: Literal["container", "snapshot", "image"] = Field(
        ..., description="Source type"
    )
    target_name: str = Field(..., description="Target container/snapshot name")
    target_type: Literal["container", "snapshot", "image"] = Field(
        ..., description="Target type"
    )
    runtime: str = Field(..., description="Runtime used")
    created_at: datetime = Field(
        default_factory=datetime.now, description="When diff was computed"
    )
    file_changes: list[FileChange] = Field(
        default_factory=list, description="File changes"
    )
    package_diff: PackageDiff | None = Field(
        default=None, description="Package differences"
    )
    summary: DiffSummary = Field(
        default_factory=DiffSummary, description="Summary statistics"
    )
    include_patterns: list[str] = Field(
        default_factory=list, description="Include patterns applied"
    )
    exclude_patterns: list[str] = Field(
        default_factory=list, description="Exclude patterns applied"
    )

    @property
    def has_changes(self) -> bool:
        """Whether there are any changes."""
        has_file_changes = len(self.file_changes) > 0
        has_package_changes = (
            self.package_diff is not None and self.package_diff.has_changes
        )
        return has_file_changes or has_package_changes


class DiffOptions(BaseModel):
    """Options for controlling diff behavior.

    Allows filtering paths, controlling what comparisons to make,
    and performance tuning.
    """

    # Path filtering
    include_paths: list[str] = Field(
        default_factory=list, description="Glob patterns to include (empty = all)"
    )
    exclude_paths: list[str] = Field(
        default_factory=lambda: [
            "/proc/*",
            "/sys/*",
            "/dev/*",
            "/run/*",
            "/tmp/*",
            "/var/run/*",
            "/var/lock/*",
        ],
        description="Glob patterns to exclude",
    )

    # Comparison options
    compare_content: bool = Field(
        default=False, description="Compare file content (slower)"
    )
    compare_permissions: bool = Field(
        default=True, description="Compare file permissions"
    )
    compare_ownership: bool = Field(
        default=False, description="Compare file ownership (uid/gid)"
    )
    compare_packages: bool = Field(
        default=True, description="Compare installed packages"
    )

    # Content options
    checksum_files: bool = Field(
        default=False, description="Calculate checksums for comparison"
    )
    max_file_size_kb: int = Field(
        default=1024, description="Max file size for content diff (KB)"
    )

    # Performance
    follow_symlinks: bool = Field(
        default=False, description="Follow symbolic links"
    )
