"""Sync canonical membership projections to Cloud (Workstream E4 orchestration).

Canonical state → build_project_projections → schema-v1 DTO → writer. Only memberships whose
current projection digest differs from the digest Cloud last acknowledged are sent. A Cloud
failure marks the row pending and records the error. It never changes Core authority, and it
never raises, so one failing membership cannot block the others.

Audit is not written here. The mutation that caused a semantic change already audited it.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime
from uuid import UUID

import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.project_auth.cloud_projection import (
    build_project_projections,
    to_cloud_payload,
)
from src.domain.project_auth.cloud_projection_source import load_project_snapshot
from src.domain.project_auth.models import ProjectMembershipProjectionSync
from src.integrations.auth_projection_writer import CloudAuthProjectionWriter

logger = structlog.get_logger()

SYNCED = "synced"
PENDING = "pending"
UNCHANGED = "unchanged"


class ProjectionWriterProtocol:
    async def put_projection(self, project_id: str, cloud_app_user_id: str, payload: dict): ...


@dataclass(frozen=True)
class SyncOutcome:
    membership_id: UUID
    email: str
    status: str
    code: str | None = None


async def sync_project_projections(
    db: AsyncSession,
    project_id: UUID,
    writer: ProjectionWriterProtocol,
) -> list[SyncOutcome]:
    snapshot = await load_project_snapshot(db, project_id)
    projections = build_project_projections(snapshot)
    states = {
        row.membership_id: row
        for row in (
            await db.execute(
                select(ProjectMembershipProjectionSync).where(
                    ProjectMembershipProjectionSync.project_id == project_id
                )
            )
        ).scalars().all()
    }

    outcomes: list[SyncOutcome] = []
    now = datetime.now(UTC)
    project_key = str(project_id)
    for projection in projections:
        if projection.membership_id is None:
            continue
        digest = projection.digest()
        state = states.get(projection.membership_id)
        if state is not None and state.status == SYNCED and state.acknowledged_digest == digest:
            outcomes.append(SyncOutcome(projection.membership_id, projection.email, UNCHANGED))
            continue

        if state is None:
            state = ProjectMembershipProjectionSync(
                membership_id=projection.membership_id,
                project_id=project_id,
                status=PENDING,
                attempts=0,
            )
            db.add(state)
            states[projection.membership_id] = state
        state.attempts = (state.attempts or 0) + 1
        state.last_attempted_at = now

        if projection.cloud_app_user_id is None:
            state.status = PENDING
            state.last_error = "no_cloud_identity"
            outcomes.append(
                SyncOutcome(
                    projection.membership_id, projection.email, PENDING, "no_cloud_identity"
                )
            )
            continue

        result = await writer.put_projection(
            project_key,
            projection.cloud_app_user_id,
            to_cloud_payload(project_key, projection),
        )
        if result.ok:
            state.status = SYNCED
            state.acknowledged_digest = digest
            state.acknowledged_at = now
            state.last_error = None
            outcomes.append(SyncOutcome(projection.membership_id, projection.email, SYNCED))
        else:
            state.status = PENDING
            state.last_error = result.code
            logger.warning(
                "membership_projection_sync_pending",
                project_id=project_key,
                membership_id=str(projection.membership_id),
                reason=result.code,
                status=result.status,
            )
            outcomes.append(
                SyncOutcome(projection.membership_id, projection.email, PENDING, result.code)
            )

    await db.flush()
    return outcomes


def build_projection_writer(settings) -> CloudAuthProjectionWriter | None:
    """None while the projection is disabled or unconfigured: nothing is sent."""

    if not settings.project_membership_projection_enabled:
        return None
    if not settings.cloud_control_api_url or len(settings.cloud_auth_projection_secret) < 32:
        return None
    return CloudAuthProjectionWriter(
        settings.cloud_control_api_url, settings.cloud_auth_projection_secret
    )
