"""nspawn filesystem reader with direct filesystem access."""

from __future__ import annotations

import fnmatch
import logging
import os
import stat
import subprocess
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from gaol.diff.exceptions import (
    DiffFilesystemAccessError,
    DiffSnapshotNotFoundError,
    DiffSourceNotFoundError,
)
from gaol.diff.models import (
    DiffOptions,
    FileEntry,
    FilesystemManifest,
    FileType,
)
from gaol.diff.readers.base import FilesystemReader

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)

MACHINES_DIR = Path("/var/lib/machines")


class NspawnFilesystemReader(FilesystemReader):
    """Read nspawn container filesystems directly.

    nspawn containers store their filesystem at /var/lib/machines/{name}/,
    which allows efficient direct filesystem access without needing to
    extract tarballs or use container APIs.

    Snapshots are stored at /var/lib/machines/.gaol-snapshots-{name}/{snapshot}/
    """

    @property
    def runtime_name(self) -> str:
        return "nspawn"

    def read_container(
        self,
        container_id: str,
        options: DiffOptions | None = None,
    ) -> FilesystemManifest:
        """Read filesystem manifest from /var/lib/machines/{name}/.

        Args:
            container_id: Container name
            options: Diff options for filtering

        Returns:
            FilesystemManifest with all file entries
        """
        machine_path = MACHINES_DIR / container_id

        if not machine_path.exists():
            raise DiffSourceNotFoundError("container", container_id)

        return self._read_directory(
            root_path=machine_path,
            container_name=container_id,
            source_type="container",
            source_id=container_id,
            options=options,
        )

    def read_snapshot(
        self,
        container_name: str,
        snapshot_name: str,
        options: DiffOptions | None = None,
    ) -> FilesystemManifest:
        """Read filesystem manifest from snapshot directory.

        Snapshots are stored at:
        /var/lib/machines/.gaol-snapshots-{container}/{snapshot}/

        Args:
            container_name: Container name
            snapshot_name: Snapshot name
            options: Diff options for filtering

        Returns:
            FilesystemManifest with all file entries
        """
        # Check for snapshot in the standard location
        snapshots_dir = MACHINES_DIR / f".gaol-snapshots-{container_name}"
        snapshot_path = snapshots_dir / snapshot_name

        if not snapshot_path.exists():
            raise DiffSnapshotNotFoundError(container_name, snapshot_name)

        return self._read_directory(
            root_path=snapshot_path,
            container_name=container_name,
            source_type="snapshot",
            source_id=snapshot_name,
            options=options,
        )

    def read_file_content(
        self,
        container_id: str,
        path: str,
        max_size: int = 1024 * 1024,
    ) -> bytes | None:
        """Read file content directly from filesystem.

        Args:
            container_id: Container name
            path: File path within container
            max_size: Maximum bytes to read

        Returns:
            File content bytes or None if not found/too large
        """
        file_path = MACHINES_DIR / container_id / path.lstrip("/")

        try:
            if not file_path.exists() or not file_path.is_file():
                return None

            if file_path.stat().st_size > max_size:
                return None

            # Try direct read first
            try:
                return file_path.read_bytes()
            except PermissionError:
                # Fall back to sudo
                result = subprocess.run(
                    ["sudo", "cat", str(file_path)],
                    capture_output=True,
                    timeout=10,
                )
                if result.returncode == 0:
                    return result.stdout
                return None

        except Exception as e:
            logger.debug(f"Failed to read {path}: {e}")
            return None

    def _read_directory(
        self,
        root_path: Path,
        container_name: str,
        source_type: str,
        source_id: str,
        options: DiffOptions | None = None,
    ) -> FilesystemManifest:
        """Walk a directory tree and build a filesystem manifest.

        Args:
            root_path: Root directory to walk
            container_name: Container name for the manifest
            source_type: Type of source (container, snapshot)
            source_id: Source identifier
            options: Diff options for filtering

        Returns:
            FilesystemManifest with all entries
        """
        if options is None:
            options = DiffOptions()

        manifest = FilesystemManifest(
            container_name=container_name,
            runtime=self.runtime_name,
            source_type=source_type,  # type: ignore
            source_id=source_id,
        )

        try:
            # Use sudo ls to check if we can access
            if not self._can_access_directory(root_path):
                raise DiffFilesystemAccessError(
                    container_name,
                    "Cannot access filesystem (permission denied)",
                )

            # Walk the filesystem
            for entry in self._walk_filesystem(root_path, options):
                manifest.add_entry(entry)

        except DiffFilesystemAccessError:
            raise
        except Exception as e:
            raise DiffFilesystemAccessError(
                container_name, f"Error reading filesystem: {e}", e
            )

        return manifest

    def _can_access_directory(self, path: Path) -> bool:
        """Check if we can access a directory (possibly with sudo)."""
        try:
            # Try direct access
            list(path.iterdir())
            return True
        except PermissionError:
            # Try with sudo
            result = subprocess.run(
                ["sudo", "ls", str(path)],
                capture_output=True,
                timeout=5,
            )
            return result.returncode == 0

    def _walk_filesystem(
        self,
        root_path: Path,
        options: DiffOptions,
    ) -> list[FileEntry]:
        """Walk filesystem and return all entries.

        Uses sudo if necessary for permission.

        Args:
            root_path: Root directory to walk
            options: Diff options for filtering

        Returns:
            List of FileEntry objects
        """
        entries: list[FileEntry] = []

        # Try to get file listing with sudo find for reliability
        try:
            result = subprocess.run(
                [
                    "sudo", "find", str(root_path),
                    "-printf", "%P\\t%y\\t%s\\t%m\\t%U\\t%G\\t%T@\\t%l\\n"
                ],
                capture_output=True,
                text=True,
                timeout=300,  # 5 minute timeout for large filesystems
            )

            if result.returncode != 0:
                logger.warning(f"find command failed: {result.stderr}")
                # Fall back to Python walk
                return self._walk_filesystem_python(root_path, options)

            for line in result.stdout.strip().split("\n"):
                if not line:
                    continue

                entry = self._parse_find_line(line, root_path, options)
                if entry:
                    entries.append(entry)

        except subprocess.TimeoutExpired:
            logger.warning("find timed out, falling back to Python walk")
            return self._walk_filesystem_python(root_path, options)
        except Exception as e:
            logger.warning(f"find failed: {e}, falling back to Python walk")
            return self._walk_filesystem_python(root_path, options)

        return entries

    def _parse_find_line(
        self,
        line: str,
        root_path: Path,
        options: DiffOptions,
    ) -> FileEntry | None:
        """Parse a line from find output.

        Format: path\ttype\tsize\tmode\tuid\tgid\tmtime\tlink_target
        """
        try:
            parts = line.split("\t")
            if len(parts) < 7:
                return None

            rel_path = parts[0]
            if not rel_path:
                rel_path = "/"
            else:
                rel_path = "/" + rel_path

            # Check exclusions
            if not self._should_include_path(rel_path, options):
                return None

            file_type_char = parts[1]
            size = int(parts[2]) if parts[2] else 0
            mode = int(parts[3], 8) if parts[3] else 0o644
            uid = int(parts[4]) if parts[4] else 0
            gid = int(parts[5]) if parts[5] else 0

            # Parse mtime (Unix timestamp with decimals)
            mtime = None
            if parts[6]:
                try:
                    mtime = datetime.fromtimestamp(float(parts[6]))
                except (ValueError, OSError):
                    pass

            link_target = parts[7] if len(parts) > 7 and parts[7] else None

            # Map find type to FileType
            file_type = self._find_type_to_file_type(file_type_char)

            return FileEntry(
                path=rel_path,
                file_type=file_type,
                size=size,
                mode=mode,
                uid=uid,
                gid=gid,
                mtime=mtime,
                link_target=link_target,
            )

        except Exception as e:
            logger.debug(f"Failed to parse find line '{line}': {e}")
            return None

    def _find_type_to_file_type(self, type_char: str) -> FileType:
        """Convert find type character to FileType enum."""
        mapping = {
            "f": FileType.FILE,
            "d": FileType.DIRECTORY,
            "l": FileType.SYMLINK,
            "c": FileType.DEVICE,
            "b": FileType.DEVICE,
            "s": FileType.SOCKET,
            "p": FileType.FIFO,
        }
        return mapping.get(type_char, FileType.UNKNOWN)

    def _walk_filesystem_python(
        self,
        root_path: Path,
        options: DiffOptions,
    ) -> list[FileEntry]:
        """Walk filesystem using Python's os.walk (fallback).

        This may fail on permission-restricted directories without sudo.
        """
        entries: list[FileEntry] = []

        for dirpath, dirnames, filenames in os.walk(
            root_path, followlinks=options.follow_symlinks
        ):
            # Get relative path from root
            rel_dir = "/" + str(Path(dirpath).relative_to(root_path))
            if rel_dir == "/.":
                rel_dir = "/"

            # Add directory entry
            if self._should_include_path(rel_dir, options):
                try:
                    dir_stat = os.lstat(dirpath)
                    entries.append(self._stat_to_entry(rel_dir, dir_stat))
                except OSError:
                    pass

            # Add file entries
            for filename in filenames:
                file_path = Path(dirpath) / filename
                rel_path = rel_dir.rstrip("/") + "/" + filename

                if not self._should_include_path(rel_path, options):
                    continue

                try:
                    file_stat = os.lstat(file_path)
                    entries.append(self._stat_to_entry(rel_path, file_stat))
                except OSError:
                    pass

        return entries

    def _stat_to_entry(self, path: str, stat_result: os.stat_result) -> FileEntry:
        """Convert stat result to FileEntry."""
        # Determine file type
        mode = stat_result.st_mode
        if stat.S_ISREG(mode):
            file_type = FileType.FILE
        elif stat.S_ISDIR(mode):
            file_type = FileType.DIRECTORY
        elif stat.S_ISLNK(mode):
            file_type = FileType.SYMLINK
        elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode):
            file_type = FileType.DEVICE
        elif stat.S_ISSOCK(mode):
            file_type = FileType.SOCKET
        elif stat.S_ISFIFO(mode):
            file_type = FileType.FIFO
        else:
            file_type = FileType.UNKNOWN

        return FileEntry(
            path=path,
            file_type=file_type,
            size=stat_result.st_size,
            mode=stat_result.st_mode & 0o7777,
            uid=stat_result.st_uid,
            gid=stat_result.st_gid,
            mtime=datetime.fromtimestamp(stat_result.st_mtime),
        )

    def _should_include_path(self, path: str, options: DiffOptions) -> bool:
        """Check if path should be included based on options."""
        # Check exclusions
        for pattern in options.exclude_paths:
            if fnmatch.fnmatch(path, pattern):
                return False

        # Check inclusions
        if options.include_paths:
            for pattern in options.include_paths:
                if fnmatch.fnmatch(path, pattern):
                    return True
            return False

        return True
