from __future__ import annotations

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


CAPABILITY_VOTING_BALLOTS = "voting.ballots"
CAPABILITY_VOTING_PROVIDER_PREFIX = "voting.provider."

VOTING_ASSURANCE_RECORDED = "recorded"
VOTING_ASSURANCE_CONFIDENTIAL = "confidential"
VOTING_ASSURANCE_SECRET = "secret"
VOTING_ASSURANCE_EXTERNAL_CERTIFIED = "external_certified"


class VotingCapabilityError(ValueError):
    """Stable error raised by Voting capability implementations."""


def voting_provider_capability(provider_id: str) -> str:
    normalized = str(provider_id or "").strip().lower()
    if not normalized or any(
        character not in "abcdefghijklmnopqrstuvwxyz0123456789_.-"
        for character in normalized
    ):
        raise VotingCapabilityError("Voting provider id is invalid.")
    return f"{CAPABILITY_VOTING_PROVIDER_PREFIX}{normalized}"


@dataclass(frozen=True, slots=True)
class VotingOption:
    key: str
    label: str
    description: str | None = None


@dataclass(frozen=True, slots=True)
class VotingElector:
    subject_id: str
    label: str | None = None
    weight: int = 1
    provenance: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class VotingBallotCreateCommand:
    title: str
    method: str
    assurance_profile: str
    options: tuple[VotingOption, ...]
    electorate: tuple[VotingElector, ...]
    description: str | None = None
    context_module: str | None = None
    context_resource_type: str | None = None
    context_resource_id: str | None = None
    quorum_weight: int = 0
    threshold_numerator: int = 1
    threshold_denominator: int = 2
    allow_replacement: bool = True
    opens_at: datetime | None = None
    closes_at: datetime | None = None
    provider_id: str | None = None
    provider_ballot_ref: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class VotingCastCommand:
    selections: tuple[str, ...]
    idempotency_key: str
    elector_id: str | None = None


@dataclass(frozen=True, slots=True)
class VotingBallotRef:
    id: str
    revision: int
    state: str
    assurance_profile: str
    definition_sha256: str | None = None
    electorate_sha256: str | None = None


@dataclass(frozen=True, slots=True)
class VotingReceipt:
    ballot_id: str
    revision: int
    receipt_sha256: str
    cast_at: datetime
    replaced_previous: bool = False
    replayed: bool = False


@dataclass(frozen=True, slots=True)
class VotingResult:
    ballot_id: str
    revision: int
    counts: Mapping[str, int]
    weighted_counts: Mapping[str, int]
    cast_count: int
    cast_weight: int
    eligible_count: int
    eligible_weight: int
    quorum_met: bool
    threshold_met: bool
    winning_options: tuple[str, ...]
    result_sha256: str
    evidence: tuple[Mapping[str, object], ...] = ()

    def __post_init__(self) -> None:
        _validate_provider_evidence(self.evidence)


@dataclass(frozen=True, slots=True)
class ExternalVotingFinalizationRequest:
    tenant_id: str
    ballot_id: str
    provider_ballot_ref: str
    definition_sha256: str
    electorate_sha256: str
    options: tuple[VotingOption, ...]
    eligible_count: int
    eligible_weight: int
    requested_at: datetime
    idempotency_key: str


@dataclass(frozen=True, slots=True)
class ExternalVotingPreparationRequest:
    tenant_id: str
    ballot_id: str
    requested_provider_ballot_ref: str | None
    definition_sha256: str
    electorate_sha256: str
    assurance_profile: Literal["confidential", "secret", "external_certified"]
    method: str
    options: tuple[VotingOption, ...]
    electorate: tuple[VotingElector, ...]
    allow_replacement: bool
    quorum_weight: int
    threshold_numerator: int
    threshold_denominator: int
    opens_at: datetime | None
    closes_at: datetime | None
    requested_at: datetime
    idempotency_key: str


@dataclass(frozen=True, slots=True)
class ExternalVotingBallotRef:
    provider_id: str
    provider_ballot_ref: str
    state: Literal["prepared", "open", "closed", "outcome_unknown"]
    definition_sha256: str
    electorate_sha256: str
    evidence: tuple[Mapping[str, object], ...] = ()

    def __post_init__(self) -> None:
        _validate_provider_evidence(self.evidence)


@dataclass(frozen=True, slots=True)
class ExternalVotingCastRequest:
    tenant_id: str
    ballot_id: str
    provider_ballot_ref: str
    definition_sha256: str
    electorate_sha256: str
    elector_id: str
    selections: tuple[str, ...]
    allow_replacement: bool
    requested_at: datetime
    idempotency_key: str


