"""Git-facing branch and metabranch models."""

from __future__ import annotations

from datetime import datetime, timezone

import git
from pydantic import BaseModel, ConfigDict, Field

from ..exceptions import (
    BranchAlreadyExistsError,
    InvalidBranchError,
    InvalidMetabranchError,
)
from ..models.metabranch import (
    CONFIG_FILE_NAME,
    INITIALIZE_CONFIG_COMMIT_SUBJECT,
    METADATA_COMMIT_TRAILER,
    MetabranchConfigFile,
    MetabranchDefinition,
    UPDATE_CONFIG_COMMIT_SUBJECT,
)
from .client import GitClient


class GitBranch(BaseModel):
    """Base class for all Git branches."""

    name: str = Field(..., description="Name of the branch")

    model_config = ConfigDict(frozen=True, validate_assignment=True)

    def __eq__(self, other: object) -> bool:
        """Check if the branch is equal to another object."""
        if isinstance(other, Branch):
            return self.name == other.name
        return False

    @property
    def exists(self) -> bool:
        """Check if the branch exists."""
        return GitClient().branch_exists(self.name)

    def checkout(self, create: bool = False) -> None:
        """Checkout the branch."""
        try:
            GitClient().checkout(self.name, create=create)
        except git.GitCommandError as exc:
            if create:
                raise BranchAlreadyExistsError() from exc
            raise

    def push(self, force: bool = False) -> None:
        """Push the branch."""
        GitClient().push(self.name, force=force)

    def rebase(self, base: str, interactive: bool = False) -> None:
        """Rebase the branch."""
        git_client = GitClient()
        git_client.checkout(self.name)
        git_client.rebase(base, interactive=interactive)

    def status(self) -> str:
        """Get the status of the branch."""
        return GitClient().status(self.name)


class Branch(GitBranch):
    """A branch that belongs to a metabranch."""

    added_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc),
        description="Timestamp when added",
    )

    model_config = ConfigDict(frozen=True, validate_assignment=True)


class Metabranch(GitBranch):
    """A metabranch - a git branch that tracks multiple branches."""

    base_ref: str = Field(default="", description="Base branch for this metabranch")
    branches: list[Branch] = Field(default_factory=list, description="List of branches")

    model_config = ConfigDict(frozen=True, validate_assignment=True)

    @property
    def exists(self) -> bool:
        """Check if the metabranch exists."""
        return MetabranchConfigFile.exists_in_branch(GitClient(), self.name)

    def add(self, name: str) -> None:
        """Add a branch to the metabranch."""
        branch = Branch(name=name)
        if not branch.exists:
            raise InvalidBranchError()
        if branch in self.branches:
            return
        self.branches.append(branch)

    def remove(self, name: str) -> None:
        """Remove a branch from the metabranch."""
        self.branches[:] = [branch for branch in self.branches if branch.name != name]

    def push(self, force: bool = False) -> None:
        """Push the metabranch."""
        for branch in self.branches:
            branch.push(force=force)
        super().push(force=force)

    def status(self) -> list[str]:
        """Get the status of the metabranch."""
        return [super().status(), *[branch.status() for branch in self.branches]]

    def rebase(self, base: str, interactive: bool = False) -> None:
        """Rebase the metabranch."""
        git_client = GitClient()
        git_client.checkout(self.name)
        git_client.reset_hard(base)

        for branch in self.branches:
            for commit in git_client.list_unique_commits(
                branch.name,
                exclude_refs=(base,),
            ):
                git_client.cherry_pick(commit.sha, record_origin=True)

        MetabranchConfigFile.save_for(
            git_client,
            self.to_definition(base_ref=base),
        )
        git_client.commit_only_path(
            CONFIG_FILE_NAME,
            subject=UPDATE_CONFIG_COMMIT_SUBJECT,
            body=METADATA_COMMIT_TRAILER,
        )

        self.checkout()

    @classmethod
    def from_definition(cls, definition: MetabranchDefinition) -> Metabranch:
        """Create a compatibility model from a definition."""
        return cls(
            name=definition.name,
            base_ref=definition.base_ref,
            branches=[Branch(name=branch_name) for branch_name in definition.branches],
        )

    def to_definition(self, base_ref: str | None = None) -> MetabranchDefinition:
        """Convert the compatibility model to a durable definition."""
        return MetabranchDefinition(
            name=self.name,
            base_ref=base_ref or self.base_ref or GitClient().current_branch_name(),
            branches=tuple(branch.name for branch in self.branches),
        )


class MetabranchConfig(BaseModel):
    """Configuration wrapper backed by the shared config file."""

    metabranch: Metabranch | None = Field(None, description="The metabranch")

    model_config = ConfigDict(frozen=False, validate_assignment=True)

    @property
    def exists(self) -> bool:
        """Check if the configuration file exists."""
        return MetabranchConfigFile.exists_for(GitClient())

    def load(self) -> None:
        """Load the current metabranch configuration."""
        if not self.exists:
            raise InvalidMetabranchError()
        self.metabranch = Metabranch.from_definition(
            MetabranchConfigFile.load_for(GitClient())
        )

    def save(self) -> None:
        """Save this metabranch configuration and set it as current."""
        if self.metabranch is None:
            raise InvalidMetabranchError()
        git_client = GitClient()
        subject = (
            UPDATE_CONFIG_COMMIT_SUBJECT
            if MetabranchConfigFile.exists_for(git_client)
            else INITIALIZE_CONFIG_COMMIT_SUBJECT
        )
        MetabranchConfigFile.save_for(git_client, self.metabranch.to_definition())
        git_client.commit_only_path(
            CONFIG_FILE_NAME,
            subject=subject,
            body=METADATA_COMMIT_TRAILER,
        )

    def __enter__(self) -> Metabranch:
        """Enter the context of the metabranch configuration."""
        self.load()
        if self.metabranch is None:
            raise InvalidMetabranchError()
        return self.metabranch

    def __exit__(self, exc_type, exc_value, traceback) -> None:
        """Exit the context of the metabranch configuration."""
        self.save()
