"""Stable SB_* error codes and English messages (parity with Node sdk-errors.ts)."""

from __future__ import annotations

from enum import Enum
from typing import Any, Optional, Tuple

import grpc

ServiceBridgeErrorSeverity = str  # "fatal" | "retriable" | "ignorable"


class SB(str, Enum):
    """Stable ServiceBridge SDK error codes (string identifiers)."""

    INVALID_SERVICE_KEY_MISSING_SEGMENT = "SB_INVALID_SERVICE_KEY_MISSING_SEGMENT"
    INVALID_SERVICE_KEY_MALFORMED_SEGMENT = "SB_INVALID_SERVICE_KEY_MALFORMED_SEGMENT"
    INVALID_SERVICE_KEY_MALFORMED_PEM = "SB_INVALID_SERVICE_KEY_MALFORMED_PEM"
    INVALID_SERVICE_KEY_LEGACY = "SB_INVALID_SERVICE_KEY_LEGACY"
    INVALID_SERVICE_KEY_MISSING_KEY_ID = "SB_INVALID_SERVICE_KEY_MISSING_KEY_ID"
    INVALID_SERVICE_KEY_MALFORMED_SECRET = "SB_INVALID_SERVICE_KEY_MALFORMED_SECRET"
    EMPTY_CONTROL_PLANE_CA_OPTION = "SB_EMPTY_CONTROL_PLANE_CA_OPTION"
    INVALID_CONTROL_PLANE_CA = "SB_INVALID_CONTROL_PLANE_CA"
    WORKER_TLS_CA_MALFORMED = "SB_WORKER_TLS_CA_MALFORMED"
    WORKER_TLS_INVALID_PAIR = "SB_WORKER_TLS_INVALID_PAIR"
    WORKER_TLS_INCOMPLETE_PAIR = "SB_WORKER_TLS_INCOMPLETE_PAIR"
    WORKER_ENDPOINT_EMPTY = "SB_WORKER_ENDPOINT_EMPTY"
    WORKER_ENDPOINT_INVALID_SCHEME = "SB_WORKER_ENDPOINT_INVALID_SCHEME"
    ISSUE_CERTIFICATE_EMPTY_PEM = "SB_ISSUE_CERTIFICATE_EMPTY_PEM"
    OFFLINE_QUEUE_FULL = "SB_OFFLINE_QUEUE_FULL"
    RPC_TARGET_REQUIRED = "SB_RPC_TARGET_REQUIRED"
    RPC_TARGET_AMBIGUOUS = "SB_RPC_TARGET_AMBIGUOUS"
    NO_RPC_ENDPOINTS = "SB_NO_RPC_ENDPOINTS"
    RPC_CALLER_NOT_ALLOWED = "SB_RPC_CALLER_NOT_ALLOWED"
    RPC_FN_REQUIRED = "SB_RPC_FN_REQUIRED"
    START_STATE_NOT_INITIALIZED = "SB_START_STATE_NOT_INITIALIZED"
    START_ALREADY_CALLED = "SB_START_ALREADY_CALLED"
    START_INVALID_MAX_INFLIGHT = "SB_START_INVALID_MAX_INFLIGHT"
    WORKER_SERVER_NOT_INITIALIZED = "SB_WORKER_SERVER_NOT_INITIALIZED"
    WORKER_TLS_NOT_PROVISIONED = "SB_WORKER_TLS_NOT_PROVISIONED"
    WORKER_TLS_CA_REQUIRED = "SB_WORKER_TLS_CA_REQUIRED"
    DUPLICATE_EVENT_CONSUMER_GROUP = "SB_DUPLICATE_EVENT_CONSUMER_GROUP"
    FROZEN_AFTER_START = "SB_FROZEN_AFTER_START"
    UNDECLARED_OUTBOUND_RPC = "SB_UNDECLARED_OUTBOUND_RPC"
    RECONCILE_FAILED_PRECONDITION = "SB_RECONCILE_FAILED_PRECONDITION"
    JOB_RPC_ARGS_REQUIRED = "SB_JOB_RPC_ARGS_REQUIRED"
    JOB_VIA_MISMATCH = "SB_JOB_VIA_MISMATCH"
    JOB_TARGET_FORM_REQUIRED = "SB_JOB_TARGET_FORM_REQUIRED"
    EXECUTE_WORKFLOW_ARGS_REQUIRED = "SB_EXECUTE_WORKFLOW_ARGS_REQUIRED"
    WORKFLOW_NOT_CANCELLED = "SB_WORKFLOW_NOT_CANCELLED"
    INTERNAL_INVARIANT_VIOLATION = "SB_INTERNAL_INVARIANT_VIOLATION"
    HANDLER_RETURNED_FAILURE = "SB_HANDLER_RETURNED_FAILURE"
    WATCH_TRACE_INVALID_JSON = "SB_WATCH_TRACE_INVALID_JSON"
    WATCH_TRACE_BACKPRESSURE = "SB_WATCH_TRACE_BACKPRESSURE"
    GRPC_CANCELLED = "SB_GRPC_CANCELLED"
    GRPC_UNKNOWN = "SB_GRPC_UNKNOWN"
    GRPC_INVALID_ARGUMENT = "SB_GRPC_INVALID_ARGUMENT"
    GRPC_DEADLINE_EXCEEDED = "SB_GRPC_DEADLINE_EXCEEDED"
    GRPC_NOT_FOUND = "SB_GRPC_NOT_FOUND"
    GRPC_ALREADY_EXISTS = "SB_GRPC_ALREADY_EXISTS"
    GRPC_PERMISSION_DENIED = "SB_GRPC_PERMISSION_DENIED"
    GRPC_RESOURCE_EXHAUSTED = "SB_GRPC_RESOURCE_EXHAUSTED"
    GRPC_FAILED_PRECONDITION = "SB_GRPC_FAILED_PRECONDITION"
    GRPC_ABORTED = "SB_GRPC_ABORTED"
    GRPC_OUT_OF_RANGE = "SB_GRPC_OUT_OF_RANGE"
    GRPC_UNIMPLEMENTED = "SB_GRPC_UNIMPLEMENTED"
    GRPC_INTERNAL = "SB_GRPC_INTERNAL"
    GRPC_UNAVAILABLE = "SB_GRPC_UNAVAILABLE"
    GRPC_DATA_LOSS = "SB_GRPC_DATA_LOSS"
    GRPC_UNAUTHENTICATED = "SB_GRPC_UNAUTHENTICATED"
    GRPC_UNSPECIFIED = "SB_GRPC_UNSPECIFIED"
    NON_GRPC = "SB_NON_GRPC"


