"""NoteFlow gRPC server entry point (async)."""

from __future__ import annotations

import argparse
import asyncio
import os
import signal
import time
from typing import TYPE_CHECKING, cast

import grpc.aio
from pydantic import ValidationError

from noteflow.config.constants import SETTING_CLOUD_CONSENT_GRANTED
from noteflow.config.settings import get_feature_flags, get_settings
from noteflow.infrastructure.asr import FasterWhisperEngine
from noteflow.infrastructure.asr.engine import VALID_MODEL_SIZES
from noteflow.infrastructure.logging import LoggingConfig, configure_logging, get_logger
from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork
from noteflow.infrastructure.summarization import create_summarization_service

from ._config import (
    DEFAULT_MODEL,
    DEFAULT_PORT,
    AsrConfig,
    DiarizationConfig,
    GrpcServerConfig,
    ServicesConfig,
)
from ._startup import (
    create_calendar_service,
    create_diarization_engine,
    create_ner_service,
    create_webhook_service,
    init_database_and_recovery,
    print_startup_banner,
    setup_summarization_with_consent,
)
from .proto import noteflow_pb2_grpc
from .service import NoteFlowServicer

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker

    from noteflow.config.settings import Settings

logger = get_logger(__name__)


class NoteFlowServer:
    """Async gRPC server for NoteFlow."""

    def __init__(
        self,
        port: int = DEFAULT_PORT,
        asr: AsrConfig | None = None,
        session_factory: async_sessionmaker[AsyncSession] | None = None,
        db_engine: AsyncEngine | None = None,
        services: ServicesConfig | None = None,
    ) -> None:
        """Initialize the server.

        Args:
            port: Port to listen on.
            asr: ASR engine configuration (defaults to AsrConfig()).
            session_factory: Optional async session factory for database.
            db_engine: Optional database engine for lifecycle management.
            services: Optional services configuration grouping all optional services.
        """
        self._port = port
        asr = asr or AsrConfig()
        self._asr_model = asr.model
        self._asr_device = asr.device
        self._asr_compute_type = asr.compute_type
        self._session_factory = session_factory
        self._db_engine = db_engine
        services = services or ServicesConfig()
        self._summarization_service = services.summarization_service
        self._diarization_engine = services.diarization_engine
        self._diarization_refinement_enabled = services.diarization_refinement_enabled
        self._ner_service = services.ner_service
        self._calendar_service = services.calendar_service
        self._webhook_service = services.webhook_service
        self._project_service = services.project_service
        self._server: grpc.aio.Server | None = None
        self._servicer: NoteFlowServicer | None = None

    async def start(self) -> None:
        """Start the async gRPC server."""
        logger.info("Starting NoteFlow gRPC server (async)...")

        # Create ASR engine
        logger.info(
            "Loading ASR model '%s' on %s (%s)...",
            self._asr_model,
            self._asr_device,
            self._asr_compute_type,
        )
        start_time = time.perf_counter()

        asr_engine = FasterWhisperEngine(
            compute_type=self._asr_compute_type,
            device=self._asr_device,
        )
        asr_engine.load_model(self._asr_model)

        load_time = time.perf_counter() - start_time
        logger.info("ASR model loaded in %.2f seconds", load_time)

        # Lazy-create summarization service if not provided
        if self._summarization_service is None:
            self._summarization_service = create_summarization_service()
            logger.info("Summarization service initialized (default factory)")

        # Lazy-create project service if not provided (requires database)
        if self._project_service is None and self._session_factory is not None:
            from noteflow.application.services.project_service import ProjectService

            self._project_service = ProjectService()
            logger.info("Project service initialized")

        # Wire consent persistence if database is available
        await self._wire_consent_persistence()

        # Create servicer with session factory and services config
        self._servicer = NoteFlowServicer(
            asr_engine=asr_engine,
            session_factory=self._session_factory,
            services=ServicesConfig(
                summarization_service=self._summarization_service,
                diarization_engine=self._diarization_engine,
                diarization_refinement_enabled=self._diarization_refinement_enabled,
                ner_service=self._ner_service,
                calendar_service=self._calendar_service,
                webhook_service=self._webhook_service,
                project_service=self._project_service,
            ),
        )

        # Recover orphaned diarization jobs from previous instance
        await self._recover_orphaned_jobs()

        # Create async gRPC server
        self._server = grpc.aio.server(
            options=[
                ("grpc.max_send_message_length", 100 * 1024 * 1024),  # 100MB
                ("grpc.max_receive_message_length", 100 * 1024 * 1024),
            ],
        )

        # Register service
        noteflow_pb2_grpc.add_NoteFlowServiceServicer_to_server(
            cast(noteflow_pb2_grpc.NoteFlowServiceServicer, self._servicer),
            self._server,
        )

        # Bind to port
        address = f"[::]:{self._port}"
        self._server.add_insecure_port(address)

        # Start server
        await self._server.start()
        logger.info("Server listening on %s", address)

    async def stop(self, grace_period: float = 5.0) -> None:
        """Stop the server gracefully.

        Args:
            grace_period: Time to wait for in-flight RPCs.
        """
        if self._server:
            logger.info("Stopping server (grace period: %.1fs)...", grace_period)
            # Clean up servicer state before stopping
            if self._servicer:
                await self._servicer.shutdown()
            await self._server.stop(grace_period)

            # Dispose database engine to release connection pool
            if self._db_engine is not None:
                logger.debug("Disposing database engine connection pool")
                await self._db_engine.dispose()
                self._db_engine = None

            logger.info("Server stopped")

    async def wait_for_termination(self) -> None:
        """Block until server is terminated."""
        if self._server:
            await self._server.wait_for_termination()

    async def _recover_orphaned_jobs(self) -> None:
        """Mark orphaned diarization jobs as failed on startup.

        Any jobs left in QUEUED or RUNNING state from a previous server instance
        are marked as FAILED since the processing task no longer exists.
        """
        if self._session_factory is None:
            logger.debug("Job recovery skipped (no database)")
            return

        settings = get_settings()
        try:
            async with SqlAlchemyUnitOfWork(self._session_factory, settings.meetings_dir) as uow:
                if not uow.supports_diarization_jobs:
                    logger.debug("Job recovery skipped (diarization jobs not supported)")
                    return

                failed_count = await uow.diarization_jobs.mark_running_as_failed()
                await uow.commit()

                if failed_count > 0:
                    logger.warning(
                        "Marked %d orphaned diarization job(s) as failed on startup",
                        failed_count,
                    )
                else:
                    logger.debug("No orphaned diarization jobs to recover")
        except Exception as exc:
            logger.warning("Failed to recover orphaned jobs: %s", exc)

    async def _wire_consent_persistence(self) -> None:
        """Load consent from database and wire persistence callback.

        If database is available, loads the cloud consent setting and sets up
        a callback to persist consent changes. Without database, consent resets
        on server restart (in-memory only).
        """
        if self._session_factory is None or self._summarization_service is None:
            logger.debug("Consent persistence not wired (no database or service)")
            return

        settings = get_settings()
        meetings_dir = settings.meetings_dir

        # Load consent from database
        try:
            async with SqlAlchemyUnitOfWork(self._session_factory, meetings_dir) as uow:
                stored_consent = await uow.preferences.get(SETTING_CLOUD_CONSENT_GRANTED)
                if stored_consent is not None:
                    self._summarization_service.settings.cloud_consent_granted = bool(
                        stored_consent
                    )
                    logger.info(
                        "Loaded cloud consent from database: %s",
                        self._summarization_service.cloud_consent_granted,
                    )
        # INTENTIONAL BROAD HANDLER: Startup resilience
        # - Database may be unavailable at startup
        # - Consent loading should not block server startup
        except Exception:
            logger.exception("Failed to load cloud consent from database")

        # Create consent persistence callback
        session_factory = self._session_factory

        async def persist_consent(granted: bool) -> None:
            """Persist consent change to database."""
            try:
                settings = get_settings()
                async with SqlAlchemyUnitOfWork(session_factory, settings.meetings_dir) as uow:
                    await uow.preferences.set(SETTING_CLOUD_CONSENT_GRANTED, granted)
                    await uow.commit()
                    logger.info("Persisted cloud consent: %s", granted)
            # INTENTIONAL BROAD HANDLER: Fire-and-forget persistence
            # - Consent persistence should not crash application
            # - Next startup will retry
            except Exception:
                logger.exception("Failed to persist cloud consent")

        self._summarization_service.on_consent_change = persist_consent
        logger.debug("Consent persistence callback wired")


