"""Error handlers and classification for transport execution.

The runtime middleware pipeline delegates backend-specific error mapping to
service-agnostic handler chains.

- `ExecContext` captures operation + statement/url + metadata for diagnostics.
- `ErrorHandler` implementations match on error text/context and raise a stable
  `Client*` exception (or a more specific Snowflake operational error).
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any, Protocol

from .. import constants as c
from .exceptions import (
    AbortedTransactionError,
    ClientServerError,
    ClientSqlCompilationError,
    ClientUnknownError,
    DuoSecurityFailed,
    SnowflakeAppMissingException,
    SnowflakeConnectionFailed,
    SnowflakeDatabaseException,
    SnowflakeRaiAppNotStarted,
)


@dataclass(frozen=True)
class ExecContext:
    statement: str
    params: list[Any] | None
    operation: str
    meta: dict[str, Any]

    @property
    def app(self) -> str:
        val = self.meta.get("app")
        return str(val) if isinstance(val, str) else ""

    @property
    def schema(self) -> str:
        val = self.meta.get("schema")
        return str(val) if isinstance(val, str) else ""


class ErrorHandler(Protocol):
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool: ...
    def handle(self, error: Exception, ctx: ExecContext) -> Any: ...


def collect_error_messages(e: Exception) -> list[str]:
    messages = [str(e).lower()]

    if hasattr(e, "message"):
        msg = getattr(e, "message", None)
        if isinstance(msg, str):
            messages.append(msg.lower())

    if hasattr(e, "args") and getattr(e, "args", None):
        for arg in e.args:  # type: ignore[attr-defined]
            if isinstance(arg, str):
                messages.append(arg.lower())

    if getattr(e, "__cause__", None):
        messages.append(str(e.__cause__).lower())
    if getattr(e, "__context__", None):
        messages.append(str(e.__context__).lower())

    for msg in messages[:]:
        if re.search(r"javascript execution error", msg):
            matches = re.findall(r'"message"\s*:\s*"([^"]+)"', msg, re.IGNORECASE)
            messages.extend([m.lower() for m in matches])

    return messages


def is_database_issue(response_message: str) -> bool:
    return any(kw in response_message.lower() for kw in c.DATABASE_ERRORS)


class AppMissingErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        if not ctx.app:
            return False
        pattern = f"database '{ctx.app.lower()}' does not exist or not authorized."
        messages = collect_error_messages(error)
        return any(re.search(pattern, msg) for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        raise SnowflakeAppMissingException(ctx.app)


class AppFunctionMissingErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        if not ctx.app:
            return False
        app_lower = ctx.app.lower()
        messages = [" ".join(msg.split()).lower() for msg in collect_error_messages(error)]
        needle = f"unknown user-defined function {app_lower}.app."
        return any(needle in msg for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        raise SnowflakeAppMissingException(ctx.app)


class AppObjectMissingErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        if not ctx.app:
            return False
        app_lower = ctx.app.lower()
        messages = [" ".join(msg.split()).lower() for msg in collect_error_messages(error)]

        needles = (
            f"unknown user-defined function {app_lower}.",
            f"unknown function {app_lower}.",
            "unknown function",
            "unknown stored procedure",
            "stored procedure does not exist",
        )
        if any(n in msg for msg in messages for n in needles):
            return not any(f"unknown user-defined function {app_lower}.app." in msg for msg in messages)
        return False

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        raise ClientServerError(
            "\n".join(
                [
                    "Request failed because a required app function/stored procedure was not found or is not authorized.",
                    "",
                    "What this usually means:",
                    "- The RelationalAI app is installed, but the expected proc/function name does not exist in this app version, or",
                    "- Your Snowflake role does not have EXECUTE/USAGE privileges for that proc/function/schema.",
                    "",
                    f"operation: {ctx.operation}",
                    f"statement: {ctx.statement.strip()}",
                    "",
                    f"Original error: {error}",
                ]
            )
        )


class ServiceNotStartedErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:
        messages = [" ".join(msg.split()).lower() for msg in collect_error_messages(error)]
        return any(("service has not been started" in msg or "not reachable: service suspended" in msg) for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        raise SnowflakeRaiAppNotStarted(ctx.app)


class DuoSecurityErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:  # noqa: ARG002
        messages = collect_error_messages(error)
        return any("duo security" in msg for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:  # noqa: ARG002
        raise DuoSecurityFailed(error)


class SnowflakeConnectErrorHandler:
    _NEEDLES = (
        "could not connect to snowflake backend",
        "login request is retryable",
        "will be handled by authenticator",
        "handle_timeout",
    )

    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(
            any(needle in m for needle in self._NEEDLES) or re.search(r"\b250001\b", m) or re.search(r"\b251012\b", m)
            for m in messages
        )

    def handle(self, error: Exception, ctx: ExecContext) -> Any:  # noqa: ARG002
        raise SnowflakeConnectionFailed(cause=error)


class DatabaseErrorsHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:  # noqa: ARG002
        messages = collect_error_messages(error)
        return any(is_database_issue(msg) for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:  # noqa: ARG002
        raise SnowflakeDatabaseException(error)


class TransactionAbortedErrorHandler:
    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:  # noqa: ARG002
        messages = collect_error_messages(error)
        return any(re.search(r"state:\s*aborted", msg) for msg in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:  # noqa: ARG002
        messages = collect_error_messages(error)
        type_field: str | None = None
        message_field: str | None = None
        report_field: str | None = None

        for msg in messages:
            m_type = re.search(r'"type"\s*:\s*"([^"]+)"', msg, re.IGNORECASE)
            m_msg = re.search(r'"message"\s*:\s*"([^"]+)"', msg, re.IGNORECASE)
            m_rep = re.search(r'"report"\s*:\s*"([^"]+)"', msg, re.IGNORECASE)
            if m_type and not type_field:
                type_field = m_type.group(1)
            if m_msg and not message_field:
                message_field = m_msg.group(1)
            if m_rep and not report_field:
                report_field = m_rep.group(1)
            if type_field or message_field or report_field:
                break

        raise AbortedTransactionError(type_field, message_field or str(error), report_field)


class SqlCompilationErrorHandler:
    """Classify Snowflake SQL compilation/syntax errors as invalid requests.

    More specific handlers (e.g. missing proc/function in the app DB) should run earlier
    so they can provide better domain-specific guidance.
    """

    def matches(self, error: Exception, message: str, ctx: ExecContext) -> bool:  # noqa: ARG002
        messages = [" ".join(m.split()).lower() for m in collect_error_messages(error)]
        needles = (
            "sql compilation error",
            "syntax error line",
            "unexpected '<eof>'",
        )
        return any(any(n in m for n in needles) for m in messages)

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        raise ClientSqlCompilationError(operation=ctx.operation, statement=ctx.statement, cause=error)


class ErrorHandlerChain:
    def __init__(self, handlers: list[ErrorHandler] | None = None):
        self._handlers: list[ErrorHandler] = handlers or [
            AppMissingErrorHandler(),
            AppFunctionMissingErrorHandler(),
            AppObjectMissingErrorHandler(),
            ServiceNotStartedErrorHandler(),
            DuoSecurityErrorHandler(),
            SnowflakeConnectErrorHandler(),
            SqlCompilationErrorHandler(),
            DatabaseErrorsHandler(),
            TransactionAbortedErrorHandler(),
        ]

    @property
    def handlers(self) -> list[ErrorHandler]:
        return list(self._handlers)

    def with_handlers(self, extra: list[ErrorHandler]) -> "ErrorHandlerChain":
        return ErrorHandlerChain(list(self._handlers) + list(extra))

    def handle(self, error: Exception, ctx: ExecContext) -> Any:
        message = str(error).lower()
        for h in self._handlers:
            if h.matches(error, message, ctx):
                # When mapping a backend error to a stable client exception, suppress
                # exception chaining to keep user-facing output concise.
                try:
                    return h.handle(error, ctx)
                except Exception as mapped:  # noqa: BLE001
                    raise mapped from None
        raise ClientUnknownError(str(error)) from None