SB_MESSAGES: dict[str, str] = {
    SB.INVALID_SERVICE_KEY_MISSING_SEGMENT.value: (
        "The service key is missing a required segment."
    ),
    SB.INVALID_SERVICE_KEY_MALFORMED_SEGMENT.value: (
        "The service key contains a malformed segment."
    ),
    SB.INVALID_SERVICE_KEY_MALFORMED_PEM.value: (
        "The embedded CA in the service key is not valid PEM."
    ),
    SB.INVALID_SERVICE_KEY_LEGACY.value: (
        "Legacy service keys are not supported; use an sbv2 service key."
    ),
    SB.INVALID_SERVICE_KEY_MISSING_KEY_ID.value: (
        "The service key is missing the key identifier segment."
    ),
    SB.INVALID_SERVICE_KEY_MALFORMED_SECRET.value: (
        "The service key secret segment is too short or malformed."
    ),
    SB.EMPTY_CONTROL_PLANE_CA_OPTION.value: (
        "The explicit control-plane CA option is empty."
    ),
    SB.INVALID_CONTROL_PLANE_CA.value: (
        "The control-plane CA certificate is not valid PEM."
    ),
    SB.WORKER_TLS_CA_MALFORMED.value: "A worker TLS CA certificate is not valid PEM.",
    SB.WORKER_TLS_INVALID_PAIR.value: (
        "The worker TLS certificate and private key do not form a valid pair."
    ),
    SB.WORKER_TLS_INCOMPLETE_PAIR.value: (
        "Worker TLS certificate and private key must both be set, or both omitted."
    ),
    SB.WORKER_ENDPOINT_EMPTY.value: "Worker endpoint address is empty.",
    SB.WORKER_ENDPOINT_INVALID_SCHEME.value: (
        "Worker endpoint must be host:port without a URL scheme."
    ),
    SB.ISSUE_CERTIFICATE_EMPTY_PEM.value: (
        "IssueCertificate returned empty certificate or CA PEM from the runtime."
    ),
    SB.OFFLINE_QUEUE_FULL.value: "The offline operation queue is full.",
    SB.RPC_TARGET_REQUIRED.value: "RPC target function name is required.",
    SB.RPC_TARGET_AMBIGUOUS.value: (
        "The RPC target matches multiple services; use a globally unique function name."
    ),
    SB.NO_RPC_ENDPOINTS.value: "No worker endpoints are available for this RPC.",
    SB.RPC_CALLER_NOT_ALLOWED.value: "This service is not allowed to call the target RPC.",
    SB.RPC_FN_REQUIRED.value: "RPC function name is required.",
    SB.START_STATE_NOT_INITIALIZED.value: (
        "Worker start state is not initialized; call start() first."
    ),
    SB.START_ALREADY_CALLED.value: "start() has already been called on this client.",
    SB.START_INVALID_MAX_INFLIGHT.value: (
        "start() option maxInFlight must be a positive integer."
    ),
    SB.WORKER_SERVER_NOT_INITIALIZED.value: "Worker gRPC server failed to initialize.",
    SB.WORKER_TLS_NOT_PROVISIONED.value: (
        "Worker TLS materials are required but were not provisioned."
    ),
    SB.WORKER_TLS_CA_REQUIRED.value: (
        "A TLS CA certificate is required when starting the worker with TLS."
    ),
    SB.DUPLICATE_EVENT_CONSUMER_GROUP.value: (
        "Duplicate event consumer group for this pattern; use a unique pattern per handler."
    ),
    SB.FROZEN_AFTER_START.value: (
        "This registration cannot be changed after start(); configure handlers and "
        "outbound declarations before start()."
    ),
    SB.UNDECLARED_OUTBOUND_RPC.value: (
        "Outbound RPC was not declared; call callsRpc(fn) before invoking this RPC "
        "when strict outbound declarations are enabled."
    ),
    SB.RECONCILE_FAILED_PRECONDITION.value: (
        "Registration reconcile failed: the runtime rejected the current declaration state "
        "(FailedPrecondition)."
    ),
    SB.JOB_RPC_ARGS_REQUIRED.value: (
        "job(): service and function name are required for RPC jobs."
    ),
    SB.JOB_VIA_MISMATCH.value: (
        "job(): via must be 'rpc' when using the (service, fn, opts) form."
    ),
    SB.JOB_TARGET_FORM_REQUIRED.value: "job(): use job(service, fn, opts) when via is 'rpc'.",
    SB.EXECUTE_WORKFLOW_ARGS_REQUIRED.value: (
        "executeWorkflow(): service name and workflow name are required."
    ),
    SB.WORKFLOW_NOT_CANCELLED.value: "The workflow was not cancelled.",
    SB.INTERNAL_INVARIANT_VIOLATION.value: "Internal SDK invariant was violated.",
    SB.HANDLER_RETURNED_FAILURE.value: "The RPC handler reported a failure.",
    SB.WATCH_TRACE_INVALID_JSON.value: "watchTrace received a chunk that is not valid JSON.",
    SB.WATCH_TRACE_BACKPRESSURE.value: (
        "watchTrace consumer is not draining events fast enough."
    ),
    SB.GRPC_CANCELLED.value: "The gRPC call was cancelled.",
    SB.GRPC_UNKNOWN.value: "The server returned an unknown gRPC error.",
    SB.GRPC_INVALID_ARGUMENT.value: "The request was rejected (invalid argument).",
    SB.GRPC_DEADLINE_EXCEEDED.value: "The gRPC call deadline was exceeded.",
    SB.GRPC_NOT_FOUND.value: "The requested resource was not found.",
    SB.GRPC_ALREADY_EXISTS.value: "The resource already exists.",
    SB.GRPC_PERMISSION_DENIED.value: "Permission was denied for this call.",
    SB.GRPC_RESOURCE_EXHAUSTED.value: "A resource limit was exceeded.",
    SB.GRPC_FAILED_PRECONDITION.value: "The call failed a precondition check.",
    SB.GRPC_ABORTED.value: "The operation was aborted.",
    SB.GRPC_OUT_OF_RANGE.value: "A value was out of range.",
    SB.GRPC_UNIMPLEMENTED.value: "The operation is not implemented on the server.",
    SB.GRPC_INTERNAL.value: "The server reported an internal error.",
    SB.GRPC_UNAVAILABLE.value: "The server was unavailable.",
    SB.GRPC_DATA_LOSS.value: "Unrecoverable data loss or corruption was reported.",
    SB.GRPC_UNAUTHENTICATED.value: "The call was not authenticated.",
    SB.GRPC_UNSPECIFIED.value: "A gRPC error occurred.",
    SB.NON_GRPC.value: "A non-gRPC error occurred.",
}


