"""Dulwich-based tape file repository implementation."""

from __future__ import annotations

import io
import logging
from pathlib import Path

from dulwich.objects import Blob, Tree
from dulwich.repo import Repo

from supertape.core.file.api import TapeFile
from supertape.core.file.load import file_load
from supertape.core.file.save import file_save
from supertape.core.repository.api import (
    RepositoryInfo,
    TapeFileRepository,
    TapeFileRepositoryObserver,
    TapeVersion,
)

logger = logging.getLogger(__name__)


class RepositoryError(Exception):
    """Exception raised for repository errors."""

    pass


class DulwichRepository(TapeFileRepository):
    """Dulwich-based tape file repository implementation.

    Stores tape files as .k7 binary files in a git repository using pure Python.
    Each add/remove operation creates an automatic commit.
    Uses Dulwich for pure Python git integration (no external git binary required).
    """

    def __init__(self, repository_dir: str | None, observers: list[TapeFileRepositoryObserver]) -> None:
        """Initialize Dulwich repository.

        Args:
            repository_dir: Directory to store .k7 files, or None for default (~/.supertape/tapes)
            observers: List of observers to notify on repository changes

        Raises:
            RepositoryError: If dulwich is not installed or repository initialization fails
        """
        self.repository_dir: Path = (
            Path(repository_dir) if repository_dir else Path.home() / ".supertape" / "tapes"
        )
        self.observers: list[TapeFileRepositoryObserver] = observers

        # Create repository directory if it doesn't exist
        self.repository_dir.mkdir(parents=True, exist_ok=True)

        # Ensure git repository is initialized
        self._ensure_git_repo()

    def __str__(self) -> str:
        """String representation of the repository."""
        return f"DulwichRepository at {self.repository_dir}"

    def _ensure_git_repo(self) -> None:
        """Ensure git repository is initialized using Dulwich.

        Creates a new git repository if one doesn't exist.
        Raises RepositoryError if dulwich is not available.
        """
        git_dir = self.repository_dir / ".git"

        if not git_dir.exists():
            try:
                # Initialize new git repository using Dulwich
                Repo.init(str(self.repository_dir))
                logger.info(f"Initialized git repository at {self.repository_dir}")

                # Create .gitignore file
                gitignore = self.repository_dir / ".gitignore"
                if not gitignore.exists():
                    gitignore.write_text("__pycache__/\n*.pyc\n")

                    # Add and commit .gitignore
                    repo = Repo(str(self.repository_dir))
                    worktree = repo.get_worktree()
                    worktree.stage([b".gitignore"])
                    worktree.commit(
                        b"Initial commit: Add .gitignore",
                        committer=b"Supertape <supertape@local>",
                        author=b"Supertape <supertape@local>",
                    )

            except ImportError as e:
                raise RepositoryError(
                    "Dulwich is not installed.\n" "Please install it: pip install dulwich"
                ) from e
            except Exception as e:
                raise RepositoryError(f"Failed to initialize git repository: {e}") from e

    def _get_file_path(self, tape: TapeFile) -> Path:
        """Get the file path for a tape file.

        Args:
            tape: TapeFile to get path for

        Returns:
            Path where the tape file should be stored (.k7 extension)
        """
        # Sanitize filename: keep alphanumeric, spaces, hyphens, underscores
        # Normalize to uppercase for case-insensitive storage (CSAVEM format)
        safe_name: str = "".join(c for c in tape.fname.upper() if c.isalnum() or c in (" ", "-", "_")).strip()
        if not safe_name:
            safe_name = f"tape_{id(tape)}"

        return self.repository_dir / f"{safe_name}.k7"

    def add_tape_file(self, file: TapeFile) -> None:
        """Add a tape file to the repository.

        Args:
            file: TapeFile to add to the repository

        Raises:
            RepositoryError: If git operations fail
        """
        file_path: Path = self._get_file_path(file)

        # Handle duplicate names by appending a number
        counter: int = 1
        original_path: Path = file_path
        while file_path.exists():
            stem: str = original_path.stem
            file_path = original_path.parent / f"{stem}_{counter}.k7"
            counter += 1

        try:
            # Write .k7 file
            file_save(str(file_path), file)

            # Add to git and commit using Dulwich
            repo = Repo(str(self.repository_dir))
            worktree = repo.get_worktree()

            # Stage the file (relative path as bytes)
            relative_path = file_path.relative_to(self.repository_dir)
            worktree.stage([str(relative_path).encode("utf-8")])

            # Commit
            commit_msg = f"Add tape file: {file.fname}".encode()
            author = b"Supertape <supertape@local>"
            worktree.commit(commit_msg, committer=author, author=author)

            logger.info(f"Added tape file: {file.fname} -> {file_path.name}")

            # Notify observers
            for observer in self.observers:
                observer.file_added(file)

        except Exception as e:
            # Rollback: remove file if commit failed
            if file_path.exists():
                file_path.unlink()
            raise RepositoryError(f"Failed to add tape file: {e}") from e

    def remove_tape_file(self, file: TapeFile) -> None:
        """Remove a tape file from the repository.

        Args:
            file: TapeFile to remove from the repository

        Raises:
            RepositoryError: If git operations fail
        """
        # Find and remove the file
        for k7_file in self.repository_dir.glob("*.k7"):
            try:
                existing_tape: TapeFile = file_load(str(k7_file))
                # Case-insensitive comparison (CSAVEM format treats names as uppercase)
                if existing_tape.fname.upper() == file.fname.upper() and existing_tape == file:
                    try:
                        # Remove file from filesystem
                        k7_file.unlink()

                        # Stage deletion and commit using Dulwich
                        repo = Repo(str(self.repository_dir))
                        worktree = repo.get_worktree()
                        relative_path = k7_file.relative_to(self.repository_dir)
                        worktree.stage([str(relative_path).encode("utf-8")])

                        commit_msg = f"Remove tape file: {file.fname}".encode()
                        author = b"Supertape <supertape@local>"
                        worktree.commit(commit_msg, committer=author, author=author)

                        logger.info(f"Removed tape file: {file.fname} ({k7_file.name})")

                        # Notify observers
                        for observer in self.observers:
                            observer.file_removed(file)

                        return

                    except Exception as e:
                        raise RepositoryError(f"Failed to remove tape file: {e}") from e

            except (ValueError, OSError) as e:
                # Skip corrupted files
                logger.warning(f"Skipping corrupted file {k7_file}: {e}")
                continue

    def get_tape_files(self) -> list[TapeFile]:
        """Get all tape files from the repository.

        Returns:
            List of all TapeFile objects in the repository
        """
        tapes: list[TapeFile] = []

        for k7_file in self.repository_dir.glob("*.k7"):
            try:
                tape: TapeFile = file_load(str(k7_file))
                tapes.append(tape)
            except (ValueError, OSError) as e:
                # Skip corrupted files
                logger.warning(f"Skipping corrupted file {k7_file}: {e}")
                continue

        return tapes

    def add_observer(self, observer: TapeFileRepositoryObserver) -> None:
        """Add an observer to watch repository events.

        Args:
            observer: Observer to add
        """
        self.observers.append(observer)

    def remove_observer(self, observer: TapeFileRepositoryObserver) -> None:
        """Remove an observer from watching repository events.

        Args:
            observer: Observer to remove
        """
        self.observers.remove(observer)

    def get_repository_info(self) -> RepositoryInfo:
        """Get information about the repository.

        Returns:
            RepositoryInfo object containing:
            - file_count: Number of .k7 files in the repository
            - path: Repository storage path (absolute)
            - type: Repository type ("git")
            - storage_size: Total size of stored .k7 files in bytes
        """
        k7_files = list(self.repository_dir.glob("*.k7"))
        file_count = len(k7_files)

        # Calculate total storage size
        storage_size = sum(k7_file.stat().st_size for k7_file in k7_files)

        return RepositoryInfo(
            file_count=file_count,
            path=str(self.repository_dir.absolute()),
            type="git",  # Still "git" since we're using git format
            storage_size=storage_size,
        )

    def get_tape_file_versions(self, filename: str) -> list[TapeVersion]:
        """Get all historical versions of a tape file.

        Walks through the git history to find all commits that affected the
        specified tape file, ordered from newest to oldest.

        Args:
            filename: The original tape filename (not sanitized, as stored in
                      TapeFile.fname). The method will handle sanitization internally.

        Returns:
            List of TapeVersion objects, ordered from newest to oldest.
            Empty list if the file was never committed to the repository.

        Raises:
            RepositoryError: If git operations fail or history cannot be accessed.
        """
        try:
            # Sanitize filename: keep alphanumeric, spaces, hyphens, underscores
            # Normalize to uppercase for case-insensitive lookup (CSAVEM format)
            safe_name: str = "".join(
                c for c in filename.upper() if c.isalnum() or c in (" ", "-", "_")
            ).strip()
            if not safe_name:
                safe_name = filename  # Fallback to original if all chars were removed

            # Construct file path
            relative_path = Path(f"{safe_name}.k7")
            path_bytes = str(relative_path).encode("utf-8")

            # Access git repository
            repo = Repo(str(self.repository_dir))

            # Walk history filtering by this file path
            walker = repo.get_walker(paths=[path_bytes])

            versions: list[TapeVersion] = []

            for entry in walker:
                commit = entry.commit

                # Extract metadata
                commit_hash = commit.id.decode("ascii")  # Dulwich stores hash as ASCII bytes
                commit_message = commit.message.decode("utf-8").strip()
                author = commit.author.decode("utf-8")
                timestamp = commit.commit_time

                try:
                    # Try to load file content from this commit
                    tree: Tree = repo[commit.tree]  # type: ignore[assignment]
                    mode, blob_id = tree.lookup_path(repo.__getitem__, path_bytes)
                    blob: Blob = repo[blob_id]  # type: ignore[assignment]

                    # Load TapeFile from blob data using BytesIO
                    blob_stream = io.BytesIO(blob.data)
                    tape_file = file_load(blob_stream)

                    # Create version entry
                    version = TapeVersion(
                        commit_hash=commit_hash,
                        commit_message=commit_message,
                        author=author,
                        timestamp=timestamp,
                        tape_file=tape_file,
                        is_deleted=False,
                    )
                    versions.append(version)

                except KeyError:
                    # File doesn't exist in this commit's tree
                    # This happens on removal commits
                    if "Remove tape file:" in commit_message:
                        version = TapeVersion(
                            commit_hash=commit_hash,
                            commit_message=commit_message,
                            author=author,
                            timestamp=timestamp,
                            tape_file=None,
                            is_deleted=True,
                        )
                        versions.append(version)
                    # Otherwise, skip (file not yet added in this commit)

                except (ValueError, OSError) as e:
                    # Corrupted version or parsing error
                    logger.warning(
                        f"Skipping corrupted version of '{filename}' " f"in commit {commit_hash[:8]}: {e}"
                    )
                    continue

            return versions

        except Exception as e:
            raise RepositoryError(f"Failed to get version history for '{filename}': {e}") from e
