"""Generic Event Service for handling connector-specific events"""

import logging
from typing import Any

from dependency_injector import providers

from app.config.constants.arangodb import (
    AppStatus,
    CollectionNames,
    Connectors,
    EventTypes,
)
from app.connectors.core.constants import ConnectorStateKeys
from app.connectors.core.base.connector.connector_service import BaseConnector
from app.connectors.core.base.data_store.graph_data_store import GraphDataStore
from app.connectors.core.factory.connector_factory import ConnectorFactory
from app.connectors.core.sync.task_manager import sync_task_manager
from app.containers.connector import ConnectorAppContainer
from app.services.graph_db.interface.graph_db_provider import IGraphDBProvider
from app.utils.time_conversion import get_epoch_timestamp_in_ms


class EventService:
    """Event service for handling connector-specific events"""

    def __init__(
        self,
        logger: logging.Logger,
        app_container: ConnectorAppContainer,
        graph_provider: IGraphDBProvider,
    ) -> None:
        self.logger = logger
        self.graph_provider = graph_provider
        self.app_container = app_container

    async def _update_app_status(
        self,
        connector_id: str,
        *,
        status: str | None = None,
        is_locked: bool | None = None,
    ) -> None:
        """Update app document status and/or isLocked for a connector.

        Pass status (an AppStatus value string) and/or is_locked (bool).
        Omitted arguments (None) are not written to the DB.
        Always sets updatedAtTimestamp.
        """
        payload: dict[str, Any] = {
            "id": connector_id,
            "updatedAtTimestamp": get_epoch_timestamp_in_ms(),
        }
        if status is not None:
            payload["status"] = status
        if is_locked is not None:
            payload["isLocked"] = is_locked
        await self.graph_provider.batch_upsert_nodes(
            [payload], CollectionNames.APPS.value
        )

    def _get_connector(self, connector_id: str) -> BaseConnector | None:
        """
        Get connector instance from app_container.
        """
        connector_key = f"{connector_id}_connector"

        if hasattr(self.app_container, connector_key):
            return getattr(self.app_container, connector_key)()
        elif hasattr(self.app_container, 'connectors_map'):
            return self.app_container.connectors_map.get(connector_id)

        return None

    def _store_connector(self, connector_id: str, connector: BaseConnector) -> None:
        """Store a connector instance in the app_container."""
        connector_key = f"{connector_id}_connector"
        if hasattr(self.app_container, connector_key):
            getattr(self.app_container, connector_key).override(providers.Object(connector))
        else:
            if not hasattr(self.app_container, 'connectors_map'):
                self.app_container.connectors_map = {}
            self.app_container.connectors_map[connector_id] = connector

    async def _ensure_connector(self, connector_name: str, connector_id: str) -> BaseConnector | None:
        """
        Get connector from memory, or auto-initialize it if missing.
        Handles the case where the init event was missed or the service restarted.
        Checks that the connector is active in the database before initializing.
        """
        connector = self._get_connector(connector_id)
        if connector:
            return connector

        self.logger.warning(
            f"{connector_name} connector {connector_id} not in memory — attempting auto-initialization"
        )

        try:
            connector_doc = await self.graph_provider.get_document(
                document_key=connector_id,
                collection=CollectionNames.APPS.value,
            )
            if not connector_doc:
                self.logger.error(
                    f"Connector {connector_id} not found in database — skipping initialization"
                )
                return None
            if not connector_doc.get("isActive", False):
                self.logger.warning(
                    f"Connector {connector_id} is not active in database — skipping initialization"
                )
                return None
            config_service = self.app_container.config_service()
            data_store_provider = GraphDataStore(self.logger, self.graph_provider)

            # Extract scope and createdBy from connector document
            scope = connector_doc.get("scope", "personal")
            created_by = connector_doc.get("createdBy", "")

            connector = await ConnectorFactory.initialize_connector(
                name=connector_name,
                logger=self.logger,
                data_store_provider=data_store_provider,
                config_service=config_service,
                connector_id=connector_id,
                scope=scope,
                created_by=created_by,
            )

            if not connector:
                self.logger.error(
                    f"Auto-initialization failed for {connector_name} connector {connector_id}"
                )
                return None

            self._store_connector(connector_id, connector)
            self.logger.info(
                f"Auto-initialized {connector_name} connector {connector_id} successfully"
            )
            return connector
        except Exception as e:
            self.logger.error(
                f"Auto-initialization error for {connector_name} connector {connector_id}: {e}",
                exc_info=True,
            )
            return None

    async def process_event(self, event_type: str, payload: dict[str, Any]) -> bool:
        """Handle connector-specific events - implementing abstract method"""
        try:
            if "." in event_type:
                parts = event_type.split(".")
                connector_name = parts[0].replace(" ", "").lower()
                action = parts[1].lower()
            else:
                self.logger.error(f"Invalid event type format (missing connector prefix): {event_type}")
                return False

            self.logger.info(f"Handling {connector_name} connector event: {action}")

            if action == "init":
                return await self._handle_init(connector_name, payload)
            elif action == "start":
                return await self._handle_start_sync(connector_name, payload)
            elif action == "resync":
                return await self._handle_start_sync(connector_name, payload)
            elif action == "reindex":
                return await self._handle_reindex(connector_name, payload)
            elif action == "delete":
                return await self._handle_delete(connector_name, payload)
            else:
                self.logger.error(f"Unknown {connector_name.capitalize()} connector event type: {action}")
                return False

        except Exception as e:
            self.logger.error(f"Error handling connector event {event_type}: {e}", exc_info=True)
            return False

    async def _handle_init(self, connector_name: str, payload: dict[str, Any]) -> bool:
        """Initializes the event service connector and its dependencies."""
        try:
            org_id = payload.get("orgId")
            connector_id = payload.get("connectorId")
            if not org_id:
                self.logger.error(f"'orgId' is required in the payload for '{connector_name}.init' event.")
                return False

            self.logger.info(f"Initializing {connector_name} init sync service for org_id: {org_id} and connector_id: {connector_id}")
            config_service = self.app_container.config_service()
            # Create data_store manually using already-resolved graph_provider (arango_service) to avoid coroutine reuse
            data_store_provider = GraphDataStore(self.logger, self.graph_provider)
            
            # Fetch scope and createdBy from database App node
            connector_doc = await self.graph_provider.get_document(
                document_key=connector_id,
                collection=CollectionNames.APPS.value,
            )
            if not connector_doc:
                self.logger.error(f"Connector {connector_id} not found in database")
                return False
            scope = connector_doc.get("scope", "personal")
            created_by = connector_doc.get("createdBy", "")
            
            # Use generic connector factory
            connector = await ConnectorFactory.create_connector(
                name=connector_name,
                logger=self.logger,
                data_store_provider=data_store_provider,
                config_service=config_service,
                connector_id=connector_id,
                scope=scope,
                created_by=created_by
            )

            if not connector:
                self.logger.error(f"❌ Failed to create {connector_name} connector")
                return False

            is_initialized = await connector.init()

            if not is_initialized:
                self.logger.error(f"❌ Failed to initialize {connector_name} connector (init returned False). Not storing in container.")
                return False

            self.logger.info(f"✅ Successfully initialized {connector_name} connector")

            self._store_connector(connector_id, connector)
            return True
        except Exception as e:
            self.logger.error(f"Failed to initialize event service connector {connector_name} for org_id %s: %s", org_id, e, exc_info=True)
            return False

    async def _handle_start_sync(self, connector_name: str, payload: dict[str, Any]) -> bool:
        """Queue immediate start of the sync service"""
        org_id = payload.get("orgId")
        connector_id = payload.get("connectorId")
        full_sync = payload.get("fullSync", False)

        if not org_id:
            self.logger.error("orgId is required in start sync payload")
            return False

        connector = await self._ensure_connector(connector_name, connector_id)
        if not connector:
            self.logger.error(f"{connector_name.capitalize()} {connector_id} connector could not be initialized")

        connector = self._get_connector(connector_id)
        if not connector:
            self.logger.error(f"{connector_name.capitalize()} {connector_id} connector not initialized")
            return False

        # Load apps document only after connector is valid (avoids extra I/O on init failure).
        connector_doc = await self.graph_provider.get_document(
            document_key=connector_id,
            collection=CollectionNames.APPS.value,
        )
        pending_full_sync = False
        if connector_doc:
            pending_full_sync = bool(connector_doc.get(ConnectorStateKeys.PENDING_FULL_SYNC, False))

        # Merge payload fullSync with pending flag - if either is true, do full sync
        effective_full_sync = bool(full_sync) or pending_full_sync

        if pending_full_sync:
            self.logger.info(f"Connector {connector_id} has pendingFullSync flag set, will perform full sync")

        self.logger.info(f"Starting {connector_name} sync service for org_id: {org_id}, full_sync: {effective_full_sync} (payload: {full_sync}, pending: {pending_full_sync})")

        if effective_full_sync:
            # --- Full sync: acquire lock for the prep phase ---
            try:
                await self._update_app_status(
                    connector_id,
                    status=AppStatus.FULL_SYNCING.value,
                    is_locked=True,
                )
                self.logger.info(f"🔒 Set status=FULL_SYNCING, isLocked=True for connector {connector_id}")
            except Exception as lock_err:
                self.logger.error(f"❌ Failed to set lock for connector {connector_id}: {lock_err}")
                return False

            try:
                # Delete sync points
                self.logger.info(f"Full sync requested - deleting sync points for connector {connector_id}")
                try:
                    deleted_count, success = await self.graph_provider.delete_sync_points_by_connector_id(
                        connector_id=connector_id
                    )
                    if success:
                        self.logger.info(f"✅ Successfully deleted {deleted_count} sync points for connector {connector_id}")
                    else:
                        self.logger.warning(f"⚠️ Failed to delete sync points for connector {connector_id}, continuing with sync")
                except Exception as sync_point_error:
                    self.logger.error(f"❌ Error deleting sync points for connector {connector_id}: {sync_point_error}")
                    self.logger.warning("Continuing with sync despite sync point deletion failure")

                # Delete sync edges
                try:
                    deleted_edges, success = await self.graph_provider.delete_connector_sync_edges(
                        connector_id=connector_id
                    )
                    if success:
                        self.logger.info(f"Successfully deleted {deleted_edges} sync edges for connector {connector_id}")
                    else:
                        self.logger.warning(f"Failed to delete some sync edges for connector {connector_id}, continuing with sync")
                except Exception as edge_error:
                    self.logger.error(f"Error deleting connector sync edges for {connector_id}: {edge_error}")

                # Schedule the background sync task
                await sync_task_manager.start_sync(connector_id, self._run_sync_and_clear_status(connector, connector_id))
                self.logger.info(f"Started full sync task for {connector_name} {connector_id}")

                # Clear only when we consumed a persisted pending flag (avoids redundant writes on manual full sync).
                if pending_full_sync:
                    try:
                        await self.graph_provider.update_node(
                            connector_id,
                            CollectionNames.APPS.value,
                            {ConnectorStateKeys.PENDING_FULL_SYNC: False},
                        )
                        self.logger.info(f"Cleared pendingFullSync flag for connector {connector_id}")
                    except Exception as clear_err:
                        self.logger.error(f"Failed to clear pendingFullSync flag for connector {connector_id}: {clear_err}")

            except Exception as e:
                self.logger.error(f"❌ Failed during full sync prep for {connector_id}: {e}")
                # Release lock immediately so the connector is not stuck
                try:
                    await self._update_app_status(connector_id, status=AppStatus.IDLE.value, is_locked=False)
                except Exception as revert_err:
                    self.logger.error(f"❌ Failed to revert lock for connector {connector_id}: {revert_err}")
                return False

            # Prep done and task scheduled — release the lock now.
            # Status stays FULL_SYNCING until run_sync() finishes.
            try:
                await self._update_app_status(connector_id, is_locked=False)
                self.logger.info(f"🔓 Released lock for connector {connector_id} (status remains FULL_SYNCING)")
            except Exception as unlock_err:
                self.logger.error(f"❌ Failed to release lock for connector {connector_id}: {unlock_err}")
                # Non-fatal: sync task is already running; log and continue

        else:
            # --- Normal sync: set status only, no lock ---
            try:
                await self._update_app_status(connector_id, status=AppStatus.SYNCING.value)
                self.logger.info(f"Set status=SYNCING for connector {connector_id}")
            except Exception as status_err:
                self.logger.error(f"❌ Failed to set SYNCING status for connector {connector_id}: {status_err}")
                # Non-fatal: proceed with sync even if status write failed

            await sync_task_manager.start_sync(connector_id, self._run_sync_and_clear_status(connector, connector_id))
            self.logger.info(f"Started sync task for {connector_name} {connector_id}")

        return True

    async def _run_sync_and_clear_status(self, connector: BaseConnector, connector_id: str) -> None:
        """Wrap run_sync() so that status is cleared to null when the task finishes."""
        try:
            await connector.run_sync()
        finally:
            try:
                await self._update_app_status(connector_id, status=AppStatus.IDLE.value)
                self.logger.info(f"✅ Cleared status for connector {connector_id} after sync")
            except Exception as clear_err:
                self.logger.error(f"❌ Failed to clear status for connector {connector_id}: {clear_err}")

    async def _handle_reindex(self, connector_name: str, payload: dict[str, Any]) -> bool:
        """Handle reindex event for a connector with pagination support.

        Supports three modes:
        1. Record with depth: recordId + depth - reindex a folder and its children
        2. Record group with depth: recordGroupId + depth - reindex all records in a record group
        3. Status-based: statusFilters - reindex records by indexing status (e.g., FAILED)
        """
        try:

            org_id = payload.get("orgId")
            record_id = payload.get("recordId")
            record_group_id = payload.get("recordGroupId")
            depth = payload.get("depth", 0)
            status_filters = payload.get("statusFilters", ["FAILED"])
            connector_id = payload.get("connectorId")
            user_key = payload.get("userKey")

            if not org_id:
                raise ValueError("orgId is required")

            if not connector_id:
                self.logger.error("connectorId is required in payload for reindex event")
                return False

            connector = await self._ensure_connector(connector_name, connector_id)
            if not connector:
                self.logger.error(f"{connector_name.capitalize()} {connector_id} connector could not be initialized")
                return False

            connector_app_name = connector.app.get_app_name()
            # Get connector enum value
            enum_key = connector_app_name.name.upper().replace(" ", "_")
            connector_enum = getattr(Connectors, enum_key, None)
            if not connector_enum:
                self.logger.error(f"Unknown connector name: {connector_name}")
                return False

            # Log which mode we're using
            if record_id is not None:
                self.logger.info(f"Starting reindex for {connector_name}, {connector_id} connector record {record_id} with depth {depth}")
            elif record_group_id is not None:
                self.logger.info(f"Starting reindex for {connector_name}, {connector_id} connector record group {record_group_id} with depth {depth}")
            else:
                self.logger.info(f"Starting reindex for {connector_name}, {connector_id} connector with status filters: {status_filters}")

            # Fetch and process records in batches of 100
            batch_size = 100
            offset = 0
            total_processed = 0

            while True:
                # Fetch batch of typed Record instances based on mode
                if record_id is not None:
                    # Mode 1: Reindex a folder and its children
                    records = await self.graph_provider.get_records_by_parent_record(
                        parent_record_id=record_id,
                        connector_id=connector_id,
                        org_id=org_id,
                        depth=depth,
                        user_key=user_key,
                        limit=batch_size,
                        offset=offset
                    )
                elif record_group_id is not None:
                    # Mode 2: Reindex records in a record group
                    records = await self.graph_provider.get_records_by_record_group(
                        record_group_id=record_group_id,
                        connector_id=connector_id,
                        org_id=org_id,
                        depth=depth,
                        user_key=user_key,
                        limit=batch_size,
                        offset=offset
                    )
                else:
                    # Mode 3: Reindex by status
                    records = await self.graph_provider.get_records_by_status(
                        org_id=org_id,
                        connector_id=connector_id,
                        status_filters=status_filters,
                        limit=batch_size,
                        offset=offset
                    )

                if not records:
                    break

                self.logger.info(f"Processing batch of {len(records)} records (offset: {offset})")

                # Clear AUTO_INDEX_OFF (etc.) on the same batch we are about to reindex — one graph
                # read path (get_records_* / get_records_by_status) + status updates only.
                record_ids_to_queue = [
                    rid
                    for r in records
                    if (rid := getattr(r, "id", None)) and isinstance(rid, str)
                ]
                if record_ids_to_queue:
                    await self.graph_provider.reset_indexing_status_to_queued_for_record_ids(
                        record_ids_to_queue
                    )

                # Process this batch with typed records
                await connector.reindex_records(records)

                total_processed += len(records)
                offset += batch_size

                # If we got fewer records than batch_size, we've reached the end
                if len(records) < batch_size:
                    break

            self.logger.info(f"✅ Completed reindex for {connector_name} {connector_id} connector. Total records processed: {total_processed}")
            return True

        except Exception as e:
            self.logger.error(f"Failed to handle reindex for {connector_name.capitalize()} {connector_id}: {str(e)}", exc_info=True)
            return False

    async def _handle_delete(self, connector_name: str, payload: dict[str, Any]) -> bool:
        """
        Handle the async connector deletion event.

        Flow:
        1. Call delete_connector_instance on the graph DB
        2. On success: publish bulkDeleteRecords for Qdrant cleanup, delete etcd config
        3. On failure: revert status to null so the connector is not stuck
        """
        org_id = payload.get("orgId")
        connector_id = payload.get("connectorId")
        previous_is_active = payload.get("previousIsActive", False)

        if not org_id or not connector_id:
            self.logger.error("'orgId' and 'connectorId' are required in the delete payload")
            return False

        self.logger.info(f"🗑️ Processing async deletion for {connector_name} connector {connector_id}")

        try:
            # Cancel any running sync task for this connector before deleting
            await sync_task_manager.cancel_sync(connector_id)

            # Delete from graph DB
            result = await self.graph_provider.delete_connector_instance(
                connector_id=connector_id,
                org_id=org_id
            )

            if not result.get("success"):
                raise Exception(result.get("error", "Unknown deletion failure from graph DB"))

            self.logger.info(
                f"✅ Graph DB deletion complete for connector {connector_id}. "
                f"Records: {result.get('deleted_records_count', 0)}"
            )

            # Publish bulkDeleteRecords so the indexing service cleans up Qdrant embeddings
            virtual_record_ids = result.get("virtual_record_ids", [])
            if virtual_record_ids:
                try:
                    await self.app_container.messaging_producer.send_message(
                        topic="record-events",
                        message={
                            "eventType": EventTypes.BULK_DELETE_RECORDS.value,
                            "payload": {
                                "orgId": org_id,
                                "connectorId": connector_id,
                                "virtualRecordIds": virtual_record_ids,
                                "totalRecords": len(virtual_record_ids),
                            },
                            "timestamp": get_epoch_timestamp_in_ms(),
                        },
                    )
                    self.logger.info(f"✅ Published bulkDeleteRecords for {len(virtual_record_ids)} records")
                except Exception as kafka_err:
                    self.logger.error(
                        f"❌ Failed to publish bulkDeleteRecords for connector {connector_id}: {kafka_err}. "
                        f"Embeddings may persist in Qdrant — manual cleanup may be required."
                    )

            # Delete connector credentials from etcd/config store
            try:
                config_service = self.app_container.config_service()
                config_path = f"/services/connectors/{connector_id}/config"
                await config_service.delete_config(config_path)
                self.logger.info(f"✅ Deleted etcd config for connector {connector_id}")
            except Exception as config_err:
                self.logger.error(
                    f"❌ Failed to delete etcd config for connector {connector_id}: {config_err}. "
                    f"Orphaned configuration may remain."
                )

            self.logger.info(f"✅ Async deletion complete for connector {connector_id}")
            return True

        except Exception as e:
            self.logger.error(
                f"❌ Async deletion failed for connector {connector_id}: {e}",
                exc_info=True
            )
            try:
                await self.graph_provider.batch_upsert_nodes(
                    [{
                        "id": connector_id,
                        "status": None,
                        "isActive": previous_is_active,
                        "updatedAtTimestamp": get_epoch_timestamp_in_ms(),
                    }],
                    CollectionNames.APPS.value
                )
                self.logger.info(f"↩️ Reverted status for connector {connector_id}")
            except Exception as revert_err:
                self.logger.error(
                    f"❌ Failed to revert status for connector {connector_id}: {revert_err}. "
                    f"Connector may be stuck in DELETING state."
                )
            return False
