"""High-level mesh workflows: join, kick, rotate.

These call into `state` (which handles DB writes + audit chain) and
verify authority before mutating roster. An admin-role member is
required to add or kick. Self-rotate (refresh secret without changing
roster) is open to any current member; this is a session hygiene op.
"""

from __future__ import annotations

import sqlite3

from aborist.mesh.state import (
    MeshRosterEntry,
    current_epoch,
    load_identity,
    roster_at,
    rotate_epoch,
)


def add_member(
    conn: sqlite3.Connection,
    *,
    member_id: str,
    sign_pub: bytes,
    dh_pub: bytes,
    role: str = "member",
) -> int:
    """Admin-only. Add a new member, bumping the epoch.

    The new envelope wraps a fresh secret to every member of the new
    roster — including the new joiner — so they can decrypt all gossip
    from this epoch forward. Prior epochs' secrets remain unrecoverable
    by the joiner unless they can independently reconstruct an earlier
    envelope (they can't, by design).
    """
    me = load_identity(conn)
    if me is None:
        raise RuntimeError("mesh not initialized")
    epoch = current_epoch(conn)
    if epoch is None:
        raise RuntimeError("no epoch state; call init_identity first")
    _require_admin(conn, epoch, me.member_id)

    existing = roster_at(conn, epoch)
    if any(m.member_id == member_id for m in existing):
        raise ValueError(f"member {member_id!r} already in roster")
    new_roster = existing + [
        MeshRosterEntry(
            member_id=member_id,
            sign_pub=sign_pub,
            dh_pub=dh_pub,
            role=role if role in ("admin", "member") else "member",
        )
    ]
    return rotate_epoch(
        conn,
        new_members=new_roster,
        reason=f"join:{member_id}",
        actor_member_id=me.member_id,
    )


def kick_member(
    conn: sqlite3.Connection,
    *,
    member_id: str,
    reason: str,
) -> int:
    """Admin-only. Remove a member from the roster, bumping the epoch.

    The kicked member's prior signatures remain verifiable forever (their
    roster row at older epochs is preserved). They have no entry in the
    new envelope, so they can't decrypt anything from epoch+1 onward.
    """
    me = load_identity(conn)
    if me is None:
        raise RuntimeError("mesh not initialized")
    epoch = current_epoch(conn)
    if epoch is None:
        raise RuntimeError("no epoch state")
    _require_admin(conn, epoch, me.member_id)

    if member_id == me.member_id:
        raise ValueError("cannot kick yourself; use 'mesh disable' instead")
    existing = roster_at(conn, epoch)
    if not any(m.member_id == member_id for m in existing):
        raise ValueError(f"no such member: {member_id!r}")
    new_roster = [m for m in existing if m.member_id != member_id]
    if not any(m.role == "admin" for m in new_roster):
        raise ValueError(
            "kick would leave roster with no admin; promote someone first"
        )
    return rotate_epoch(
        conn,
        new_members=new_roster,
        reason=f"kick:{member_id}:{reason}",
        actor_member_id=me.member_id,
    )


def scheduled_rotate(conn: sqlite3.Connection, *, reason: str = "scheduled") -> int:
    """Refresh the epoch secret without changing the roster.

    Open to any current member. Useful for periodic rekey hygiene or
    after suspicion of secret compromise without an identifiable bad
    actor (every member's keys still trusted).
    """
    me = load_identity(conn)
    if me is None:
        raise RuntimeError("mesh not initialized")
    epoch = current_epoch(conn)
    if epoch is None:
        raise RuntimeError("no epoch state")
    if not any(m.member_id == me.member_id for m in roster_at(conn, epoch)):
        raise PermissionError("not a member of the current epoch")
    new_roster = roster_at(conn, epoch)
    return rotate_epoch(
        conn,
        new_members=new_roster,
        reason=f"rotate:{reason}",
        actor_member_id=me.member_id,
    )


def _require_admin(
    conn: sqlite3.Connection, epoch: int, member_id: str
) -> None:
    rows = roster_at(conn, epoch)
    me = next((m for m in rows if m.member_id == member_id), None)
    if me is None:
        raise PermissionError(f"{member_id} not in epoch {epoch} roster")
    if me.role != "admin":
        raise PermissionError(f"{member_id} is not an admin in epoch {epoch}")
