import asyncio
import logging
from typing import Optional, Set, Sequence

import aiohttp

from clarabot_nano.common.config.client import AbstractAioConfiguredWorkerProcess
from clarabot_nano.common.core.message import (
    MSG_TYPE_REQUEST_STATE_UPDATE,
    MSG_TYPE_INDEXER_SYNC,
    MSG_TYPE_CONTROL_INDEXER_WORK,
)
from clarabot_nano.common.solr.api import SolrApiError
from clarabot_nano.common.spawn.constants import ShortNames
from clarabot_nano.common.zmq.requester import AioClientRequester
from clarabot_nano.indexer.config import IndexerConfig
from clarabot_nano.indexer.event import SchedulerFilesystemEvent
from clarabot_nano.indexer.idxstatedb import IndexingStateDatabase
from clarabot_nano.indexer.initializer import IndexerSolrSchemaStateBindInitializerMixin
from clarabot_nano.indexer.scheduler import IndexingSchedulerEngine
from clarabot_nano.indexer.utility import get_index_supported_languages
from clarabot_nano.utility.path.common import normalize_validate_absolute_path
from clarabot_nano.utility.path.validate import ValidationError
from clarabot_nano.utility.serialize import msgpack_dumps, msgpack_loads

logger = logging.getLogger(__name__)


class IndexerProcess(AbstractAioConfiguredWorkerProcess, IndexerSolrSchemaStateBindInitializerMixin):
    def __init__(self):
        AbstractAioConfiguredWorkerProcess.__init__(self, ShortNames.INDEXER, extracted_config_cls=IndexerConfig)
        self._core_requester: AioClientRequester = None
        self._event_queue: asyncio.Queue = None
        self._init_failures = 0
        self._supported_languages = get_index_supported_languages()
        self._core_handler.add_handler(MSG_TYPE_INDEXER_SYNC, self._handle_core_sync)

    async def _on_new_extracted_config(self, old_extracted_config: Optional[IndexerConfig]):
        # the changes are always work relevant, no more optimization here
        self.queue_main_event({"type": "new-config"})

    async def _process_main_event(self, event):
        """
        Work task is not running, execute command requests or events made from config changes.
        """
        t = event.get("type")
        if t == "new-config":
            pass  # new config has the updated data, that will be used for new work task
        else:  # pragma: no cover
            raise Exception(f"unknown indexer main event {t}")

    async def _main_wrapper(self):
        self._core_requester = self.create_core_requester()
        self._event_queue = asyncio.Queue(maxsize=4096)
        with self._core_requester:
            await AbstractAioConfiguredWorkerProcess._main_wrapper(self)

    async def _work(self):
        await self._set_status("starting")
        ec = self._extracted_config
        if not ec.solr_port or not ec.solr_key or not ec.tika_port:
            while True:
                # This will block if no solr/tika config is available when starting.
                # When a solr starts up this work task will end and a new one will start with the new config.
                await asyncio.sleep(2.5)
        async with aiohttp.ClientSession() as session:
            with IndexingStateDatabase.create(
                asyncio.get_event_loop(), str(ec.data_dir / "idxr.db"), sqlite_timeout=10.0
            ) as index_state_db:
                try:
                    core_name, languages, core_config = await self._initialise(session, ec, index_state_db)
                except (SolrApiError, aiohttp.ClientError):
                    # these are expected to happen so only log them after N consecutive failures
                    if self._init_failures < 50:
                        self._init_failures += 1
                        await asyncio.sleep(0.5)
                    else:
                        logger.exception("Failed to initialise search indexing (stalling)")
                        await asyncio.sleep(5.0)
                except Exception:
                    logger.exception("Failed to initialise search indexing")
                    await asyncio.sleep(5.0)  # delay exit so retries are not too forceful
                else:
                    await self._set_bind_core_name(core_name)  # NOTE: will config change can feedback here to restart
                    self._init_failures = 0
                    await self._create_scheduler(session, ec, index_state_db, core_name, languages, core_config).run()

    def _create_scheduler(
        self, session, ec, index_state_db, core_name, languages, core_config
    ) -> IndexingSchedulerEngine:
        return IndexingSchedulerEngine(
            session,
            ec,
            index_state_db,
            core_name,
            languages,
            core_config,
            self._event_queue,
            self._set_status,
            self._notify_work,
        )

    def get_supported_languages(self) -> Set[str]:
        return self._supported_languages

    async def _set_bind_core_name(self, name: Optional[str]):
        await self._core_requester.request(
            [
                MSG_TYPE_REQUEST_STATE_UPDATE,
                msgpack_dumps({"_handler_key": "set_solr_core", "name": name}),
            ]
        )

    async def _set_status(self, status: str):
        await self._core_requester.request(
            [
                MSG_TYPE_REQUEST_STATE_UPDATE,
                msgpack_dumps({"_handler_key": "set_indexer_status", "status": status}),
            ]
        )

    def _notify_work(self, work: str):
        self._core_requester.notify([MSG_TYPE_CONTROL_INDEXER_WORK, msgpack_dumps({"work": work})])

    def _handle_core_sync(self, message: Sequence[bytes]):
        """
        This is called on the aio-thread so interacting with the queue is fine.
        """
        if len(message) != 2:
            raise Exception("Invalid sync command")
        data = msgpack_loads(message[1])
        if isinstance(path := data.get("path"), str):
            try:
                self._event_queue.put_nowait(
                    SchedulerFilesystemEvent(normalize_validate_absolute_path(path), data.get("exists"))
                )
            except asyncio.QueueFull:
                logger.warning("Internal sync event dropped because queue is full")
            except (OSError, ValidationError):
                logger.warning("Internal sync event dropped because path", exc_info=True)
