from __future__ import annotations

import asyncio
import re
import time
from typing import 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):
    pass


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)

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


class ServerInternalExtractError(RetryExtractError):
    """
    Raised when the extractor process explicitly reported some error internal/processing issue for the request.
    These happens similarly to RetryExtractError, but retrying is not performed.
    """


class StopExtraction(ExtractError):
    """
    Feed-control exception to stop processing prematurely.
    """

    def __init__(self, reason: str):
        ExtractError.__init__(self, reason)

    @property
    def reason(self) -> str:
        return self.args[0]


class ExtractFailed(ExtractError):
    """
    This is used for unexpected issues, like garbage output, parsing errors.
    This may be similar to ServerInternalExtractError, but the extractor process could not report the error well.
    The extraction will handle these with implicit-retry then abandon.
    """


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 StopExtraction:
            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, ExtractFailed) as e:
            # Something infrastructural: payload parsing or connection error most likely. In case the extractor is
            # still operable, there is little chance for the retry to be successful, but we can't find out reliably
            # unless we try.
            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()
