from __future__ import annotations

import re

from relationalai.client.errors.exceptions import (
    ClientError,
    ClientInvalidRequestError,
    ClientNotFoundError,
    ClientNotReadyError,
    ClientServerError,
    ClientTimeoutError,
    ClientUnknownError,
)
from relationalai.client.errors.handlers import (
    ErrorHandler,
    ErrorHandlerChain as ClientErrorHandlerChain,
    ExecContext,
    collect_error_messages,
)


_DEFAULT_JOB_NOT_READY_DETAIL = "engine is not ready to accept jobs"
_JOB_NOT_READY_SUBSTRING = "not ready to accept jobs"
_JOB_NOT_READY_MESSAGES = frozenset(
    {
        "engine is not ready to accept jobs",
        "reasoner is not ready to accept jobs",
        "worker is not ready to accept jobs",
    }
)


def _normalize_job_error_text(value: object) -> str:
    return " ".join(str(value).split()).lower() if isinstance(value, str) else ""


def is_jobs_not_ready_message(value: object) -> bool:
    """Shared matcher for transient backend "not ready to accept jobs" text."""
    text = _normalize_job_error_text(value)
    return bool(text) and (text in _JOB_NOT_READY_MESSAGES or _JOB_NOT_READY_SUBSTRING in text)


class JobError(ClientError):
    """Base error for jobs operations."""


class JobInvalidRequestError(ClientInvalidRequestError, JobError):
    """Raised when a Jobs API request is invalid."""


class JobCapabilityError(ClientInvalidRequestError, JobError):
    """Raised when an operation is not supported by the configured backend/capabilities."""


class JobConfigurationError(ClientInvalidRequestError, JobError):
    """Raised when the client/configuration is missing required setup for an operation."""


class JobNotFoundError(ClientNotFoundError, JobError):
    """Raised when a job is not found."""


class JobNotReadyError(ClientNotReadyError, JobError):
    """Raised when a job result/operation is not ready yet."""

    def __init__(
        self,
        message: str | None = None,
        *,
        operation: str | None = None,
        details: str | None = None,
    ):
        self.operation = operation
        self.details = details

        # Preserve legacy call sites that pass a single preformatted message.
        if operation is None and details is None and message is not None:
            super().__init__(message)
            return

        lines: list[str] = ["Job request could not be completed yet."]
        if operation:
            lines.append(f"operation: {operation}")
        lines.append("")
        if details or message:
            lines.append("Details:")
            lines.append(details or message or "")
            lines.append("")
        lines.append("What to do:")
        lines.append("- Wait a bit and retry the command.")
        lines.append("- If you recently created or resumed the reasoner, give it time to become healthy.")
        lines.append("- Check the reasoner status before retrying if the issue persists.")

        super().__init__("\n".join(lines))


class JobTimeoutError(ClientTimeoutError, JobError):
    """Raised when waiting for a job exceeds the deadline."""


class JobServerError(ClientServerError, JobError):
    """Raised when the backend fails processing a job request."""


class JobClientDisconnectedError(JobServerError):
    """Raised when a LOGIC transaction is aborted because the client disconnected."""

    def __init__(self) -> None:
        super().__init__(
            "\n".join(
                [
                    "The client disconnected before the operation could complete.",
                    "",
                    "This may occur due to network issues or if the client application was terminated unexpectedly.",
                    "Please ensure a stable connection and try again.",
                ]
            )
        )


class JobUnknownError(ClientUnknownError, JobError):
    """Raised for unexpected job failures."""


class JobNotReadyErrorHandler:
    """Map generic backend exceptions into JobNotReadyError.

    The jobs DA gateways do an earlier structured-payload check on raw HTTP
    responses. This handler exists for later-stage exceptions where the backend
    message has already been wrapped by lower layers.
    """

    _MESSAGE_RE = re.compile(r'"message"\s*:\s*"([^"]+)"', re.IGNORECASE)

    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:  # noqa: ARG002
        messages = [" ".join(m.split()).lower() for m in collect_error_messages(error)]
        return any(is_jobs_not_ready_message(m) for m in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> None:
        raw = str(error) or repr(error)
        match = self._MESSAGE_RE.search(raw)
        details = match.group(1).strip() if match else _DEFAULT_JOB_NOT_READY_DETAIL
        raise JobNotReadyError(operation=ctx.operation, details=details)


class ErrorHandlerChain(ClientErrorHandlerChain):
    """Jobs error handler chain.

    Extends the base client error handler chain with jobs-specific mappings.
    """

    def __init__(self, handlers: list[ErrorHandler] | None = None):
        if handlers is not None:
            super().__init__(handlers)
            return

        base = ClientErrorHandlerChain()
        super().__init__(base.handlers + [JobNotReadyErrorHandler()])


__all__ = [
    "JobError",
    "JobInvalidRequestError",
    "JobCapabilityError",
    "JobConfigurationError",
    "JobClientDisconnectedError",
    "JobNotFoundError",
    "JobNotReadyError",
    "JobTimeoutError",
    "JobServerError",
    "JobUnknownError",
    "ErrorHandlerChain",
    "is_jobs_not_ready_message",
]