class ServiceBridgeError(Exception):
    """Typed error raised by ServiceBridge SDK operations (Node ServiceBridgeError parity).

    Attributes:
        sb_code: Stable ``SB_*`` string (Node: ``code``).
        grpc_status: ``grpc.StatusCode`` when the error originated from gRPC, else ``None``.
        component: SDK subsystem.
        operation: Operation that failed.
        severity: ``fatal``, ``retriable``, or ``ignorable``.
        retryable: ``True`` when severity is ``retriable``.
    """

    def __init__(
        self,
        message: str,
        *,
        sb_code: str = "",
        grpc_status: Any = None,
        component: str = "",
        operation: str = "",
        severity: ServiceBridgeErrorSeverity = "fatal",
        cause: Optional[BaseException] = None,
    ) -> None:
        super().__init__(message)
        self.sb_code = sb_code or SB.NON_GRPC.value
        self.grpc_status = grpc_status
        self.component = component
        self.operation = operation
        self.severity = severity
        self.retryable = severity == "retriable"
        if cause is not None:
            self.__cause__ = cause

    @property
    def code(self) -> str:
        """Stable SB_* identifier (same semantics as Node ``ServiceBridgeError.code``)."""
        return self.sb_code

    def __repr__(self) -> str:
        return (
            f"ServiceBridgeError({self!s}, sb_code={self.sb_code!r}, "
            f"component={self.component!r}, operation={self.operation!r}, "
            f"severity={self.severity!r})"
        )


