from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Annotated, Any

from jsonpointer import JsonPointer, JsonPointerException
from pydantic import AfterValidator, BaseModel, ConfigDict


class ConfigPatch:
    """A storage-agnostic set of config changes expressed as JSON Patch operations."""

    def __init__(
        self, *operations: PatchOp, fingerprint: str, reason: str = ""
    ) -> None:
        self.operations = list(operations)
        self.fingerprint = fingerprint
        self.reason = reason

    def add(self, *operations: PatchOp) -> ConfigPatch:
        """Append operations after construction."""
        self.operations.extend(operations)
        return self

    def to_json_patch(self) -> list[dict[str, Any]]:
        return [op.to_json_patch() for op in self.operations]

    def describe(self) -> list[str]:
        """Human-readable summary of each operation."""
        return [op.describe() for op in self.operations]


class _OperationPatch(BaseModel, ABC):
    model_config = ConfigDict(frozen=True, extra="forbid")

    @staticmethod
    def _validate_json_pointer(path: str) -> str:
        try:
            JsonPointer(path)
        except JsonPointerException as e:
            raise ValueError("path must be a valid JSON Pointer") from e

        return path

    path: Annotated[str, AfterValidator(_validate_json_pointer)]

    @abstractmethod
    def to_json_patch(self) -> dict[str, Any]:
        raise NotImplementedError

    @abstractmethod
    def describe(self) -> str:
        raise NotImplementedError


class AddOperationPatch(_OperationPatch):
    """Add a value at a JSON Pointer path."""

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

    value: Any

    def to_json_patch(self) -> dict[str, Any]:
        return {"op": "add", "path": self.path, "value": self.value}

    def describe(self) -> str:
        return f"add {self.path!r} = {self.value!r}"


class ReplaceOperationPatch(_OperationPatch):
    """Replace the existing value at a JSON Pointer path."""

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

    value: Any

    def to_json_patch(self) -> dict[str, Any]:
        return {"op": "replace", "path": self.path, "value": self.value}

    def describe(self) -> str:
        return f"replace {self.path!r} = {self.value!r}"


class RemoveOperationPatch(_OperationPatch):
    """Remove the existing value at a JSON Pointer path."""

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

    def to_json_patch(self) -> dict[str, Any]:
        return {"op": "remove", "path": self.path}

    def describe(self) -> str:
        return f"remove {self.path!r}"


type PatchOp = AddOperationPatch | ReplaceOperationPatch | RemoveOperationPatch
