"""Reasoners error types and error mapping.

The reasoners service builds on the shared client error hierarchy, but exposes
service-specific exception types so callers can catch reasoner failures precisely
(e.g. `ReasonerNotFoundError`).
"""

from __future__ import annotations

import re
from typing import Any

from relationalai.client.errors.exceptions import (
    AbortedTransactionError,
    ClientAuthError,
    ClientConflictError,
    ClientError,
    ClientInvalidRequestError,
    ClientNotFoundError,
    ClientNotReadyError,
    ClientServerError,
    ClientTimeoutError,
    ClientUnknownError,
    DuoSecurityFailed,
    SnowflakeAppMissingException,
    SnowflakeConnectionFailed,
    SnowflakeDatabaseException,
    SnowflakeRaiAppNotStarted,
)
from relationalai.client.errors.handlers import (
    ErrorHandler,
    ErrorHandlerChain as ClientErrorHandlerChain,
    ExecContext,
    collect_error_messages,
)

from . import constants as c


class ReasonerError(ClientError):
    """Base error for reasoners operations."""


class ReasonerAuthError(ClientAuthError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerNotFoundError(ClientNotFoundError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerNotReadyError(ClientNotReadyError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerConflictError(ClientConflictError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerInvalidRequestError(ClientInvalidRequestError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerValidationException(ReasonerInvalidRequestError):
    """Invalid parameters for a reasoner operation (bad request / validation error)."""

    def __init__(
        self,
        *,
        operation: str | None = None,
        details: str | None = None,
        fields: dict[str, str] | None = None,
        cause: Exception | None = None,
    ):
        self.operation = operation
        self.details = details
        self.fields = fields or {}
        self.cause = cause

        lines: list[str] = ["Reasoner request failed validation (bad request)."]
        if operation:
            lines.append(f"operation: {operation}")
        lines.append("")
        if self.fields:
            lines.append("Invalid fields:")
            for k in sorted(self.fields.keys()):
                lines.append(f"- {k}: {self.fields[k]}")
            lines.append("")
        if details:
            lines.append("Details:")
            lines.append(details)
            lines.append("")
        lines.append("Common fixes:")
        lines.append("- Ensure required parameters are provided (e.g. size, type).")
        lines.append("- Use client.sizes() to pick a valid size.")
        lines.append("- Check spelling/casing of type (Logic/Prescriptive/Predictive).")
        if cause:
            lines.append("")
            lines.append("Original error:")
            lines.append(str(cause))

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


class ReasonerTimeoutError(ClientTimeoutError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerServerError(ClientServerError, ReasonerError):
    pass  # noqa: PIE790


class ReasonerHttpError(ReasonerServerError):
    """Direct Access HTTP error with status code (useful for retries/classification)."""

    def __init__(self, *, status: int, message: str, operation: str | None = None):
        self.status = int(status)
        self.operation = operation
        self.message = message
        op = f" op={operation}" if operation else ""
        super().__init__(f"Direct Access request failed (status={self.status}).{op} {message}")


class ReasonerUnknownError(ClientUnknownError, ReasonerError):
    pass  # noqa: PIE790


# -----------------------------------------------------------------------------
# Reasoners-specific error mapping for SQL backend
# -----------------------------------------------------------------------------


def is_reasoner_resource_issue(response_message: str) -> bool:
    return any(kw in response_message.lower() for kw in c.REASONER_RESOURCE_ERRORS + c.REASONER_RESOURCE_NOT_READY_MSGS)


def _ctx_reasoner_name(ctx: ExecContext) -> str:
    if isinstance(ctx.params, list):
        if len(ctx.params) >= 2 and isinstance(ctx.params[1], str):
            return ctx.params[1]
        if len(ctx.params) >= 1 and isinstance(ctx.params[0], str):
            return ctx.params[0]
    return "<unknown>"


class ReasonerInProgressConflictHandler:
    """Maps backend "engine ... in progress" errors to a stable conflict error.

    This is used by ensure()-style workflows to treat concurrent create/resume as
    a benign conflict (another caller is already doing the work) and switch to polling.
    """

    # Normalize whitespace before matching; keep the regex permissive to tolerate
    # backend message wording changes (e.g. "engine create is in progress").
    _RE = re.compile(r"\bengine\b.*\bin\s+progress\b", re.IGNORECASE)

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

    def handle(self, error: Exception, ctx: ExecContext) -> Any:  # noqa: ARG002
        name = _ctx_reasoner_name(ctx)
        raise ReasonerConflictError(f"Reasoner '{name}' operation already in progress.")


class ReasonerValidationErrorHandler:
    """Maps RelationalAI service validation errors (HTTP 400 / RAIERR:1000) into ReasonerValidationException."""

    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        messages = collect_error_messages(error)
        return any(("validation error" in m and "raierr:1000" in m) or "remote service error: 400" in m for m in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        messages = collect_error_messages(error)

        # Snowflake external function errors often surface as "remote service error: 400"
        # even when the underlying semantic is "Not Found" (e.g. engine does not exist).
        #
        # Example:
        # - Not Found - engine not found (RAIERR:1000)
        # - 400 '{"data":[[0,{"status":"Not Found","message":"engine not found"}]]}'
        #
        # Map these to a stable `ReasonerNotFoundError` so callers (and CLIs) get a clear
        # "reasoner does not exist" outcome instead of a validation/bad-request error.
        name = _ctx_reasoner_name(ctx)
        for m in messages:
            ml = m.lower()
            if "engine not found" in ml or '"status":"not found"' in ml or "not found - engine not found" in ml:
                raise ReasonerNotFoundError(f"Reasoner '{name}' not found.")

        # Try to extract the JSON-ish payload embedded in Snowflake external function errors.
        # Example fragment:
        # 400 '{"data":[[0,{"status":"Bad Request","message":"validation error","details":"\"size\" field is required"}]]}'
        fields: dict[str, str] = {}
        details: str | None = None

        for m in messages:
            # details:"\"size\" field is required"
            m_details = re.search(r'"details"\s*:\s*"([^"]+)"', m, re.IGNORECASE)
            if m_details:
                details = m_details.group(1)
                break
            # or: details":"\"size\" field is required"
            m_details2 = re.search(r'details"\s*:\s*"([^"]+)"', m, re.IGNORECASE)
            if m_details2:
                details = m_details2.group(1)
                break

        # Best-effort: parse patterns like "\"size\" field is required" / "\"type\" is invalid"
        if details:
            d = details.replace("\\\"", '"')
            m_req = re.search(r'"([^"]+)"\s+field\s+is\s+required', d, re.IGNORECASE)
            if m_req:
                fields[m_req.group(1)] = "field is required"
            m_inv = re.search(r'"([^"]+)"\s+is\s+invalid', d, re.IGNORECASE)
            if m_inv:
                fields[m_inv.group(1)] = "is invalid"

        raise ReasonerValidationException(operation=ctx.operation, details=details, fields=fields, cause=error)


class ReasonerResourceErrorsHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        messages = collect_error_messages(error)
        return any(is_reasoner_resource_issue(msg) for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        messages = collect_error_messages(error)
        name = _ctx_reasoner_name(ctx)

        for msg in messages:
            if "engine is in pending" in msg or "engine is provisioning" in msg:
                raise ReasonerNotReadyError(f"Reasoner '{name}' is in a pending/provisioning state.")
            if "engine not found" in msg or "no engines found" in msg:
                raise ReasonerNotFoundError(f"Reasoner '{name}' not found. {error}")
            if "engine was deleted" in msg:
                raise ReasonerNotFoundError(f"Reasoner '{name}' was deleted.")
            if "engine is suspended" in msg:
                raise ReasonerServerError(
                    "\n".join(
                        [
                            f"Reasoner '{name}' is suspended.",
                            "Resume it first (or use ensure(..., resume=True)).",
                        ]
                    )
                )
            if "create/resume" in msg:
                raise ReasonerServerError(f"Reasoner provisioning failed for '{name}'. {error}")

        raise ReasonerServerError(str(error))


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

    Extends the base client error handler chain with reasoners-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
            + [
                ReasonerInProgressConflictHandler(),
                ReasonerValidationErrorHandler(),
                ReasonerResourceErrorsHandler(),
            ]
        )


__all__ = [
    # Reasoners errors
    "ReasonerError",
    "ReasonerAuthError",
    "ReasonerNotFoundError",
    "ReasonerNotReadyError",
    "ReasonerConflictError",
    "ReasonerInvalidRequestError",
    "ReasonerValidationException",
    "ReasonerTimeoutError",
    "ReasonerServerError",
    "ReasonerHttpError",
    "ReasonerUnknownError",
    # Reasoners error mapping
    "ErrorHandlerChain",
    # Platform / operational errors (re-exported)
    "DuoSecurityFailed",
    "SnowflakeDatabaseException",
    "SnowflakeConnectionFailed",
    "SnowflakeAppMissingException",
    "SnowflakeRaiAppNotStarted",
    "AbortedTransactionError",
    # Platform bases (sometimes useful for catching)
    "ClientError",
    "ClientAuthError",
    "ClientNotFoundError",
    "ClientNotReadyError",
    "ClientConflictError",
    "ClientInvalidRequestError",
    "ClientTimeoutError",
    "ClientServerError",
    "ClientUnknownError",
]

