"""error hierarchy for the 3tears-nats wrapper.

every public method on :class:`threetears.nats.NatsClient` raises one
of these typed exceptions on failure rather than leaking
``nats-py``-specific exception types to callers. this keeps consumer
code decoupled from the underlying client library and makes it
possible to rev ``nats-py`` without breaking every catch site.
"""

from __future__ import annotations

__all__ = [
    "KvError",
    "NamespaceNotConfiguredError",
    "NatsClientError",
    "NoRespondersError",
    "OpLogError",
    "OpLogSequenceConflict",
    "PublishError",
    "RequestError",
    "RequestTimeoutError",
    "StreamSubjectsOverlapError",
    "SubscribeError",
]


class NatsClientError(Exception):
    """base class for every 3tears-nats wrapper error.

    callers wanting a single ``except`` clause that catches every
    wrapper-raised condition should catch this base.
    """


class NamespaceNotConfiguredError(NatsClientError):
    """raised when a subject namespace is required but none is configured.

    the subject namespace prefix has no default. configure it explicitly
    by calling :func:`threetears.nats.set_default_namespace`, by
    connecting a :class:`threetears.nats.NatsClient` (which binds its
    configured ``nats_subject_namespace``), or by setting the
    ``THREETEARS_NATS_SUBJECT_NAMESPACE`` environment variable. requiring
    an explicit value prevents two unconfigured deployments from silently
    colliding on the same subjects when they share a NATS cluster.
    """


class StreamSubjectsOverlapError(NatsClientError):
    """raised when a JetStream stream cannot be created because its subjects
    are already claimed by a DIFFERENT stream on the same NATS account.

    a subject belongs to exactly one stream. this is NOT the "stream already
    exists under the same name" case (which is reconciled by updating the
    existing stream's subject set): the requested stream name was never
    created, so updating it would raise a confusing ``NotFoundError`` and mask
    this actionable error. the usual cause is another connection publishing
    over these subjects under the WRONG subject namespace -- e.g. a client that
    did not pass its own ``nats_subject_namespace`` and fell back to a shared
    default, minting a stream that squats the subject space. resolve the
    conflicting stream or correct the namespace. ``__cause__`` carries the
    underlying nats-py exception for diagnostics.
    """


class PublishError(NatsClientError):
    """raised when a publish operation fails.

    wraps any underlying nats-py exception. ``__cause__`` carries
    original exception for diagnostics.
    """


class SubscribeError(NatsClientError):
    """raised when a subscription operation fails to register.

    not raised for callback-side exceptions during message processing
    (those are routed to deadletter or re-raised by the dispatcher).
    """


class RequestError(NatsClientError):
    """raised when a request/reply round-trip fails.

    covers timeout, no-responders, transport failure, and response
    decode failure. the two failures a caller most often needs to tell
    apart -- nobody is subscribed, versus somebody is but did not answer
    in time -- have their own subclasses below. every one of them still
    IS a ``RequestError``, so an existing ``except RequestError`` keeps
    catching all of them unchanged.
    """


class NoRespondersError(RequestError):
    """raised when a request reaches NATS but nothing is subscribed to the subject.

    distinct from :class:`RequestTimeoutError`, and the distinction is
    operationally load-bearing: "nobody is listening" points at a service
    that never started, was never deployed, or is subscribed on a different
    subject/namespace, while a timeout points at one that IS there and is
    slow or wedged. those send an operator to different places.

    the caller that motivated splitting these apart is an agent registering
    with the agent router at boot: on a cold rollout the router may not have
    subscribed yet, which is an ordinary startup race worth waiting out,
    where a transport failure is not. collapsing both into ``RequestError``
    left "wait and retry" indistinguishable from "give up" except by
    matching on message text.
    """


class RequestTimeoutError(RequestError):
    """raised when a request is delivered but no reply arrives within the timeout.

    the responder exists (contrast :class:`NoRespondersError`) and either did
    not answer, answered too slowly, or answered to a reply subject that never
    got back. retrying is often right; retrying forever is not.
    """


class KvError(NatsClientError):
    """raised when a JetStream KV bucket operation fails.

    covers bucket creation, get/put/delete/create/update, and CAS
    conflicts. CAS-specific outcomes (revision-mismatch on update)
    surface as a return-value of ``None`` from
    :meth:`threetears.nats.NatsKvBucket.update`; this exception is
    reserved for transport / bucket-existence failures.
    """


class OpLogError(NatsClientError):
    """raised when a JetStream op-log (WAL) operation fails.

    covers stream creation/binding, append transport failures, and
    replay failures on :class:`threetears.nats.OpLog`. the
    expected-last-sequence CAS conflict (fence #1) is *not* a generic
    op-log failure -- it surfaces as the dedicated
    :class:`OpLogSequenceConflict` subclass so callers can catch the
    fence specifically. this base is reserved for transport /
    stream-existence failures.
    """


class OpLogSequenceConflict(OpLogError):
    """raised when an op-log append fails the expected-last-sequence CAS.

    this is fence #1 of the durable write path: an appender whose
    ``expected_last_seq`` no longer matches the stream's current last
    sequence is rejected **in-band** (a stale / split-brain second
    writer). it is distinct from a duplicate-``op_id`` retry, which is
    an at-most-once no-op and returns
    :class:`threetears.nats.AppendResult` with ``deduplicated=True``
    rather than raising.
    """