def severity_from_grpc(grpc_code: grpc.StatusCode) -> ServiceBridgeErrorSeverity:
    if grpc_code in (
        grpc.StatusCode.UNAVAILABLE,
        grpc.StatusCode.UNKNOWN,
        grpc.StatusCode.DEADLINE_EXCEEDED,
        grpc.StatusCode.RESOURCE_EXHAUSTED,
    ):
        return "retriable"
    if grpc_code == grpc.StatusCode.CANCELLED:
        return "ignorable"
    return "fatal"


def grpc_code_to_sb(grpc_code: grpc.StatusCode) -> str:
    mapping = {
        grpc.StatusCode.CANCELLED: SB.GRPC_CANCELLED,
        grpc.StatusCode.UNKNOWN: SB.GRPC_UNKNOWN,
        grpc.StatusCode.INVALID_ARGUMENT: SB.GRPC_INVALID_ARGUMENT,
        grpc.StatusCode.DEADLINE_EXCEEDED: SB.GRPC_DEADLINE_EXCEEDED,
        grpc.StatusCode.NOT_FOUND: SB.GRPC_NOT_FOUND,
        grpc.StatusCode.ALREADY_EXISTS: SB.GRPC_ALREADY_EXISTS,
        grpc.StatusCode.PERMISSION_DENIED: SB.GRPC_PERMISSION_DENIED,
        grpc.StatusCode.RESOURCE_EXHAUSTED: SB.GRPC_RESOURCE_EXHAUSTED,
        grpc.StatusCode.FAILED_PRECONDITION: SB.GRPC_FAILED_PRECONDITION,
        grpc.StatusCode.ABORTED: SB.GRPC_ABORTED,
        grpc.StatusCode.OUT_OF_RANGE: SB.GRPC_OUT_OF_RANGE,
        grpc.StatusCode.UNIMPLEMENTED: SB.GRPC_UNIMPLEMENTED,
        grpc.StatusCode.INTERNAL: SB.GRPC_INTERNAL,
        grpc.StatusCode.UNAVAILABLE: SB.GRPC_UNAVAILABLE,
        grpc.StatusCode.DATA_LOSS: SB.GRPC_DATA_LOSS,
        grpc.StatusCode.UNAUTHENTICATED: SB.GRPC_UNAUTHENTICATED,
    }
    return mapping.get(grpc_code, SB.GRPC_UNSPECIFIED).value


