"""Converge project memberships with Workspace authority (Workstream D trigger).

Events only say that Workspace authority may have changed. Each run reads the current
Workspace state and converges, so a missed, late, or replayed event is harmless. The
hourly sweep repairs drift that no event ever reported. Each project is isolated in its
own savepoint, so one unsafe projection never blocks or partially writes another.
"""

from __future__ import annotations

from uuid import UUID

import structlog

from src.config import get_settings

logger = structlog.get_logger()

_SWEEP_BATCH = 100


async def _db_factory(ctx: dict):
    from src.core.database import async_session_factory

    return ctx.get("db_factory") or async_session_factory


def _disabled() -> dict:
    return {"skipped": "projection_disabled"}


async def _sync_projects(db_factory, project_ids: list[UUID]) -> dict:
    """Push canonical projections for these projects. Failures are recorded as pending by the
    sync itself and never raised, so they cannot undo or fail the committed reconciliation."""
    from src.domain.project_auth.membership_projection_sync import (
        build_projection_writer,
        sync_project_projections,
    )

    from src.domain.project_auth.projection_eligibility import may_write_cloud, project_state

    writer = build_projection_writer(get_settings())
    if writer is None:
        return {"skipped": "projection_writer_unconfigured"}
    totals = {"projects": 0, "synced": 0, "pending": 0, "not_enabled": 0}
    for project_id in project_ids:
        async with db_factory() as db:
            if not may_write_cloud(True, await project_state(db, project_id)):
                totals["not_enabled"] += 1
                continue
            try:
                outcomes = await sync_project_projections(db, project_id, writer)
                await db.commit()
            except Exception:
                await db.rollback()
                logger.exception("membership_projection_sync_failed", project_id=str(project_id))
                continue
        totals["projects"] += 1
        totals["synced"] += sum(1 for o in outcomes if o.status == "synced")
        totals["pending"] += sum(1 for o in outcomes if o.status == "pending")
    return totals


async def reconcile_workspace_projection(ctx: dict, workspace_id: str) -> dict:
    if not get_settings().project_membership_projection_enabled:
        return _disabled()
    from src.domain.project_auth.membership_projection import reconcile_workspace_projects

    db_factory = await _db_factory(ctx)
    async with db_factory() as db:
        outcomes = await reconcile_workspace_projects(db, UUID(workspace_id))
        await db.commit()
    changed = [o.project_id for o in outcomes if o.operations and not o.error]
    await _sync_projects(db_factory, changed)
    errors = {str(o.project_id): o.error for o in outcomes if o.error}
    for project_id, error in errors.items():
        logger.warning("membership_projection_refused", project_id=project_id, reason=error)
    return {
        "workspace_id": workspace_id,
        "projects": len(outcomes),
        "operations": sum(o.operations for o in outcomes),
        "refused": errors,
    }


async def reconcile_project_membership(ctx: dict, project_id: str) -> dict:
    """Repair entry point for one project. Independent of Workspace events."""
    if not get_settings().project_membership_projection_enabled:
        return _disabled()
    from src.domain.project_auth.membership_projection import (
        ProjectionError,
        reconcile_project_from_workspace,
    )

    db_factory = await _db_factory(ctx)
    async with db_factory() as db:
        try:
            async with db.begin_nested():
                plan = await reconcile_project_from_workspace(db, UUID(project_id))
        except ProjectionError as exc:
            await db.rollback()
            return {"project_id": project_id, "operations": 0, "refused": str(exc)}
        await db.commit()
    if plan.operations:
        await _sync_projects(db_factory, [UUID(project_id)])
    return {"project_id": project_id, "operations": len(plan.operations), "held": list(plan.held),
            "blocked": list(plan.blocked)}


async def reconcile_all_project_memberships(ctx: dict) -> dict:
    """Hourly repair sweep over auth-configured projects, committed in batches."""
    if not get_settings().project_membership_projection_enabled:
        return _disabled()
    from src.domain.project_auth.membership_projection import (
        ProjectionError,
        project_ids_page,
        reconcile_project_from_workspace,
    )

    db_factory = await _db_factory(ctx)
    totals = {"projects": 0, "operations": 0, "refused": 0}
    after: UUID | None = None
    while True:
        async with db_factory() as db:
            page = await project_ids_page(db, after, _SWEEP_BATCH)
            if not page:
                break
            for project_id in page:
                totals["projects"] += 1
                try:
                    async with db.begin_nested():
                        plan = await reconcile_project_from_workspace(db, project_id)
                    totals["operations"] += len(plan.operations)
                except ProjectionError as exc:
                    totals["refused"] += 1
                    logger.warning("membership_projection_refused",
                                   project_id=str(project_id), reason=str(exc))
            await db.commit()
            after = page[-1]
        if len(page) < _SWEEP_BATCH:
            break
    return totals


async def sync_membership_projection(ctx: dict, project_id: str) -> dict:
    """Push one project's canonical projections to Cloud (E4). Safe to repeat: unchanged
    projections send nothing."""
    result = await _sync_projects(await _db_factory(ctx), [UUID(project_id)])
    return result


async def sync_all_membership_projections(ctx: dict) -> dict:
    """Converge every auth-configured project's Cloud projection. Pending rows and out-of-date
    projections are both repaired here, so a version change never waits for a pending row."""
    if not get_settings().project_membership_projection_enabled:
        return _disabled()
    from src.domain.project_auth.membership_projection import project_ids_page

    db_factory = await _db_factory(ctx)
    totals = {"projects": 0, "synced": 0, "pending": 0}
    after: UUID | None = None
    while True:
        async with db_factory() as db:
            page = await project_ids_page(db, after, _SWEEP_BATCH)
        if not page:
            break
        partial = await _sync_projects(db_factory, page)
        for key in totals:
            totals[key] += partial.get(key, 0)
        after = page[-1]
        if len(page) < _SWEEP_BATCH:
            break
    return totals

