"""Core mutation endpoints for project membership holds (Workstream F).

The acting user is the app-session subject verified by Core (design §4.1). That verifier
depends on Cloud's iss/aud/token_use claims and session introspection (Workstream G), which
are not live yet. Until it exists, every route fails closed with feature_not_enabled, so no
caller can act on an unverified actor.
"""

from __future__ import annotations

from uuid import UUID, uuid4

from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.database import get_db
from src.domain.project_auth.membership_holds import (
    HoldError,
    place_local_hold,
    release_hold,
)

router = APIRouter(prefix="/api/projects/{project_id}/auth", tags=["project-auth"])

_NOT_FOUND = {"membership_not_found", "actor_not_found"}


async def require_verified_app_actor(
    project_id: UUID,
    authorization: str | None = Header(default=None),
    db: AsyncSession = Depends(get_db),
) -> UUID:
    """Resolve the acting membership from a verified app access token (design §4.1).

    Fails closed at every step: the mutation routes return 503 until the feature flag is on,
    the token must verify against this project's keys, the Cloud session must be live, and the
    subject must be a membership of this project.
    """
    from src.config import get_settings
    from src.domain.project_auth.app_token import AppTokenError, verify_app_access_token
    from src.domain.project_auth.models import AuthKeyStatus, ProjectAuthKey, ProjectMembership
    from src.integrations.cloud_session_introspection import (
        SessionCheckUnavailable,
        session_is_active,
    )

    settings = get_settings()
    if not settings.authz_v2_core_mutations_enabled:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={"code": "feature_not_enabled", "project_id": str(project_id)},
        )
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail={"code": "missing_token"})
    token = authorization.removeprefix("Bearer ").strip()

    key_rows = (
        await db.execute(
            select(ProjectAuthKey.key_id, ProjectAuthKey.public_key_pem).where(
                ProjectAuthKey.project_id == project_id,
                ProjectAuthKey.status.in_([AuthKeyStatus.active, AuthKeyStatus.rotated]),
            )
        )
    ).all()
    keys = {key_id: pem for key_id, pem in key_rows}
    try:
        actor = verify_app_access_token(token, project_id=str(project_id), keys=keys)
    except AppTokenError as exc:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"code": "invalid_app_token", "reason": exc.code},
        ) from exc

    try:
        active = await session_is_active(
            settings.cloud_control_api_url,
            settings.cloud_auth_projection_secret,
            str(project_id),
            cloud_app_user_id=actor.cloud_app_user_id,
            session_id=actor.session_id,
        )
    except SessionCheckUnavailable as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={"code": "session_check_unavailable"},
        ) from exc
    if not active:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail={"code": "session_inactive"})

    membership_id = (
        await db.execute(
            select(ProjectMembership.id).where(
                ProjectMembership.project_id == project_id,
                ProjectMembership.cloud_app_user_id == actor.cloud_app_user_id,
            )
        )
    ).scalar_one_or_none()
    if membership_id is None:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail={"code": "actor_not_member"})
    return membership_id


class SuspendRequest(BaseModel):
    reason_code: str = Field(default="unspecified", min_length=1, max_length=64)


class HoldResponse(BaseModel):
    status: str
    remaining_sources: list[str]


async def _schedule_projection_sync(project_id: UUID) -> None:
    """After a committed mutation: attempt the Cloud projection. Best-effort; a lost enqueue is
    repaired by the sync sweep, and Core authority is already committed."""
    from src.config import get_settings

    if not get_settings().project_membership_projection_enabled:
        return
    try:
        from src.workers.enqueue import enqueue_sync_membership_projection

        await enqueue_sync_membership_projection(str(project_id))
    except Exception:
        import structlog

        structlog.get_logger().warning(
            "membership_projection_sync_enqueue_failed", project_id=str(project_id)
        )


def _raise(exc: HoldError) -> None:
    code = status.HTTP_404_NOT_FOUND if exc.code in _NOT_FOUND else status.HTTP_403_FORBIDDEN
    raise HTTPException(status_code=code, detail={"code": exc.code})


@router.post("/members/{membership_id}/suspend", response_model=HoldResponse)
async def suspend_member(
    project_id: UUID,
    membership_id: UUID,
    body: SuspendRequest,
    actor_membership_id: UUID = Depends(require_verified_app_actor),
    db: AsyncSession = Depends(get_db),
    x_correlation_id: str | None = Header(default=None),
) -> HoldResponse:
    try:
        status_value = await place_local_hold(
            db, project_id,
            actor_membership_id=actor_membership_id,
            target_membership_id=membership_id,
            reason_code=body.reason_code,
            correlation_id=x_correlation_id or str(uuid4()),
        )
    except HoldError as exc:
        _raise(exc)
    await db.commit()
    await _schedule_projection_sync(project_id)
    return HoldResponse(status=status_value, remaining_sources=[])


@router.post("/members/{membership_id}/reinstate", response_model=HoldResponse)
async def reinstate_member(
    project_id: UUID,
    membership_id: UUID,
    actor_membership_id: UUID = Depends(require_verified_app_actor),
    db: AsyncSession = Depends(get_db),
    x_correlation_id: str | None = Header(default=None),
) -> HoldResponse:
    """Releases only the local hold. Any remaining hold keeps the membership suspended."""
    try:
        result = await release_hold(
            db, project_id,
            actor_membership_id=actor_membership_id,
            target_membership_id=membership_id,
            source="local",
            correlation_id=x_correlation_id or str(uuid4()),
        )
    except HoldError as exc:
        _raise(exc)
    await db.commit()
    await _schedule_projection_sync(project_id)
    return HoldResponse(status=result.code, remaining_sources=list(result.remaining_sources))


@router.post("/members/{membership_id}/suspensions/legacy/release", response_model=HoldResponse)
async def release_legacy_hold(
    project_id: UUID,
    membership_id: UUID,
    actor_membership_id: UUID = Depends(require_verified_app_actor),
    db: AsyncSession = Depends(get_db),
    x_correlation_id: str | None = Header(default=None),
) -> HoldResponse:
    """Owner-only release of a legacy hold (member.reinstate_legacy). Never automatic."""
    try:
        result = await release_hold(
            db, project_id,
            actor_membership_id=actor_membership_id,
            target_membership_id=membership_id,
            source="legacy",
            correlation_id=x_correlation_id or str(uuid4()),
        )
    except HoldError as exc:
        _raise(exc)
    await db.commit()
    await _schedule_projection_sync(project_id)
    return HoldResponse(status=result.code, remaining_sources=list(result.remaining_sources))
