"""Metabranch definitions and tracked config."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_validator

from ..exceptions import InvalidMetabranchError
from .io import load_yaml_model, store_yaml_model

if TYPE_CHECKING:  # pragma: no cover
    from ..git.client import GitClient

CONFIG_FILE_NAME = ".metabranch.yaml"
SCHEMA_VERSION = 1
METADATA_COMMIT_TRAILER = "Metabranch-Metadata: true"
INITIALIZE_CONFIG_COMMIT_SUBJECT = "metabranch: initialize config"
UPDATE_CONFIG_COMMIT_SUBJECT = "metabranch: update config"


class MetabranchDefinition(BaseModel):
    """Durable metabranch metadata shared across clients."""

    name: str
    base_ref: str
    branches: tuple[str, ...] = ()
    options: dict[str, str] = Field(default_factory=dict)

    model_config = ConfigDict(frozen=True, extra="ignore")

    @model_validator(mode="before")
    @classmethod
    def normalize_fields(cls, value: Any) -> Any:
        """Coerce legacy metabranch fields into the stable runtime shape."""
        if not isinstance(value, dict):
            return value

        raw_branches = value.get("branches", [])
        branches: list[str] = []
        if isinstance(raw_branches, (list, tuple)):
            for branch in raw_branches:
                if isinstance(branch, str):
                    branches.append(branch)
                elif isinstance(branch, dict) and isinstance(branch.get("name"), str):
                    branches.append(branch["name"])

        raw_options = value.get("options", {})
        options = (
            {str(key): str(option) for key, option in raw_options.items()}
            if isinstance(raw_options, dict)
            else {}
        )

        normalized = dict(value)
        normalized["branches"] = tuple(branches)
        normalized["options"] = options
        base_ref = value.get("base_ref")
        normalized["base_ref"] = (
            base_ref
            if isinstance(base_ref, str) and base_ref
            else value.get("name", "")
        )
        return normalized

    @field_serializer("branches", when_used="json")
    def serialize_branches(self, branches: tuple[str, ...]) -> list[str]:
        """Serialize branch tuples as normal arrays."""
        return list(branches)

    def update_branches(
        self,
        *,
        added: tuple[str, ...] = (),
        removed: tuple[str, ...] = (),
    ) -> MetabranchDefinition:
        """Return a new definition with the net add/remove delta applied once."""
        added_set = set(added)
        removed_set = set(removed)
        overlap = added_set & removed_set
        effective_removed = {
            branch_name for branch_name in removed if branch_name not in overlap
        }
        effective_added = tuple(
            branch_name for branch_name in added if branch_name not in overlap
        )

        branches = tuple(
            existing for existing in self.branches if existing not in effective_removed
        )
        for branch_name in effective_added:
            if branch_name not in branches:
                branches = (*branches, branch_name)
        return self.model_copy(
            update={"branches": branches, "options": dict(self.options)}
        )


class MetabranchConfigFile(BaseModel):
    """Tracked YAML document for shared metabranch metadata."""

    schema_version: int = Field(default=SCHEMA_VERSION)
    metabranch: MetabranchDefinition

    model_config = ConfigDict(extra="ignore")

    @model_validator(mode="before")
    @classmethod
    def normalize_top_level_shape(cls, value: Any) -> Any:
        """Accept both enveloped and legacy top-level payloads."""
        if not isinstance(value, dict):
            return value
        if "metabranch" in value:
            return value
        return {"metabranch": value}

    @classmethod
    def path_for(cls, git_client: GitClient) -> Path:
        """Return the shared metabranch config path."""
        return git_client.working_dir / CONFIG_FILE_NAME

    @classmethod
    def exists_for(cls, git_client: GitClient) -> bool:
        """Return whether the current worktree has metabranch config."""
        return cls.path_for(git_client).exists()

    @classmethod
    def exists_in_branch(cls, git_client: GitClient, branch_name: str) -> bool:
        """Return whether a branch contains metabranch config."""
        if branch_name == git_client.current_branch_name() and cls.exists_for(
            git_client
        ):
            return True
        return (
            git_client.read_text_from_branch(branch_name, CONFIG_FILE_NAME) is not None
        )

    @classmethod
    def load_for(cls, git_client: GitClient) -> MetabranchDefinition:
        """Load the current metabranch definition from disk."""
        path = cls.path_for(git_client)
        if not path.exists():
            raise InvalidMetabranchError()
        return load_yaml_model(
            path,
            cls,
            "Invalid metabranch config payload",
        ).metabranch

    @classmethod
    def save_for(cls, git_client: GitClient, definition: MetabranchDefinition) -> None:
        """Persist the current metabranch definition to disk."""
        store_yaml_model(cls.path_for(git_client), cls(metabranch=definition))
