from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable


CAPABILITY_POLL_SCHEDULING = "poll.scheduling"


class PollCapabilityError(ValueError):
    """Stable error raised by Poll capability implementations."""


@dataclass(frozen=True, slots=True)
class PollOptionRequest:
    label: str
    key: str | None = None
    description: str | None = None
    value: Mapping[str, object] | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollCreateCommand:
    title: str
    kind: str
    status: str
    visibility: str
    result_visibility: str
    description: str | None = None
    context_module: str | None = None
    context_resource_type: str | None = None
    context_resource_id: str | None = None
    workflow_state: str | None = None
    workflow_steps: tuple[Mapping[str, object], ...] = ()
    allow_anonymous: bool = False
    allow_response_update: bool = True
    min_choices: int = 1
    max_choices: int | None = None
    opens_at: datetime | None = None
    closes_at: datetime | None = None
    options: tuple[PollOptionRequest, ...] = ()
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollUpdateCommand:
    """Scheduling-owned Poll fields synchronized as one policy snapshot."""

    title: str
    description: str | None
    visibility: str
    result_visibility: str
    allow_anonymous: bool
    allow_response_update: bool
    closes_at: datetime | None


@dataclass(frozen=True, slots=True)
class PollOptionUpdateCommand:
    """Complete mutable option snapshot supplied by an owning module.

    Providers must treat an exact repeat as a no-op.  When the snapshot
    changes, answers for this option are invalidated while answers for other
    options are retained.
    """

    label: str
    description: str | None = None
    value: Mapping[str, object] | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollOptionOrderCommand:
    """Complete active option order supplied by an owning module.

    Providers must reject partial, duplicate, or unknown option collections.
    An exact repeat is an idempotent no-op. Reordering changes positions only;
    option identities and their existing answers remain valid.
    """

    option_ids: tuple[str, ...]


@dataclass(frozen=True, slots=True)
class PollInvitationCommand:
    respondent_id: str | None = None
    respondent_label: str | None = None
    email: str | None = None
    expires_at: datetime | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollAnswerRequest:
    option_id: str | None = None
    option_key: str | None = None
    value: object = None
    rank: int | None = None


@dataclass(frozen=True, slots=True)
class PollSubmitResponseCommand:
    respondent_id: str
    respondent_label: str | None = None
    answers: tuple[PollAnswerRequest, ...] = ()
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollResponseRetirementCommand:
    """Retire live responses while preserving their auditable history.

    Owning modules provide every server-trusted identity that can refer to the
    participant. Providers must soft-delete matching live responses, retain
    their answers, and treat an exact ``idempotency_key`` replay as a no-op.
    """

    respondent_ids: tuple[str, ...] = ()
    invitation_id: str | None = None
    reason: str = "participant_removed"
    idempotency_key: str = ""
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class PollOptionRef:
    id: str
    position: int


@dataclass(frozen=True, slots=True)
class PollRef:
    id: str
    status: str
    options: tuple[PollOptionRef, ...] = ()


@dataclass(frozen=True, slots=True)
class PollInvitationRef:
    id: str
    token: str


@dataclass(frozen=True, slots=True)
class PollAnswerRef:
    option_id: str | None = None
    option_key: str | None = None
    value: object = None
    rank: int | None = None


@dataclass(frozen=True, slots=True)
class PollResponseRef:
    invitation_id: str | None
    submitted_at: datetime
    # Optional for backwards compatibility with existing providers.  A
    # consumer can use it to reconcile an authenticated response without
    # trusting client-supplied invitation metadata.
    respondent_id: str | None = None
    # Optional for backwards compatibility.  Providers that support response
    # editing expose the authoritative, still-valid answers here.
    answers: tuple[PollAnswerRef, ...] = ()


@dataclass(frozen=True, slots=True)
class PollResponseRetirementRef:
    """Outcome of an auditable response-retirement request."""

    response_ids: tuple[str, ...] = ()
    retired_at: datetime | None = None
    newly_retired_count: int = 0
    replayed: bool = False


@runtime_checkable
class PollSchedulingProvider(Protocol):
    def create_poll(
        self,
        session: object,
        *,
        tenant_id: str,
        user_id: str | None,
        command: PollCreateCommand,
    ) -> PollRef:
        ...

    def create_invitation(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        command: PollInvitationCommand,
    ) -> PollInvitationRef:
        ...

    def update_poll(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        command: PollUpdateCommand,
    ) -> PollRef:
        ...

    def update_option(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        option_id: str,
        command: PollOptionUpdateCommand,
    ) -> PollOptionRef:
        """Update one option and invalidate only answers for that option."""

        ...

    def reorder_options(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        command: PollOptionOrderCommand,
    ) -> PollRef:
        """Atomically replace the complete active option order."""

        ...

    def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
        ...

    def close_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
        ...

    def decide_poll(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        option_id: str | None,
    ) -> PollRef:
        ...

    def get_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
        ...

    def set_workflow_context(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        workflow_state: str,
        workflow_steps: Sequence[Mapping[str, object]],
        context_module: str,
        context_resource_type: str,
        context_resource_id: str,
    ) -> PollRef:
        ...

    def result_summary(self, session: object, *, tenant_id: str, poll_id: str) -> Mapping[str, object]:
        ...

    def list_responses(self, session: object, *, tenant_id: str, poll_id: str) -> Sequence[PollResponseRef]:
        ...

    def get_response(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        respondent_ids: Sequence[str],
        invitation_id: str | None = None,
    ) -> PollResponseRef | None:
        """Return one response matching server-trusted participant identities."""

        ...


@runtime_checkable
class PollResponseSubmissionProvider(Protocol):
    """Optional extension for modules that collect responses through Poll."""

    def submit_response(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        command: PollSubmitResponseCommand,
    ) -> PollResponseRef:
        ...


@runtime_checkable
class PollResponseRetirementProvider(Protocol):
    """Optional extension for owning modules that remove participants."""

    def retire_responses(
        self,
        session: object,
        *,
        tenant_id: str,
        poll_id: str,
        command: PollResponseRetirementCommand,
    ) -> PollResponseRetirementRef:
        ...


def poll_scheduling_provider(registry: object | None) -> PollSchedulingProvider | None:
    if registry is None or not hasattr(registry, "has_capability"):
        return None
    if not registry.has_capability(CAPABILITY_POLL_SCHEDULING):
        return None
    capability = registry.capability(CAPABILITY_POLL_SCHEDULING)
    return capability if isinstance(capability, PollSchedulingProvider) else None


def poll_response_submission_provider(
    registry: object | None,
) -> PollResponseSubmissionProvider | None:
    provider = poll_scheduling_provider(registry)
    return provider if isinstance(provider, PollResponseSubmissionProvider) else None


def poll_response_retirement_provider(
    registry: object | None,
) -> PollResponseRetirementProvider | None:
    """Resolve response retirement without making it mandatory for Poll v1 providers."""

    if registry is None or not hasattr(registry, "has_capability"):
        return None
    if not registry.has_capability(CAPABILITY_POLL_SCHEDULING):
        return None
    capability = registry.capability(CAPABILITY_POLL_SCHEDULING)
    return (
        capability
        if isinstance(capability, PollResponseRetirementProvider)
        else None
    )
