"""Core diff computation engine."""

from __future__ import annotations

import fnmatch
import logging
from datetime import datetime

from gaol.diff.models import (
    DiffOptions,
    DiffResult,
    DiffSummary,
    FileChange,
    FileChangeType,
    FileEntry,
    FilesystemManifest,
    FileType,
    PackageChange,
    PackageDiff,
    PackageInfo,
)

logger = logging.getLogger(__name__)


class DiffEngine:
    """Computes differences between filesystem manifests.

    Takes two FilesystemManifest objects and produces a DiffResult
    showing all file and package changes between them.

    Example:
        >>> engine = DiffEngine()
        >>> result = engine.compute_diff(source_manifest, target_manifest)
        >>> print(f"Files changed: {result.summary.total_file_changes}")
    """

    def compute_diff(
        self,
        source: FilesystemManifest,
        target: FilesystemManifest,
        options: DiffOptions | None = None,
    ) -> DiffResult:
        """Compute diff between two filesystem states.

        Args:
            source: Source filesystem manifest (the "before" state)
            target: Target filesystem manifest (the "after" state)
            options: Options controlling diff behavior

        Returns:
            DiffResult with all changes and summary statistics
        """
        if options is None:
            options = DiffOptions()

        file_changes: list[FileChange] = []
        summary = DiffSummary()

        # Get all paths from both manifests
        source_paths = set(source.entries.keys())
        target_paths = set(target.entries.keys())

        # Find added paths (in target but not source)
        added_paths = target_paths - source_paths
        for path in sorted(added_paths):
            if not self._should_include_path(path, options):
                continue

            target_entry = target.entries[path]
            change = FileChange(
                path=path,
                change_type=FileChangeType.ADDED,
                target_entry=target_entry,
                size_delta=target_entry.size,
            )
            file_changes.append(change)
            self._update_summary_for_add(summary, target_entry)

        # Find deleted paths (in source but not target)
        deleted_paths = source_paths - target_paths
        for path in sorted(deleted_paths):
            if not self._should_include_path(path, options):
                continue

            source_entry = source.entries[path]
            change = FileChange(
                path=path,
                change_type=FileChangeType.DELETED,
                source_entry=source_entry,
                size_delta=-source_entry.size,
            )
            file_changes.append(change)
            self._update_summary_for_delete(summary, source_entry)

        # Find modified paths (in both)
        common_paths = source_paths & target_paths
        for path in sorted(common_paths):
            if not self._should_include_path(path, options):
                continue

            source_entry = source.entries[path]
            target_entry = target.entries[path]

            change = self._compare_entries(source_entry, target_entry, options)
            if change:
                file_changes.append(change)
                self._update_summary_for_change(summary, change)

        result = DiffResult(
            source_name=source.source_id,
            source_type=source.source_type,
            target_name=target.source_id,
            target_type=target.source_type,
            runtime=source.runtime,
            created_at=datetime.now(),
            file_changes=file_changes,
            summary=summary,
            include_patterns=options.include_paths,
            exclude_patterns=options.exclude_paths,
        )

        return result

    def compute_package_diff(
        self,
        source_packages: list[PackageInfo],
        target_packages: list[PackageInfo],
    ) -> PackageDiff:
        """Compare two package lists.

        Args:
            source_packages: Packages in source state
            target_packages: Packages in target state

        Returns:
            PackageDiff with added, removed, and version changes
        """
        # Index packages by name
        source_by_name = {p.name: p for p in source_packages}
        target_by_name = {p.name: p for p in target_packages}

        source_names = set(source_by_name.keys())
        target_names = set(target_by_name.keys())

        diff = PackageDiff()

        # Added packages
        for name in sorted(target_names - source_names):
            diff.added.append(target_by_name[name])

        # Removed packages
        for name in sorted(source_names - target_names):
            diff.removed.append(source_by_name[name])

        # Changed packages
        for name in sorted(source_names & target_names):
            source_pkg = source_by_name[name]
            target_pkg = target_by_name[name]

            if source_pkg.version != target_pkg.version:
                change = PackageChange(
                    package_name=name,
                    change_type=FileChangeType.MODIFIED,
                    source_version=source_pkg.version,
                    target_version=target_pkg.version,
                )

                # Determine if upgrade or downgrade (simple string comparison)
                if self._is_version_upgrade(source_pkg.version, target_pkg.version):
                    diff.upgraded.append(change)
                else:
                    diff.downgraded.append(change)

        return diff

    def _should_include_path(self, path: str, options: DiffOptions) -> bool:
        """Check if a path should be included based on filters.

        Args:
            path: The filesystem path to check
            options: Diff options with include/exclude patterns

        Returns:
            True if path should be included in diff
        """
        # Check exclude patterns first
        for pattern in options.exclude_paths:
            if fnmatch.fnmatch(path, pattern):
                return False

        # If include patterns specified, path must match at least one
        if options.include_paths:
            for pattern in options.include_paths:
                if fnmatch.fnmatch(path, pattern):
                    return True
            return False

        return True

    def _compare_entries(
        self,
        source: FileEntry,
        target: FileEntry,
        options: DiffOptions,
    ) -> FileChange | None:
        """Compare two file entries and return change if different.

        Args:
            source: Source file entry
            target: Target file entry
            options: Diff options

        Returns:
            FileChange if entries differ, None if identical
        """
        # Check for type change
        if source.file_type != target.file_type:
            return FileChange(
                path=source.path,
                change_type=FileChangeType.TYPE_CHANGED,
                source_entry=source,
                target_entry=target,
                size_delta=target.size - source.size,
            )

        # Check for content modification
        content_changed = False

        # Size change indicates content change for files
        if source.file_type == FileType.FILE:
            if source.size != target.size:
                content_changed = True
            elif source.mtime != target.mtime:
                # Same size but different mtime - might be modified
                if options.compare_content or options.checksum_files:
                    # Checksums would be compared here if available
                    if source.checksum and target.checksum:
                        content_changed = source.checksum != target.checksum
                    else:
                        # Without checksums, assume mtime change means content change
                        content_changed = True

        # Symlink target change
        if source.file_type == FileType.SYMLINK:
            if source.link_target != target.link_target:
                content_changed = True

        if content_changed:
            return FileChange(
                path=source.path,
                change_type=FileChangeType.MODIFIED,
                source_entry=source,
                target_entry=target,
                size_delta=target.size - source.size,
            )

        # Check for permission changes
        if options.compare_permissions:
            if source.mode != target.mode:
                return FileChange(
                    path=source.path,
                    change_type=FileChangeType.PERMISSIONS,
                    source_entry=source,
                    target_entry=target,
                )

        # Check for ownership changes
        if options.compare_ownership:
            if source.uid != target.uid or source.gid != target.gid:
                return FileChange(
                    path=source.path,
                    change_type=FileChangeType.PERMISSIONS,
                    source_entry=source,
                    target_entry=target,
                )

        return None

    def _update_summary_for_add(
        self, summary: DiffSummary, entry: FileEntry
    ) -> None:
        """Update summary for an added file."""
        if entry.file_type == FileType.DIRECTORY:
            summary.dirs_added += 1
        else:
            summary.files_added += 1
            summary.bytes_added += entry.size

    def _update_summary_for_delete(
        self, summary: DiffSummary, entry: FileEntry
    ) -> None:
        """Update summary for a deleted file."""
        if entry.file_type == FileType.DIRECTORY:
            summary.dirs_deleted += 1
        else:
            summary.files_deleted += 1
            summary.bytes_deleted += entry.size

    def _update_summary_for_change(
        self, summary: DiffSummary, change: FileChange
    ) -> None:
        """Update summary for a changed file."""
        if change.change_type == FileChangeType.MODIFIED:
            summary.files_modified += 1
            if change.size_delta > 0:
                summary.bytes_added += change.size_delta
            else:
                summary.bytes_deleted += abs(change.size_delta)
        elif change.change_type == FileChangeType.PERMISSIONS:
            summary.files_permissions_changed += 1
        elif change.change_type == FileChangeType.TYPE_CHANGED:
            summary.files_modified += 1

    def _is_version_upgrade(self, old_version: str, new_version: str) -> bool:
        """Check if new_version is newer than old_version.

        Simple string comparison - could be enhanced with proper
        version parsing (e.g., using packaging.version).

        Args:
            old_version: Old version string
            new_version: New version string

        Returns:
            True if new_version appears to be newer
        """
        # Try numeric comparison for simple versions
        try:
            # Split on common delimiters and compare parts
            old_parts = old_version.replace("-", ".").replace("_", ".").split(".")
            new_parts = new_version.replace("-", ".").replace("_", ".").split(".")

            for old_part, new_part in zip(old_parts, new_parts):
                try:
                    old_num = int(old_part)
                    new_num = int(new_part)
                    if new_num > old_num:
                        return True
                    elif new_num < old_num:
                        return False
                except ValueError:
                    # Non-numeric, compare as strings
                    if new_part > old_part:
                        return True
                    elif new_part < old_part:
                        return False

            # If all equal parts, longer version is newer
            return len(new_parts) > len(old_parts)

        except Exception:
            # Fall back to string comparison
            return new_version > old_version

    def update_summary_with_packages(
        self, summary: DiffSummary, package_diff: PackageDiff
    ) -> None:
        """Update summary with package diff statistics.

        Args:
            summary: Summary to update
            package_diff: Package differences
        """
        summary.packages_added = len(package_diff.added)
        summary.packages_removed = len(package_diff.removed)
        summary.packages_upgraded = len(package_diff.upgraded)
        summary.packages_downgraded = len(package_diff.downgraded)
