from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol, runtime_checkable

from abcreg import AnnotatedProtocolMeta  # type: ignore[import-untyped]
from fastapi import Request
from starlette.responses import Response

_DEFAULT_METHODS = ("mr_triggers", "on_mr")


class MrTrigger(StrEnum):
    OPENED = "opened"
    UPDATED = "updated"


@dataclass(frozen=True, slots=True)
class MrEvent:
    """Context passed to plugin actions when an MR is opened or updated."""

    project: str
    trigger: MrTrigger
    mr_ref: str
    mr_oid: str
    title: str
    ours: str
    theirs: str | None
    head: str


@dataclass(frozen=True, slots=True)
class ActionProgress:
    """One progress update from an async MR action runner."""

    message: str
    fraction: float | None = None
    done: bool = False
    ok: bool | None = None


@runtime_checkable
class ProgressSink(Protocol):
    """Receives progress from ``Action.on_mr`` so the caller can stream updates."""

    async def report(self, progress: ActionProgress) -> None: ...


def _install_default_methods(interface: type, impl: type) -> None:
    """Give implementations the protocol's optional methods before signature checks."""
    if getattr(impl, "_is_protocol", False):
        return
    skip = set(interface.__mro__)
    for name in _DEFAULT_METHODS:
        if any(name in base.__dict__ for base in impl.__mro__ if base not in skip):
            continue
        for base in interface.__mro__:
            default = base.__dict__.get(name)
            if callable(default):
                setattr(impl, name, default)
                break


class _ActionMeta(AnnotatedProtocolMeta):
    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> None:
        if not getattr(cls, "_is_protocol", False):
            for base in bases:
                if isinstance(base, _ActionMeta) and getattr(base, "_is_protocol", False):
                    _install_default_methods(base, cls)
        super().__init__(name, bases, namespace, **kwargs)

    def register(cls, subclass: type) -> type:
        _install_default_methods(cls, subclass)
        super().register(subclass)
        return subclass


@runtime_checkable
class Action(Protocol, metaclass=_ActionMeta):
    """
    Action: A per-project action provided by a plugin.

    ``project`` is the project path/slug as used in URLs (under PROJECTROOT).
    Subclass this protocol, or register a virtual subclass with ``@Action.register``.

    Optional MR hooks: declare interest with ``mr_triggers`` and implement ``on_mr``
    as an async runner that reports progress through ``progress``.
    """

    def action_name(self) -> str: ...

    def action(self, project: str, req: Request | None = None) -> Response | None: ...

    def mr_triggers(self) -> frozenset[MrTrigger]:
        return frozenset()

    async def on_mr(self, event: MrEvent, progress: ProgressSink) -> None:
        return None