@runtime_checkable
class ExternalVotingProvider(Protocol):
    """Provider boundary for confidential, secret, or certified voting.

    Providers return aggregate results and evidence only. Raw ballots and
    provider credentials must not cross this boundary.
    """

    def finalize_ballot(
        self,
        session: object,
        principal: object,
        *,
        request: ExternalVotingFinalizationRequest,
    ) -> VotingResult: ...


@runtime_checkable
class InteractiveExternalVotingProvider(ExternalVotingProvider, Protocol):
    """Provider that owns external ballot preparation, casting, and tallying."""

    def prepare_ballot(
        self,
        session: object,
        principal: object,
        *,
        request: ExternalVotingPreparationRequest,
    ) -> ExternalVotingBallotRef: ...

    def cast_ballot(
        self,
        session: object,
        principal: object,
        *,
        request: ExternalVotingCastRequest,
    ) -> VotingReceipt: ...

    def ballot_status(
        self,
        session: object,
        principal: object,
        *,
        tenant_id: str,
        provider_ballot_ref: str,
    ) -> ExternalVotingBallotRef | None: ...


@runtime_checkable
class VotingBallotProvider(Protocol):
    def create_ballot(
        self,
        session: object,
        principal: object,
        *,
        command: VotingBallotCreateCommand,
        idempotency_key: str,
    ) -> VotingBallotRef: ...

    def get_ballot(
        self,
        session: object,
        principal: object,
        *,
        ballot_id: str,
    ) -> Mapping[str, object] | None: ...

    def open_ballot(
        self,
        session: object,
        principal: object,
        *,
        ballot_id: str,
        expected_revision: int,
        idempotency_key: str,
    ) -> VotingBallotRef: ...

    def cast_ballot(
        self,
        session: object,
        principal: object,
        *,
        ballot_id: str,
        command: VotingCastCommand,
    ) -> VotingReceipt: ...

    def close_ballot(
        self,
        session: object,
        principal: object,
        *,
        ballot_id: str,
        expected_revision: int,
        idempotency_key: str,
    ) -> VotingResult: ...

    def certify_ballot(
        self,
        session: object,
        principal: object,
        *,
        ballot_id: str,
        expected_revision: int,
        evidence: Sequence[Mapping[str, object]],
        idempotency_key: str,
    ) -> VotingBallotRef: ...


def _validate_provider_evidence(
    evidence: Sequence[Mapping[str, object]],
) -> None:
    if len(evidence) > 64:
        raise ValueError("Voting provider evidence exceeds the item limit")
    try:
        encoded = json.dumps(
            [dict(item) for item in evidence],
            sort_keys=True,
            separators=(",", ":"),
            ensure_ascii=True,
            allow_nan=False,
        ).encode("utf-8")
    except (TypeError, ValueError) as exc:
        raise ValueError("Voting provider evidence must be bounded JSON") from exc
    if len(encoded) > 64 * 1024:
        raise ValueError("Voting provider evidence exceeds the size limit")
    _reject_sensitive_evidence(evidence)


def _reject_sensitive_evidence(value: object) -> None:
    sensitive_fragments = (
        "access_token",
        "authorization",
        "credential",
        "cookie",
        "key_material",
        "password",
        "passwd",
        "plaintext",
        "private_key",
        "raw_vote",
        "refresh_token",
        "secret",
        "selection",
    )
    if isinstance(value, Mapping):
        for key, nested in value.items():
            normalized = str(key).strip().lower().replace("-", "_")
            if any(fragment in normalized for fragment in sensitive_fragments):
                raise ValueError("Voting provider evidence contains a sensitive field")
            _reject_sensitive_evidence(nested)
    elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
        for nested in value:
            _reject_sensitive_evidence(nested)


__all__ = [
    "CAPABILITY_VOTING_BALLOTS",
    "CAPABILITY_VOTING_PROVIDER_PREFIX",
    "ExternalVotingBallotRef",
    "ExternalVotingCastRequest",
    "ExternalVotingFinalizationRequest",
    "ExternalVotingPreparationRequest",
    "ExternalVotingProvider",
    "InteractiveExternalVotingProvider",
    "VOTING_ASSURANCE_CONFIDENTIAL",
    "VOTING_ASSURANCE_EXTERNAL_CERTIFIED",
    "VOTING_ASSURANCE_RECORDED",
    "VOTING_ASSURANCE_SECRET",
    "VotingBallotCreateCommand",
    "VotingBallotProvider",
    "VotingBallotRef",
    "VotingCapabilityError",
    "VotingCastCommand",
    "VotingElector",
    "VotingOption",
    "VotingReceipt",
    "VotingResult",
    "voting_provider_capability",
]
