from __future__ import annotations

import asyncio
import re
import time
from typing import Optional, AsyncContextManager

import aiohttp

from clarabot_nano.indexer.config import IndexerConfig
from clarabot_nano.indexer.extractor.send import SendFileHandler, SendEmptyFileError

ALL_SPACE_SEARCH_RE = re.compile(r"\s+")
SPACE_SEARCH_RE = re.compile(r"[^\S\n\x0b\x0c\r\x85\u2028\u2029]+")
NEWLINE_SEARCH_RE = re.compile(r"[\n\x0b\x0c\r\x85\u2028\u2029]+")
NEWLINE_STRIP_RE = re.compile(r"\s*(?:[\n\x0b\x0c\r\x85\u2028\u2029]+\s*)+")
# STARTS_WITH_BOUNDARY_RE = re.compile(r"^.\b")
# ENDS_WITH_BOUNDARY_RE = re.compile(r"\b.$")


class ExtractError(Exception):
    def __init__(self, extraction: Optional[AbstractExtraction] = None):
        Exception.__init__(self, extraction)

    @property
    def extraction(self) -> Optional[AbstractExtraction]:
        return self.args[0]


class ExtractorInoperable(ExtractError):
    """
    Raised by the Extractor.ensure_operable() and Extractor.extract() methods if the extractor does not work at all.
    """


class RetryExtractError(ExtractError):
    """
    Raised by the Extractor.extract() method if retrying could succeed from an error.
    If no more retry attempts will be made the caller may use the extraction from this instance... maybe there's
    something usable on it.
    """

    def __init__(self, extraction: AbstractExtraction):
        ExtractError.__init__(self, extraction)


class ServerInternalExtractError(RetryExtractError):
    pass


class IgnoredMimeType(ExtractError):
    pass


class EnoughContent(ExtractError):
    pass


class AbstractExtraction(object):
    pass


class AbstractExtractionSource(object):
    async def open(self) -> AsyncContextManager[SendFileHandler]:
        """
        Return a suitable send handler. If there is no content in the source then raise SendEmptyFileError.
        """
        raise NotImplementedError()

    def fallback_summary(self) -> str:
        """
        If no content extraction is possible, this will be used instead.
        """
        raise NotImplementedError()

    def describe(self) -> str:
        """
        Concise representation of the source. For logging an error for example.
        """
        raise NotImplementedError()


class AbstractContentExtractor(object):
    def __init__(self, session: aiohttp.ClientSession, ec: IndexerConfig):
        self.session = session
        self.ec = ec

    async def extract(self, source: AbstractExtractionSource) -> AbstractExtraction:
        """
        Extract the contents from a file. Error handling of the extractor should be done here.
        """
        ex = self._create_extraction(source)
        try:
            # NOTE: Tika does not handle unfinished files gracefully (may depend on file-type).
            async with source.open() as payload:
                await self._extract_content(payload, ex)
        except SendEmptyFileError:
            pass  # empty file is not sent because there's nothing to extract
        except (IgnoredMimeType, EnoughContent):
            pass  # intentional interrupts, return the extraction
        except aiohttp.ClientResponseError as e:
            try:
                await self.ensure_operable()
            except Exception as e2:
                raise e2 from e  # chain the exceptions for clarity
            if e.status >= 500:
                # This could be a bad
                raise ServerInternalExtractError(ex) from e
            if not self._is_status_successful(source, ex, e.status):
                # Other error codes in the 4xx range are likely related to the HTTP API server, they may be temporary?
                raise RetryExtractError(ex) from e
        except aiohttp.ClientError as e:  # Something infrastructural: payload parsing or connection error most likely.
            try:
                await self.ensure_operable()
            except Exception as e2:
                raise e2 from e  # chain the exceptions for clarity
            # These issues should be temporary.
            raise RetryExtractError(ex) from e
        except asyncio.TimeoutError as e:
            try:
                await self.ensure_operable()
            except Exception as e2:
                raise e2 from e  # chain the exceptions for clarity
            # The extractor should not get stuck indefinitely, so the machine may be overwhelmed.
            raise RetryExtractError(ex)
        return ex

    def _create_extraction(self, source: AbstractExtractionSource) -> AbstractExtraction:
        raise NotImplementedError()

    def _is_status_successful(self, source: AbstractExtractionSource, ex: AbstractExtraction, status: int) -> bool:
        """
        Some 4xx statuses may be considered successful for some content files.
        """
        return False

    async def ensure_operable(self, timeout: float = 30.0):
        """
        Check if extractor is operable, raise fatal error if it won't work.
        """
        try:
            t = time.perf_counter() + timeout
            while True:
                attempt_start = time.perf_counter()
                attempt_timeout = max(0.5, min(t - attempt_start, 5.0))
                if await self.check_operable(timeout=attempt_timeout):
                    break
                if time.perf_counter() > t:
                    raise ExtractorInoperable()
                # simple backoff from retrying
                await asyncio.sleep(max(0.5, attempt_timeout - (time.perf_counter() - attempt_start)))
        except ExtractError as e:
            raise ExtractorInoperable() from e

    async def check_operable(self, timeout: float = 5.0) -> bool:
        """
        Can be used to check if the extractor seems to be working.
        """
        raise NotImplementedError()

    async def _extract_content(self, data, ex: AbstractExtraction, timeout: float = 60.0):
        """
        This should implement the submission of the data to the extractor worker. The result should be collected
        into the extraction object.
        """
        raise NotImplementedError()
