"""Meeting management mixin for gRPC service."""

from __future__ import annotations

import asyncio
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Protocol, Self, cast
from uuid import UUID

from noteflow.config.constants import (
    DEFAULT_MEETING_TITLE,
    ERROR_INVALID_PROJECT_ID_PREFIX,
)
from noteflow.domain.entities import Meeting
from noteflow.domain.value_objects import MeetingState
from noteflow.infrastructure.logging import get_logger, get_workspace_id

from ..proto import noteflow_pb2
from .converters import meeting_to_proto, parse_meeting_id_or_abort
from .errors import ENTITY_MEETING, abort_invalid_argument, abort_not_found

if TYPE_CHECKING:
    from noteflow.application.services.project_service import ProjectService
    from noteflow.application.services.webhook_service import WebhookService
    from noteflow.domain.ports.repositories import (
        DiarizationJobRepository,
        MeetingRepository,
        SegmentRepository,
        SummaryRepository,
    )
    from noteflow.domain.ports.repositories.identity import ProjectRepository, WorkspaceRepository
    from noteflow.domain.ports.unit_of_work import UnitOfWork
    from noteflow.infrastructure.audio.writer import MeetingAudioWriter

    from ._types import GrpcContext

logger = get_logger(__name__)

# Timeout for waiting for stream to exit gracefully
STOP_WAIT_TIMEOUT_SECONDS: float = 2.0


class MeetingRepositoryProvider(Protocol):
    """Repository provider protocol for meeting operations."""

    @property
    def meetings(self) -> MeetingRepository: ...

    @property
    def segments(self) -> SegmentRepository: ...

    @property
    def summaries(self) -> SummaryRepository: ...

    @property
    def diarization_jobs(self) -> DiarizationJobRepository: ...

    @property
    def projects(self) -> ProjectRepository: ...

    @property
    def workspaces(self) -> WorkspaceRepository: ...

    @property
    def supports_diarization_jobs(self) -> bool: ...

    @property
    def supports_projects(self) -> bool: ...

    @property
    def supports_workspaces(self) -> bool: ...

    async def commit(self) -> None: ...

    async def __aenter__(self) -> Self: ...

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: object,
    ) -> None: ...


class _HasField(Protocol):
    def HasField(self, field_name: str) -> bool: ...


class MeetingServicer(Protocol):
    """Protocol for hosts that support meeting operations."""

    project_service: ProjectService | None
    webhook_service: WebhookService | None
    active_streams: set[str]
    stop_requested: set[str]
    audio_writers: dict[str, MeetingAudioWriter]

    def create_repository_provider(self) -> MeetingRepositoryProvider | UnitOfWork: ...

    def close_audio_writer(self, meeting_id: str) -> None: ...

    async def fire_stop_webhooks(self, meeting: Meeting) -> None: ...


async def _resolve_active_project_id(
    host: MeetingServicer,
    repo: MeetingRepositoryProvider,
) -> UUID | None:
    """Resolve active project ID from workspace context.

    Return the active project ID for the current workspace if:
    - project_service is available on the host
    - Repository supports projects and workspaces
    - A valid workspace_id is in context
    - An active project is set for that workspace

    Args:
        host: ServicerHost with project_service.
        repo: Unit of work with capability flags.

    Returns:
        Active project UUID or None if not resolvable.
    """
    if (
        host.project_service is None
        or not repo.supports_projects
        or not repo.supports_workspaces
    ):
        return None

    workspace_id = get_workspace_id()
    if not workspace_id:
        return None

    try:
        workspace_uuid = UUID(workspace_id)
    except ValueError:
        return None

    _, active_project = await host.project_service.get_active_project(
        repo, workspace_uuid
    )
    return active_project.id if active_project else None


