"""Sync orchestration mixin for gRPC service."""

from __future__ import annotations

import asyncio
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from uuid import UUID

from noteflow.domain.entities import Integration, SyncRun
from noteflow.domain.ports.unit_of_work import UnitOfWork
from noteflow.infrastructure.logging import get_logger
from noteflow.infrastructure.persistence.constants import DEFAULT_LIST_LIMIT

from ..proto import noteflow_pb2
from .converters import sync_run_to_proto
from .errors import (
    ENTITY_INTEGRATION,
    ENTITY_SYNC_RUN,
    abort_failed_precondition,
    abort_invalid_argument,
    abort_not_found,
    abort_unavailable,
    parse_integration_id,
)

if TYPE_CHECKING:
    from .protocols import ServicerHost

from ._types import GrpcContext

logger = get_logger(__name__)

# Sync run cache expiry time in seconds (Sprint GAP-002: State Synchronization)
SYNC_RUN_CACHE_EXPIRY_SECONDS = 60

_ERR_CALENDAR_NOT_ENABLED = "Calendar integration not enabled"


def _format_enum_value(value: str | None) -> str:
    """Format an enum value to string."""
    return "" if value is None else value


def _integration_to_proto(integration: Integration) -> noteflow_pb2.IntegrationInfo:
    """Convert domain integration to protobuf IntegrationInfo.

    Extracts integration attributes and formats them for the proto message.
    """
    return noteflow_pb2.IntegrationInfo(
        id=str(integration.id),
        name=integration.name,
        type=_format_enum_value(integration.type),
        status=_format_enum_value(integration.status),
        workspace_id=str(integration.workspace_id),
    )


