"""Output formatters for diff results.

Supports console (human-readable) and JSON output formats.
"""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

from gaol.diff.models import (
    DiffResult,
    FileChange,
    FileChangeType,
    FileType,
    PackageDiff,
)

if TYPE_CHECKING:
    pass


def format_bytes(size: int) -> str:
    """Format byte size as human-readable string."""
    if size < 0:
        return f"-{format_bytes(abs(size))}"

    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if abs(size) < 1024.0:
            if unit == "B":
                return f"{size}{unit}"
            return f"{size:.1f}{unit}"
        size = int(size / 1024)
    return f"{size:.1f}PB"


class DiffFormatter(ABC):
    """Abstract base for diff output formatters."""

    @abstractmethod
    def format(self, result: DiffResult) -> str:
        """Format a diff result for output.

        Args:
            result: The diff result to format

        Returns:
            Formatted string output
        """
        pass


class ConsoleFormatter(DiffFormatter):
    """Human-readable console output formatter.

    Produces colored diff output similar to git diff.
    """

    # ANSI color codes
    RED = "\033[31m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    BLUE = "\033[34m"
    CYAN = "\033[36m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

    def __init__(
        self,
        *,
        color: bool = True,
        show_details: bool = True,
        show_packages: bool = True,
        max_changes: int | None = None,
    ) -> None:
        """Initialize console formatter.

        Args:
            color: Enable ANSI colors
            show_details: Show file size and mode details
            show_packages: Show package changes
            max_changes: Maximum changes to show (None = all)
        """
        self.color = color
        self.show_details = show_details
        self.show_packages = show_packages
        self.max_changes = max_changes

    def _c(self, code: str, text: str) -> str:
        """Apply color code if colors enabled."""
        if self.color:
            return f"{code}{text}{self.RESET}"
        return text

    def format(self, result: DiffResult) -> str:
        """Format diff result for console display."""
        lines: list[str] = []

        # Header
        lines.append(self._format_header(result))
        lines.append("")

        # File changes
        if result.file_changes:
            lines.append(self._format_file_changes(result))
        else:
            lines.append(self._c(self.CYAN, "No file changes detected"))

        # Package changes
        if self.show_packages and result.package_diff:
            lines.append("")
            lines.append(self._format_package_diff(result.package_diff))

        # Summary
        lines.append("")
        lines.append(self._format_summary(result))

        return "\n".join(lines)

    def _format_header(self, result: DiffResult) -> str:
        """Format diff header."""
        header = [
            self._c(self.BOLD, "Container Diff"),
            f"  Source: {result.source_name} ({result.source_type})",
            f"  Target: {result.target_name} ({result.target_type})",
            f"  Runtime: {result.runtime}",
        ]

        if result.include_patterns:
            header.append(f"  Include: {', '.join(result.include_patterns)}")
        if result.exclude_patterns:
            # Only show if non-default
            non_default = [p for p in result.exclude_patterns
                         if p not in ["/proc/*", "/sys/*", "/dev/*", "/run/*", "/tmp/*"]]
            if non_default:
                header.append(f"  Exclude: {', '.join(non_default)}")

        return "\n".join(header)

    def _format_file_changes(self, result: DiffResult) -> str:
        """Format file change list."""
        lines: list[str] = []
        lines.append(self._c(self.BOLD, "File Changes:"))

        changes = result.file_changes
        truncated = False

        if self.max_changes and len(changes) > self.max_changes:
            changes = changes[: self.max_changes]
            truncated = True

        for change in changes:
            lines.append(self._format_single_change(change))

        if truncated:
            remaining = len(result.file_changes) - self.max_changes
            lines.append(
                self._c(self.YELLOW, f"  ... and {remaining} more changes")
            )

        return "\n".join(lines)

    def _format_single_change(self, change: FileChange) -> str:
        """Format a single file change."""
        # Type indicator
        if change.target_entry:
            type_char = self._type_char(change.target_entry.file_type)
        elif change.source_entry:
            type_char = self._type_char(change.source_entry.file_type)
        else:
            type_char = "?"

        # Change type with color
        if change.change_type == FileChangeType.ADDED:
            prefix = self._c(self.GREEN, f"+{type_char}")
            path = self._c(self.GREEN, change.path)
        elif change.change_type == FileChangeType.DELETED:
            prefix = self._c(self.RED, f"-{type_char}")
            path = self._c(self.RED, change.path)
        elif change.change_type == FileChangeType.MODIFIED:
            prefix = self._c(self.YELLOW, f"M{type_char}")
            path = self._c(self.YELLOW, change.path)
        elif change.change_type == FileChangeType.PERMISSIONS:
            prefix = self._c(self.BLUE, f"P{type_char}")
            path = self._c(self.BLUE, change.path)
        elif change.change_type == FileChangeType.TYPE_CHANGED:
            prefix = self._c(self.CYAN, f"T{type_char}")
            path = self._c(self.CYAN, change.path)
        else:
            prefix = f"?{type_char}"
            path = change.path

        line = f"  {prefix} {path}"

        # Add details
        if self.show_details:
            details = []

            # Size change
            if change.size_delta != 0:
                size_str = format_bytes(change.size_delta)
                if change.size_delta > 0:
                    details.append(self._c(self.GREEN, f"+{size_str}"))
                else:
                    details.append(self._c(self.RED, size_str))

            # Permission change
            if change.change_type == FileChangeType.PERMISSIONS:
                old_mode = change.source_entry.mode_str if change.source_entry else "---"
                new_mode = change.target_entry.mode_str if change.target_entry else "---"
                details.append(f"{old_mode} -> {new_mode}")

            # Symlink target
            if change.target_entry and change.target_entry.link_target:
                details.append(f"-> {change.target_entry.link_target}")

            if details:
                line += f"  ({', '.join(details)})"

        return line

    def _type_char(self, file_type: FileType) -> str:
        """Single character for file type."""
        mapping = {
            FileType.FILE: "f",
            FileType.DIRECTORY: "d",
            FileType.SYMLINK: "l",
            FileType.DEVICE: "c",
            FileType.SOCKET: "s",
            FileType.FIFO: "p",
        }
        return mapping.get(file_type, "?")

    def _format_package_diff(self, pkg_diff: PackageDiff) -> str:
        """Format package changes."""
        lines: list[str] = []
        lines.append(self._c(self.BOLD, "Package Changes:"))

        # Added packages
        for pkg in pkg_diff.added:
            lines.append(
                self._c(self.GREEN, f"  + {pkg.name} {pkg.version}")
            )

        # Removed packages
        for pkg in pkg_diff.removed:
            lines.append(
                self._c(self.RED, f"  - {pkg.name} {pkg.version}")
            )

        # Upgraded packages
        for change in pkg_diff.upgraded:
            lines.append(
                self._c(
                    self.CYAN,
                    f"  ^ {change.package_name} {change.source_version} -> {change.target_version}",
                )
            )

        # Downgraded packages
        for change in pkg_diff.downgraded:
            lines.append(
                self._c(
                    self.YELLOW,
                    f"  v {change.package_name} {change.source_version} -> {change.target_version}",
                )
            )

        if not pkg_diff.has_changes:
            lines.append(self._c(self.CYAN, "  No package changes"))

        return "\n".join(lines)

    def _format_summary(self, result: DiffResult) -> str:
        """Format summary statistics."""
        s = result.summary
        lines: list[str] = []
        lines.append(self._c(self.BOLD, "Summary:"))

        # File changes
        file_parts = []
        if s.files_added:
            file_parts.append(self._c(self.GREEN, f"{s.files_added} added"))
        if s.files_deleted:
            file_parts.append(self._c(self.RED, f"{s.files_deleted} deleted"))
        if s.files_modified:
            file_parts.append(self._c(self.YELLOW, f"{s.files_modified} modified"))
        if s.files_permissions_changed:
            file_parts.append(
                self._c(self.BLUE, f"{s.files_permissions_changed} permissions changed")
            )

        if file_parts:
            lines.append(f"  Files: {', '.join(file_parts)}")
        else:
            lines.append("  Files: no changes")

        # Directory changes
        if s.dirs_added or s.dirs_deleted:
            dir_parts = []
            if s.dirs_added:
                dir_parts.append(self._c(self.GREEN, f"{s.dirs_added} added"))
            if s.dirs_deleted:
                dir_parts.append(self._c(self.RED, f"{s.dirs_deleted} deleted"))
            lines.append(f"  Directories: {', '.join(dir_parts)}")

        # Size changes
        if s.bytes_added or s.bytes_deleted:
            size_parts = []
            if s.bytes_added:
                size_parts.append(self._c(self.GREEN, f"+{format_bytes(s.bytes_added)}"))
            if s.bytes_deleted:
                size_parts.append(self._c(self.RED, f"-{format_bytes(s.bytes_deleted)}"))
            lines.append(f"  Size: {', '.join(size_parts)}")

        # Package changes
        if s.total_package_changes > 0:
            pkg_parts = []
            if s.packages_added:
                pkg_parts.append(self._c(self.GREEN, f"{s.packages_added} added"))
            if s.packages_removed:
                pkg_parts.append(self._c(self.RED, f"{s.packages_removed} removed"))
            if s.packages_upgraded:
                pkg_parts.append(self._c(self.CYAN, f"{s.packages_upgraded} upgraded"))
            if s.packages_downgraded:
                pkg_parts.append(
                    self._c(self.YELLOW, f"{s.packages_downgraded} downgraded")
                )
            lines.append(f"  Packages: {', '.join(pkg_parts)}")

        return "\n".join(lines)


class JsonFormatter(DiffFormatter):
    """JSON output formatter for machine-readable output."""

    def __init__(self, *, indent: int = 2, include_entries: bool = False) -> None:
        """Initialize JSON formatter.

        Args:
            indent: JSON indentation level
            include_entries: Include full file entry objects
        """
        self.indent = indent
        self.include_entries = include_entries

    def format(self, result: DiffResult) -> str:
        """Format diff result as JSON."""
        data = {
            "source": {
                "name": result.source_name,
                "type": result.source_type,
            },
            "target": {
                "name": result.target_name,
                "type": result.target_type,
            },
            "runtime": result.runtime,
            "created_at": result.created_at.isoformat(),
            "has_changes": result.has_changes,
            "summary": {
                "files_added": result.summary.files_added,
                "files_deleted": result.summary.files_deleted,
                "files_modified": result.summary.files_modified,
                "files_permissions_changed": result.summary.files_permissions_changed,
                "dirs_added": result.summary.dirs_added,
                "dirs_deleted": result.summary.dirs_deleted,
                "bytes_added": result.summary.bytes_added,
                "bytes_deleted": result.summary.bytes_deleted,
                "packages_added": result.summary.packages_added,
                "packages_removed": result.summary.packages_removed,
                "packages_upgraded": result.summary.packages_upgraded,
                "packages_downgraded": result.summary.packages_downgraded,
                "total_file_changes": result.summary.total_file_changes,
                "total_package_changes": result.summary.total_package_changes,
            },
            "file_changes": [
                self._format_file_change(c) for c in result.file_changes
            ],
        }

        if result.package_diff:
            data["package_diff"] = {
                "added": [
                    {"name": p.name, "version": p.version, "architecture": p.architecture}
                    for p in result.package_diff.added
                ],
                "removed": [
                    {"name": p.name, "version": p.version, "architecture": p.architecture}
                    for p in result.package_diff.removed
                ],
                "upgraded": [
                    {
                        "name": c.package_name,
                        "from_version": c.source_version,
                        "to_version": c.target_version,
                    }
                    for c in result.package_diff.upgraded
                ],
                "downgraded": [
                    {
                        "name": c.package_name,
                        "from_version": c.source_version,
                        "to_version": c.target_version,
                    }
                    for c in result.package_diff.downgraded
                ],
            }

        if result.include_patterns:
            data["include_patterns"] = result.include_patterns
        if result.exclude_patterns:
            data["exclude_patterns"] = result.exclude_patterns

        return json.dumps(data, indent=self.indent)

    def _format_file_change(self, change: FileChange) -> dict:
        """Format a single file change for JSON."""
        data = {
            "path": change.path,
            "change_type": change.change_type.value,
            "size_delta": change.size_delta,
        }

        if self.include_entries:
            if change.source_entry:
                data["source"] = {
                    "file_type": change.source_entry.file_type.value,
                    "size": change.source_entry.size,
                    "mode": oct(change.source_entry.mode),
                    "uid": change.source_entry.uid,
                    "gid": change.source_entry.gid,
                }
                if change.source_entry.link_target:
                    data["source"]["link_target"] = change.source_entry.link_target

            if change.target_entry:
                data["target"] = {
                    "file_type": change.target_entry.file_type.value,
                    "size": change.target_entry.size,
                    "mode": oct(change.target_entry.mode),
                    "uid": change.target_entry.uid,
                    "gid": change.target_entry.gid,
                }
                if change.target_entry.link_target:
                    data["target"]["link_target"] = change.target_entry.link_target
        else:
            # Include minimal info
            if change.source_entry:
                data["source_size"] = change.source_entry.size
            if change.target_entry:
                data["target_size"] = change.target_entry.size
                if change.target_entry.link_target:
                    data["link_target"] = change.target_entry.link_target

        return data


def get_formatter(
    output_format: str = "console",
    **kwargs,
) -> DiffFormatter:
    """Get appropriate formatter for output format.

    Args:
        output_format: Format name (console, json)
        **kwargs: Additional formatter arguments

    Returns:
        DiffFormatter instance
    """
    if output_format == "json":
        return JsonFormatter(**kwargs)
    else:
        return ConsoleFormatter(**kwargs)