async def run_server_with_config(config: GrpcServerConfig) -> None:
    """Run the async gRPC server with structured configuration.

    This is the preferred entry point for running the server programmatically.

    Args:
        config: Complete server configuration.
    """
    # Initialize database if configured
    session_factory: async_sessionmaker[AsyncSession] | None = None
    db_engine: AsyncEngine | None = None
    if config.database_url:
        db_engine, session_factory = await init_database_and_recovery(config.database_url)

    # Create summarization service and configure cloud consent
    summarization_service = create_summarization_service()
    logger.info("Summarization service initialized")

    cloud_llm_provider: str | None = None
    settings = get_settings()
    if session_factory:
        cloud_llm_provider = await setup_summarization_with_consent(
            session_factory, summarization_service, settings
        )

    # Create optional services based on configuration
    ner_service = create_ner_service(session_factory, settings)
    calendar_service = await create_calendar_service(session_factory, settings)
    diarization_engine = create_diarization_engine(config.diarization)
    webhook_service = await create_webhook_service(session_factory, settings) if session_factory else None

    # Log warning if webhooks enabled but no database
    if get_feature_flags().webhooks_enabled and not session_factory:
        logger.warning(
            "Webhooks feature enabled but no database configured. "
            "Webhooks require database for configuration persistence."
        )

    server = NoteFlowServer(
        port=config.port,
        asr=config.asr,
        session_factory=session_factory,
        db_engine=db_engine,
        services=ServicesConfig(
            summarization_service=summarization_service,
            diarization_engine=diarization_engine,
            diarization_refinement_enabled=config.diarization.refinement_enabled,
            ner_service=ner_service,
            calendar_service=calendar_service,
            webhook_service=webhook_service,
        ),
    )

    # Set up graceful shutdown
    loop = asyncio.get_running_loop()
    shutdown_event = asyncio.Event()

    def signal_handler() -> None:
        logger.info("Received shutdown signal...")
        shutdown_event.set()

    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, signal_handler)

    try:
        await server.start()
        print_startup_banner(
            config,
            diarization_engine,
            cloud_llm_provider,
            calendar_service,
            webhook_service,
        )
        await shutdown_event.wait()
    finally:
        if webhook_service is not None:
            await webhook_service.close()
        await server.stop()