class SyncMixin:
    """Mixin providing integration sync orchestration functionality.

    Handles starting, monitoring, and listing sync operations for integrations.
    """

    # In-memory cache for active sync runs (cleared on completion)
    sync_runs: dict[UUID, SyncRun]
    # Track when each sync run was cached (Sprint GAP-002: State Synchronization)
    sync_run_cache_times: dict[UUID, datetime]

    def ensure_sync_runs_cache(self: ServicerHost) -> dict[UUID, SyncRun]:
        """Ensure the sync runs cache exists."""
        if not hasattr(self, "sync_runs"):
            self.sync_runs = {}
        if not hasattr(self, "sync_run_cache_times"):
            self.sync_run_cache_times = {}
        return self.sync_runs

    def cache_sync_run(self: ServicerHost, sync_run: SyncRun) -> None:
        """Cache a sync run with timestamp tracking (Sprint GAP-002)."""
        cache = self.ensure_sync_runs_cache()
        cache[sync_run.id] = sync_run
        self.sync_run_cache_times[sync_run.id] = datetime.now(UTC)

    def get_sync_run_expires_at(self: ServicerHost, sync_run_id: UUID) -> str | None:
        """Get expiry timestamp for a cached sync run (Sprint GAP-002)."""
        if not hasattr(self, "sync_run_cache_times"):
            return None
        cache_time = self.sync_run_cache_times.get(sync_run_id)
        if cache_time is None:
            return None
        expires_at = cache_time + timedelta(seconds=SYNC_RUN_CACHE_EXPIRY_SECONDS)
        return expires_at.isoformat()

    async def StartIntegrationSync(
        self: ServicerHost,
        request: noteflow_pb2.StartIntegrationSyncRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.StartIntegrationSyncResponse:
        """Start a sync operation for an integration."""
        if self.calendar_service is None:
            await abort_unavailable(context, _ERR_CALENDAR_NOT_ENABLED)

        integration_id = await parse_integration_id(request.integration_id, context)

        async with self.create_repository_provider() as uow:
            integration, integration_id = await self.resolve_integration(uow, integration_id, context, request)
            if integration is None:
                return noteflow_pb2.StartIntegrationSyncResponse()

            provider_value = integration.config.get("provider") if integration.config else None
            provider = provider_value if isinstance(provider_value, str) else None
            if not provider:
                await abort_failed_precondition(context, "Integration provider not configured")
                return noteflow_pb2.StartIntegrationSyncResponse()

            sync_run = SyncRun.start(integration_id)
            sync_run = await uow.integrations.create_sync_run(sync_run)
            await uow.commit()

        self.cache_sync_run(sync_run)
        asyncio.create_task(
            self.perform_sync(integration_id, sync_run.id, str(provider)),
            name=f"sync-{sync_run.id}",
        ).add_done_callback(lambda _: None)
        logger.info("Started sync run %s for integration %s", sync_run.id, integration_id)
        return noteflow_pb2.StartIntegrationSyncResponse(sync_run_id=str(sync_run.id), status="running")

    async def resolve_integration(
        self: ServicerHost,
        uow: UnitOfWork,
        integration_id: UUID,
        context: GrpcContext,
        request: noteflow_pb2.StartIntegrationSyncRequest,
    ) -> tuple[Integration | None, UUID]:
        """Resolve integration by ID with provider fallback.

        Returns (integration, resolved_id) tuple. Returns (None, id) if not found after aborting.
        """
        from noteflow.domain.value_objects import OAuthProvider

        integration = await uow.integrations.get(integration_id)
        if integration is not None:
            return integration, integration_id

        # Fallback: try connected calendar integrations by provider
        for provider_name in [OAuthProvider.GOOGLE, OAuthProvider.OUTLOOK]:
            candidate = await uow.integrations.get_by_provider(provider=provider_name, integration_type="calendar")
            if candidate is not None and candidate.is_connected:
                return candidate, candidate.id

        await abort_not_found(context, ENTITY_INTEGRATION, request.integration_id)
        return None, integration_id

    async def perform_sync(
        self: ServicerHost,
        integration_id: UUID,
        sync_run_id: UUID,
        provider: str,
    ) -> None:
        """Perform the actual sync operation (background task).

        Fetches calendar events and updates the sync run status.
        """
        cache = self.ensure_sync_runs_cache()

        try:
            items_synced = await self.execute_sync_fetch(provider)
            sync_run = await self.complete_sync_run(
                integration_id, sync_run_id, items_synced
            )
            if sync_run:
                self.cache_sync_run(sync_run)
            logger.info(
                "Sync run %s completed: %d items synced",
                sync_run_id,
                items_synced,
            )

        # INTENTIONAL BROAD HANDLER: Sync run error boundary
        # - Calendar sync can fail in many ways (network, auth, parsing)
        # - Must capture any failure and update sync run status
        except Exception as e:
            logger.exception("Sync run %s failed: %s", sync_run_id, e)
            sync_run = await self.fail_sync_run(sync_run_id, str(e))
            if sync_run:
                self.cache_sync_run(sync_run)

        finally:
            # Clean up cache after a delay (keep for status queries)
            # Sprint GAP-002: SYNC_RUN_CACHE_EXPIRY_SECONDS
            await asyncio.sleep(SYNC_RUN_CACHE_EXPIRY_SECONDS)
            cache.pop(sync_run_id, None)
            if hasattr(self, "sync_run_cache_times"):
                self.sync_run_cache_times.pop(sync_run_id, None)

    async def execute_sync_fetch(self: ServicerHost, provider: str) -> int:
        """Execute the calendar fetch and return items count."""
        calendar_service = self.calendar_service
        if calendar_service is None:
            msg = "Calendar service not available"
            raise RuntimeError(msg)

        events = await calendar_service.list_calendar_events(
            provider=provider,
            hours_ahead=168,  # 1 week
            limit=DEFAULT_LIST_LIMIT,
        )
        return len(events)

    async def complete_sync_run(
        self: ServicerHost,
        integration_id: UUID,
        sync_run_id: UUID,
        items_synced: int,
    ) -> SyncRun | None:
        """Mark sync run as complete and update integration last_sync."""
        async with self.create_repository_provider() as uow:
            repo = uow.integrations
            sync_run = await repo.get_sync_run(sync_run_id)
            if sync_run is None:
                return None

            sync_run.complete(items_synced=items_synced)
            await repo.update_sync_run(sync_run)

            integration = await repo.get(integration_id)
            if integration:
                integration.record_sync()
                await repo.update(integration)

            await uow.commit()
            return sync_run

    async def fail_sync_run(
        self: ServicerHost,
        sync_run_id: UUID,
        error_message: str,
    ) -> SyncRun | None:
        """Mark sync run as failed with error message."""
        async with self.create_repository_provider() as uow:
            repo = uow.integrations
            sync_run = await repo.get_sync_run(sync_run_id)
            if sync_run is None:
                return None

            sync_run.fail(error_message)
            await repo.update_sync_run(sync_run)
            await uow.commit()
            return sync_run

    async def GetSyncStatus(
        self: ServicerHost,
        request: noteflow_pb2.GetSyncStatusRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetSyncStatusResponse:
        """Get the status of a sync operation."""
        try:
            sync_run_id = UUID(request.sync_run_id)
        except ValueError:
            await abort_invalid_argument(
                context,
                f"Invalid sync_run_id format: {request.sync_run_id}",
            )
            return noteflow_pb2.GetSyncStatusResponse()

        # Check in-memory cache first (fast path for active syncs)
        cache = self.ensure_sync_runs_cache()
        sync_run = cache.get(sync_run_id)

        # Fall back to database
        if sync_run is None:
            async with self.create_repository_provider() as uow:
                sync_run = await uow.integrations.get_sync_run(sync_run_id)

        if sync_run is None:
            await abort_not_found(context, ENTITY_SYNC_RUN, request.sync_run_id)
            return noteflow_pb2.GetSyncStatusResponse()

        # Sprint GAP-002: Include expiry metadata
        expires_at = self.get_sync_run_expires_at(sync_run_id)

        return noteflow_pb2.GetSyncStatusResponse(
            status=sync_run.status.value,
            items_synced=sync_run.items_synced,
            items_total=sync_run.items_total or 0,
            error_message=sync_run.error_message or "",
            duration_ms=sync_run.duration_ms or 0,
            expires_at=expires_at,
        )

    async def ListSyncHistory(
        self: ServicerHost,
        request: noteflow_pb2.ListSyncHistoryRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ListSyncHistoryResponse:
        """List sync history for an integration."""
        integration_id = await parse_integration_id(request.integration_id, context)
        limit = min(request.limit or 20, 100)
        offset = request.offset or 0

        async with self.create_repository_provider() as uow:
            runs, total = await uow.integrations.list_sync_runs(
                integration_id=integration_id,
                limit=limit,
                offset=offset,
            )

        return noteflow_pb2.ListSyncHistoryResponse(
            runs=[sync_run_to_proto(run) for run in runs],
            total_count=total,
        )

    async def GetUserIntegrations(
        self: ServicerHost,
        request: noteflow_pb2.GetUserIntegrationsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetUserIntegrationsResponse:
        """Get all integrations for the current user/workspace.

        Used by clients to validate cached integration IDs at startup.
        Returns only integrations the user has access to based on identity context.
        """
        async with self.create_repository_provider() as uow:
            # Get all integrations (workspace filtering handled by repository)
            integrations = await uow.integrations.list_all()

        return noteflow_pb2.GetUserIntegrationsResponse(
            integrations=[_integration_to_proto(integration) for integration in integrations]
        )
