"""Suspension holds on project memberships: policy-gated placement and release (design D30, §3.6).

Every mutation loads canonical actor and target state, evaluates the policy, writes the
hold, bumps the target's authorization_version once, and records an audit event. Each
operation names one source, so no caller can clear another authority's hold:

- workspace holds are never released here (Workspace reconciliation owns them);
- local holds follow the suspend and reinstate permissions;
- legacy holds need the owner-only member.reinstate_legacy permission.

Releasing a hold reports every remaining hold, so the result can never imply full
reinstatement while another authority still holds the membership.
"""

from __future__ import annotations

from dataclasses import dataclass
from uuid import UUID

from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.project_auth.audit import record_event
from src.domain.project_auth.authorization import (
    Action,
    authorize_hold_release,
    authorize_suspension,
)
from src.domain.project_auth.authorization_loader import MembershipView, load_membership
from src.domain.project_auth.membership_projection import sync_membership_status
from src.domain.project_auth.models import ProjectMembership, ProjectMembershipSuspension

LOCAL = "local"
LEGACY = "legacy"


class HoldError(Exception):
    def __init__(self, code: str) -> None:
        super().__init__(code)
        self.code = code


@dataclass(frozen=True)
class ReleaseResult:
    membership_id: UUID
    source: str
    released: bool
    previous_sources: tuple[str, ...]
    remaining_sources: tuple[str, ...]

    @property
    def still_suspended(self) -> bool:
        return bool(self.remaining_sources)

    @property
    def code(self) -> str:
        return "membership_still_suspended" if self.still_suspended else "reinstated"


async def _views(
    db: AsyncSession, project_id: UUID, actor_id: UUID, target_id: UUID
) -> tuple[MembershipView, MembershipView]:
    actor = await load_membership(db, project_id, actor_id)
    if actor is None:
        raise HoldError("actor_not_found")
    target = await load_membership(db, project_id, target_id)
    if target is None:
        raise HoldError("membership_not_found")
    return actor, target


async def _sources(db: AsyncSession, membership_id: UUID) -> tuple[str, ...]:
    rows = await db.execute(
        select(ProjectMembershipSuspension.source).where(
            ProjectMembershipSuspension.membership_id == membership_id
        )
    )
    return tuple(sorted(str(getattr(s, "value", s)) for s in rows.scalars().all()))


async def _version(db: AsyncSession, membership_id: UUID) -> int:
    return int(
        await db.scalar(
            select(ProjectMembership.authorization_version).where(
                ProjectMembership.id == membership_id
            )
        )
    )


async def _bump_version(db: AsyncSession, membership_id: UUID) -> None:
    await db.execute(
        update(ProjectMembership)
        .where(ProjectMembership.id == membership_id)
        .values(authorization_version=ProjectMembership.authorization_version + 1)
    )


async def place_local_hold(
    db: AsyncSession,
    project_id: UUID,
    *,
    actor_membership_id: UUID,
    target_membership_id: UUID,
    reason_code: str,
    correlation_id: str | None = None,
) -> str:
    actor, target = await _views(db, project_id, actor_membership_id, target_membership_id)
    decision = authorize_suspension(
        actor.as_actor(), target.as_target(), project_id=project_id, action=Action.suspend
    )
    if not decision.allowed:
        raise HoldError(decision.code)
    if LOCAL in await _sources(db, target_membership_id):
        return await sync_membership_status(db, target_membership_id)

    before = await _version(db, target_membership_id)
    previous = await _sources(db, target_membership_id)
    db.add(
        ProjectMembershipSuspension(
            membership_id=target_membership_id,
            source=LOCAL,
            reason_code=reason_code,
            created_by_membership_id=actor_membership_id,
        )
    )
    await db.flush()
    status = await sync_membership_status(db, target_membership_id)
    await _bump_version(db, target_membership_id)
    await record_event(
        db,
        project_id=project_id,
        event="project.member.hold_placed",
        actor_membership_id=actor_membership_id,
        target_membership_id=target_membership_id,
        payload={
            "source": LOCAL,
            "reason_code": reason_code,
            "previous_sources": list(previous),
            "remaining_sources": list(await _sources(db, target_membership_id)),
            "authorization_version_before": before,
            "authorization_version_after": before + 1,
        },
        correlation_id=correlation_id,
    )
    return status


async def release_hold(
    db: AsyncSession,
    project_id: UUID,
    *,
    actor_membership_id: UUID,
    target_membership_id: UUID,
    source: str,
    correlation_id: str | None = None,
) -> ReleaseResult:
    actor, target = await _views(db, project_id, actor_membership_id, target_membership_id)
    decision = authorize_hold_release(
        actor.as_actor(), target.as_target(), project_id=project_id, source=source
    )
    if not decision.allowed:
        raise HoldError(decision.code)

    previous = await _sources(db, target_membership_id)
    if source not in previous:
        return ReleaseResult(target_membership_id, source, False, previous, previous)

    before = await _version(db, target_membership_id)
    await db.execute(
        delete(ProjectMembershipSuspension).where(
            ProjectMembershipSuspension.membership_id == target_membership_id,
            ProjectMembershipSuspension.source == source,
        )
    )
    await db.flush()
    await sync_membership_status(db, target_membership_id)
    remaining = await _sources(db, target_membership_id)
    await _bump_version(db, target_membership_id)

    event = (
        "project.member.legacy_suspension_released"
        if source == LEGACY
        else "project.member.hold_released"
    )
    await record_event(
        db,
        project_id=project_id,
        event=event,
        actor_membership_id=actor_membership_id,
        target_membership_id=target_membership_id,
        payload={
            "source": source,
            "previous_sources": list(previous),
            "remaining_sources": list(remaining),
            "authorization_version_before": before,
            "authorization_version_after": before + 1,
        },
        correlation_id=correlation_id,
    )
    return ReleaseResult(target_membership_id, source, True, previous, remaining)