async def run_server(
    port: int,
    asr_model: str,
    asr_device: str,
    asr_compute_type: str,
    database_url: str | None = None,
    diarization_enabled: bool = False,
    diarization_hf_token: str | None = None,
    diarization_device: str = "auto",
    diarization_streaming_latency: float | None = None,
    diarization_min_speakers: int | None = None,
    diarization_max_speakers: int | None = None,
    diarization_refinement_enabled: bool = True,
) -> None:
    """Run the async gRPC server (backward-compatible signature).

    Prefer run_server_with_config() for new code.

    Args:
        port: Port to listen on.
        asr_model: ASR model size.
        asr_device: Device for ASR.
        asr_compute_type: ASR compute type.
        database_url: Optional database URL for persistence.
        diarization_enabled: Whether to enable speaker diarization.
        diarization_hf_token: HuggingFace token for pyannote models.
        diarization_device: Device for diarization ("auto", "cpu", "cuda", "mps").
        diarization_streaming_latency: Streaming diarization latency in seconds.
        diarization_min_speakers: Minimum expected speakers for offline diarization.
        diarization_max_speakers: Maximum expected speakers for offline diarization.
        diarization_refinement_enabled: Whether to allow diarization refinement RPCs.
    """
    config = GrpcServerConfig.from_args(
        port=port,
        asr_model=asr_model,
        asr_device=asr_device,
        asr_compute_type=asr_compute_type,
        database_url=database_url,
        diarization_enabled=diarization_enabled,
        diarization_hf_token=diarization_hf_token,
        diarization_device=diarization_device,
        diarization_streaming_latency=diarization_streaming_latency,
        diarization_min_speakers=diarization_min_speakers,
        diarization_max_speakers=diarization_max_speakers,
        diarization_refinement_enabled=diarization_refinement_enabled,
    )
    await run_server_with_config(config)


