"""The closed network — who NEOedge is allowed to do business with, and on which devices.

NEOedge only trades with registered members. That is not a licensing rule, it is the trust
mechanism: because both sides run our signed module and both are bound by the same rules,
**every inter-company event has two independent records by construction**. A trade with a
non-member would produce one record and no counterpart, which is precisely the shape this
system treats as a fault.

Keys are per DEVICE, not per company. NEOedge runs on hardware the customer controls: a
laptop is lost, a VM is cloned, an admin leaves. One key per company would mean one
revocation takes the company off the network; per-device keys mean a compromised install
is revoked while the company keeps trading. A revoked device's past records stay valid up
to its revocation — revocation is not retroactive, because the records it already wrote
were co-recorded by the counterparty and cannot be quietly withdrawn.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

_MEMBER_URN = re.compile(r"^urn:neoedge:member:[a-z0-9][a-z0-9._-]{0,62}$")

REFUSED_NOT_A_MEMBER = "REFUSED_NOT_A_MEMBER"
REFUSED_DEVICE_REVOKED = "REFUSED_DEVICE_REVOKED"
REFUSED_UNKNOWN_DEVICE = "REFUSED_UNKNOWN_DEVICE"


class MembershipError(Exception):
    def __init__(self, code: str, detail: str):
        super().__init__(detail)
        self.code = code
        self.detail = detail


@dataclass(frozen=True)
class Device:
    device_id: str
    public_key: str          # hex; the signing identity of one install
    revoked_at_seq: int | None = None   # a POSITION in the member's book, never a wall clock


@dataclass
class Member:
    member_id: str
    legal_name: str
    country: str             # ISO-3166 alpha-2
    erp_system: str          # "SAP-S4HANA", "MS-DYNAMICS-365-FO", "ORACLE-FUSION", ...
    devices: dict[str, Device] = field(default_factory=dict)

    def __post_init__(self):
        if not _MEMBER_URN.match(self.member_id):
            raise MembershipError(
                REFUSED_NOT_A_MEMBER,
                f"member_id must look like urn:neoedge:member:<slug>, got {self.member_id!r}",
            )


class MemberRegistry:
    """The roster. In the pilot it is local; in production it is issued by NEOos governance
    and each edge holds a signed copy. Either way the edge NEVER infers membership from a
    request body — the same lesson the Arena paid for: authority is verified, not asserted."""

    def __init__(self, members: list[Member] | None = None):
        self._members: dict[str, Member] = {}
        for member in members or []:
            self.register(member)

    def register(self, member: Member) -> Member:
        self._members[member.member_id] = member
        return member

    def get(self, member_id: str) -> Member:
        member = self._members.get(member_id)
        if member is None:
            raise MembershipError(
                REFUSED_NOT_A_MEMBER,
                f"{member_id} is not a registered member — NEOedge works only inside the closed network",
            )
        return member

    def is_member(self, member_id: str) -> bool:
        return member_id in self._members

    def require_pair(self, member_id: str, counterparty_id: str) -> tuple[Member, Member]:
        """Both sides must be members, or the event would have only one record and no witness."""
        return self.get(member_id), self.get(counterparty_id)

    def add_device(self, member_id: str, device: Device) -> Device:
        self.get(member_id).devices[device.device_id] = device
        return device

    def revoke_device(self, member_id: str, device_id: str, at_seq: int) -> Device:
        member = self.get(member_id)
        device = member.devices.get(device_id)
        if device is None:
            raise MembershipError(REFUSED_UNKNOWN_DEVICE, f"{member_id} has no device {device_id!r}")
        revoked = Device(device.device_id, device.public_key, revoked_at_seq=at_seq)
        member.devices[device_id] = revoked
        return revoked

    def require_active_device(self, member_id: str, device_id: str, at_seq: int) -> Device:
        """A device may write at position `at_seq` only if it was not revoked at or before it."""
        member = self.get(member_id)
        device = member.devices.get(device_id)
        if device is None:
            raise MembershipError(REFUSED_UNKNOWN_DEVICE, f"{member_id} has no device {device_id!r}")
        if device.revoked_at_seq is not None and at_seq >= device.revoked_at_seq:
            raise MembershipError(
                REFUSED_DEVICE_REVOKED,
                f"device {device_id!r} of {member_id} was revoked at position {device.revoked_at_seq}",
            )
        return device

    def __len__(self) -> int:
        return len(self._members)

    def __iter__(self):
        return iter(self._members.values())