def map_grpc_status(
    grpc_code: grpc.StatusCode,
    details: Optional[str],
) -> Tuple[str, str]:
    sb_c = grpc_code_to_sb(grpc_code)
    message = SB_MESSAGES.get(sb_c, SB_MESSAGES[SB.GRPC_UNSPECIFIED.value])
    d = (details or "").strip()
    if not d:
        return sb_c, message
    if grpc_code in (
        grpc.StatusCode.INTERNAL,
        grpc.StatusCode.UNKNOWN,
        grpc.StatusCode.FAILED_PRECONDITION,
    ):
        return sb_c, f"{message} {d}"
    if d not in message:
        return sb_c, f"{message} {d}"
    return sb_c, message


def normalize_service_error(
    err: BaseException,
    operation: str,
    component: str = "sdk",
) -> ServiceBridgeError:
    """Map an arbitrary error to ``ServiceBridgeError`` (Node ``normalizeServiceError``)."""
    if isinstance(err, ServiceBridgeError):
        return err
    if isinstance(err, grpc.RpcError):
        grpc_code = err.code()
        raw = (err.details() or "").strip() or None
        if (
            grpc_code == grpc.StatusCode.FAILED_PRECONDITION
            and operation.startswith("reconcile")
        ):
            base = SB_MESSAGES[SB.RECONCILE_FAILED_PRECONDITION.value]
            msg = f"{base} {raw}" if raw else base
            sev = severity_from_grpc(grpc_code)
            return ServiceBridgeError(
                msg,
                sb_code=SB.RECONCILE_FAILED_PRECONDITION.value,
                grpc_status=grpc_code,
                component=component,
                operation=operation,
                severity=sev,
                cause=err,
            )
        sb_c, msg = map_grpc_status(grpc_code, raw)
        sev = severity_from_grpc(grpc_code)
        return ServiceBridgeError(
            msg,
            sb_code=sb_c,
            grpc_status=grpc_code,
            component=component,
            operation=operation,
            severity=sev,
            cause=err,
        )
    raw_msg = str(err).strip() if err else ""
    fallback = raw_msg or "An unknown error occurred."
    return ServiceBridgeError(
        fallback,
        sb_code=SB.NON_GRPC.value,
        component=component,
        operation=operation,
        severity="fatal",
        cause=err if isinstance(err, BaseException) else None,
    )


def map_reconcile_failed_precondition_message(exc: grpc.RpcError) -> str:
    """English message for Reconcile + FailedPrecondition (same text as Node)."""
    d = (exc.details() or "").strip()
    base = SB_MESSAGES[SB.RECONCILE_FAILED_PRECONDITION.value]
    return f"{base} {d}" if d else base