def _parse_args() -> argparse.Namespace:
    """Parse command-line arguments for the gRPC server."""
    parser = argparse.ArgumentParser(description="NoteFlow gRPC Server")
    parser.add_argument(
        "-p",
        "--port",
        type=int,
        default=DEFAULT_PORT,
        help=f"Port to listen on (default: {DEFAULT_PORT})",
    )
    parser.add_argument(
        "-m",
        "--model",
        type=str,
        default=DEFAULT_MODEL,
        choices=list(VALID_MODEL_SIZES),
        help=f"ASR model size (default: {DEFAULT_MODEL})",
    )
    parser.add_argument(
        "-d",
        "--device",
        type=str,
        default="cpu",
        choices=["cpu", "cuda"],
        help="ASR device (default: cpu)",
    )
    parser.add_argument(
        "-c",
        "--compute-type",
        type=str,
        default="int8",
        choices=["int8", "float16", "float32"],
        help="ASR compute type (default: int8)",
    )
    parser.add_argument(
        "--database-url",
        type=str,
        default=None,
        help="PostgreSQL database URL (overrides NOTEFLOW_DATABASE_URL)",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable verbose logging",
    )
    parser.add_argument(
        "--diarization",
        action="store_true",
        help="Enable speaker diarization (requires pyannote.audio)",
    )
    parser.add_argument(
        "--diarization-hf-token",
        type=str,
        default=None,
        help="HuggingFace token for pyannote models (overrides NOTEFLOW_DIARIZATION_HF_TOKEN)",
    )
    parser.add_argument(
        "--diarization-device",
        type=str,
        default="auto",
        choices=["auto", "cpu", "cuda", "mps"],
        help="Device for diarization (default: auto)",
    )
    return parser.parse_args()


def _build_config(args: argparse.Namespace, settings: Settings | None) -> GrpcServerConfig:
    """Build server configuration from CLI arguments and settings.

    CLI arguments take precedence over environment settings.

    Args:
        args: Parsed command-line arguments.
        settings: Optional application settings from environment.

    Returns:
        Complete server configuration.
    """
    # Database URL: args override settings
    database_url = args.database_url
    if not database_url and settings:
        database_url = str(settings.database_url)
    if not database_url:
        logger.warning("No database URL configured, running in-memory mode")

    # Diarization config: args override settings
    diarization_enabled = args.diarization
    diarization_hf_token = args.diarization_hf_token
    diarization_device = args.diarization_device
    diarization_streaming_latency: float | None = None
    diarization_min_speakers: int | None = None
    diarization_max_speakers: int | None = None
    diarization_refinement_enabled = True

    if settings and not diarization_enabled:
        diarization_enabled = settings.diarization_enabled
    if settings and not diarization_hf_token:
        diarization_hf_token = settings.diarization_hf_token
    if settings and diarization_device == "auto":
        diarization_device = settings.diarization_device
    if settings:
        diarization_streaming_latency = settings.diarization_streaming_latency
        diarization_min_speakers = settings.diarization_min_speakers
        diarization_max_speakers = settings.diarization_max_speakers
        diarization_refinement_enabled = settings.diarization_refinement_enabled

    return GrpcServerConfig(
        port=args.port,
        asr=AsrConfig(
            model=args.model,
            device=args.device,
            compute_type=args.compute_type,
        ),
        database_url=database_url,
        diarization=DiarizationConfig(
            enabled=diarization_enabled,
            hf_token=diarization_hf_token,
            device=diarization_device,
            streaming_latency=diarization_streaming_latency,
            min_speakers=diarization_min_speakers,
            max_speakers=diarization_max_speakers,
            refinement_enabled=diarization_refinement_enabled,
        ),
    )


def main() -> None:
    """Entry point for NoteFlow gRPC server."""
    args = _parse_args()

    # Configure centralized logging with structlog
    # Get log_format from env before full settings load (logging needed to report load errors)
    log_level = "DEBUG" if args.verbose else os.environ.get("NOTEFLOW_LOG_LEVEL", "INFO")
    log_format = os.environ.get("NOTEFLOW_LOG_FORMAT", "auto")
    configure_logging(LoggingConfig(level=log_level, log_format=log_format))

    # Load settings from environment
    try:
        settings = get_settings()
    except (OSError, ValueError, ValidationError) as exc:
        logger.warning("Failed to load settings: %s", exc)
        settings = None

    # Build configuration and run server
    config = _build_config(args, settings)
    asyncio.run(run_server_with_config(config))


if __name__ == "__main__":
    main()