class MeetingMixin:
    """Mixin providing meeting CRUD functionality.

    Requires host to implement MeetingServicer protocol.
    Works with both database and memory backends via RepositoryProvider.
    """

    async def CreateMeeting(
        self: MeetingServicer,
        request: noteflow_pb2.CreateMeetingRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.Meeting:
        """Create a new meeting."""
        metadata_map = cast(Mapping[str, str], request.metadata)
        metadata: dict[str, str] = dict(metadata_map) if metadata_map else {}
        project_id: UUID | None = None
        if cast(_HasField, request).HasField("project_id") and request.project_id:
            try:
                project_id = UUID(request.project_id)
            except ValueError:
                logger.warning(
                    "CreateMeeting: invalid project_id format",
                    project_id=request.project_id,
                )
                await abort_invalid_argument(context, f"{ERROR_INVALID_PROJECT_ID_PREFIX}{request.project_id}")

        async with self.create_repository_provider() as repo:
            if project_id is None:
                project_id = await _resolve_active_project_id(self, repo)

            meeting = Meeting.create(
                title=request.title,
                metadata=metadata,
                project_id=project_id,
            )
            saved = await repo.meetings.create(meeting)
            await repo.commit()
            logger.info(
                "Meeting created",
                meeting_id=str(saved.id),
                title=saved.title or DEFAULT_MEETING_TITLE,
                project_id=str(project_id) if project_id else None,
            )
            return meeting_to_proto(saved)

    async def StopMeeting(
        self: MeetingServicer,
        request: noteflow_pb2.StopMeetingRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.Meeting:
        """Stop a meeting using graceful STOPPING -> STOPPED transition.

        If the meeting is actively streaming, signals the stream to stop
        and waits briefly for it to exit before closing resources.
        """
        meeting_id = request.meeting_id
        logger.info("StopMeeting requested", meeting_id=meeting_id)

        # Signal stop to active stream and wait for graceful exit
        if meeting_id in self.active_streams:
            self.stop_requested.add(meeting_id)
            # Wait briefly for stream to detect stop request and exit
            wait_iterations = int(STOP_WAIT_TIMEOUT_SECONDS * 10)  # 100ms intervals
            for _ in range(wait_iterations):
                if meeting_id not in self.active_streams:
                    break
                await asyncio.sleep(0.1)
            # Clean up stop request even if stream didn't exit
            self.stop_requested.discard(meeting_id)

        # Close audio writer if open (stream cleanup may have done this)
        if meeting_id in self.audio_writers:
            self.close_audio_writer(meeting_id)

        parsed_meeting_id = await parse_meeting_id_or_abort(meeting_id, context)
        async with self.create_repository_provider() as repo:
            meeting = await repo.meetings.get(parsed_meeting_id)
            if meeting is None:
                logger.warning("StopMeeting: meeting not found", meeting_id=meeting_id)
                await abort_not_found(context, ENTITY_MEETING, meeting_id)
                raise  # Unreachable but helps type checker

            previous_state = meeting.state.value

            # Idempotency: return success if already stopped/stopping/completed
            terminal_states = (MeetingState.STOPPED, MeetingState.STOPPING, MeetingState.COMPLETED)
            if meeting.state in terminal_states:
                logger.debug("StopMeeting: already terminal", meeting_id=meeting_id, state=meeting.state.value)
                return meeting_to_proto(meeting)

            try:
                meeting.begin_stopping()  # RECORDING -> STOPPING -> STOPPED
                meeting.stop_recording()
            except ValueError as e:
                logger.error("StopMeeting: invalid transition", meeting_id=meeting_id, state=previous_state, error=str(e))
                await abort_invalid_argument(context, str(e))
            await repo.meetings.update(meeting)
            # Clean up streaming diarization turns if DB supports it
            if repo.supports_diarization_jobs:
                await repo.diarization_jobs.clear_streaming_turns(meeting_id)
            await repo.commit()
            logger.info("Meeting stopped", meeting_id=meeting_id, from_state=previous_state, to_state=meeting.state.value)
            await self.fire_stop_webhooks(meeting)
            return meeting_to_proto(meeting)

    async def fire_stop_webhooks(self: MeetingServicer, meeting: Meeting) -> None:
        """Trigger webhooks for meeting stop (fire-and-forget)."""
        if self.webhook_service is None:
            return
        try:
            await self.webhook_service.trigger_recording_stopped(
                meeting_id=str(meeting.id),
                title=meeting.title or DEFAULT_MEETING_TITLE,
                duration_seconds=meeting.duration_seconds or 0.0,
            )
        except Exception:
            logger.exception("Failed to trigger recording.stopped webhooks")
        try:
            await self.webhook_service.trigger_meeting_completed(meeting)
        except Exception:
            logger.exception("Failed to trigger meeting.completed webhooks")

    async def ListMeetings(
        self: MeetingServicer,
        request: noteflow_pb2.ListMeetingsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ListMeetingsResponse:
        """List meetings."""
        limit = request.limit or 100
        offset = request.offset or 0
        sort_desc = request.sort_order != noteflow_pb2.SORT_ORDER_CREATED_ASC
        state_values = cast(Sequence[int], request.states)
        states = [MeetingState(s) for s in state_values] if state_values else None
        project_id: UUID | None = None

        if cast(_HasField, request).HasField("project_id") and request.project_id:
            try:
                project_id = UUID(request.project_id)
            except ValueError:
                await abort_invalid_argument(context, f"{ERROR_INVALID_PROJECT_ID_PREFIX}{request.project_id}")

        async with self.create_repository_provider() as repo:
            if project_id is None:
                project_id = await _resolve_active_project_id(self, repo)

            meetings, total = await repo.meetings.list_all(
                states=states,
                limit=limit,
                offset=offset,
                sort_desc=sort_desc,
                project_id=project_id,
            )
            logger.debug(
                "ListMeetings returned",
                count=len(meetings),
                total=total,
                limit=limit,
                offset=offset,
                project_id=str(project_id) if project_id else None,
            )
            return noteflow_pb2.ListMeetingsResponse(
                meetings=[meeting_to_proto(m, include_segments=False) for m in meetings],
                total_count=total,
            )

    async def GetMeeting(
        self: MeetingServicer,
        request: noteflow_pb2.GetMeetingRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.Meeting:
        """Get meeting details."""
        logger.debug(
            "GetMeeting requested",
            meeting_id=request.meeting_id,
            include_segments=request.include_segments,
            include_summary=request.include_summary,
        )
        meeting_id = await parse_meeting_id_or_abort(request.meeting_id, context)
        async with self.create_repository_provider() as repo:
            meeting = await repo.meetings.get(meeting_id)
            if meeting is None:
                logger.warning("GetMeeting: meeting not found", meeting_id=request.meeting_id)
                await abort_not_found(context, ENTITY_MEETING, request.meeting_id)
                raise  # Unreachable but helps type checker
            # Load segments if requested
            if request.include_segments:
                segments = await repo.segments.get_by_meeting(meeting.id)
                meeting.segments = list(segments)
            # Load summary if requested
            if request.include_summary:
                summary = await repo.summaries.get_by_meeting(meeting.id)
                meeting.summary = summary
            return meeting_to_proto(
                meeting,
                include_segments=request.include_segments,
                include_summary=request.include_summary,
            )

    async def DeleteMeeting(
        self: MeetingServicer,
        request: noteflow_pb2.DeleteMeetingRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.DeleteMeetingResponse:
        """Delete a meeting."""
        logger.info("DeleteMeeting requested", meeting_id=request.meeting_id)
        meeting_id = await parse_meeting_id_or_abort(request.meeting_id, context)
        async with self.create_repository_provider() as repo:
            success = await repo.meetings.delete(meeting_id)
            if success:
                await repo.commit()
                logger.info("Meeting deleted", meeting_id=request.meeting_id)
                return noteflow_pb2.DeleteMeetingResponse(success=True)
            logger.warning("DeleteMeeting: meeting not found", meeting_id=request.meeting_id)
            await abort_not_found(context, ENTITY_MEETING, request.meeting_id)
