"""Project-wide exception classes."""


class ResearchTerminatedException(BaseException):
    """Raised when a user cancels an in-progress research process.

    Inherits from BaseException (not Exception) so that ``except Exception``
    blocks throughout the strategy code naturally let it propagate -- the same
    pattern Python's stdlib uses for asyncio.CancelledError (since 3.9),
    KeyboardInterrupt, and SystemExit.
    """

    pass


class SystemAtCapacityError(Exception):
    """Raised when ``start_research_process`` cannot acquire the global
    concurrency semaphore.

    Previously the semaphore was acquired *inside* the worker thread, so a
    full system would silently park the thread after the HTTP route had
    already returned 200 — the user saw a thinking spinner that never
    advanced, and the partial unique in-progress index blocked retries on
    the same chat session.

    Acquiring synchronously in the caller and surfacing this exception lets
    routes return HTTP 429 (or queue/retry, depending on caller) before any
    ``ResearchHistory`` row is committed.
    """

    pass


class DuplicateResearchError(Exception):
    """Raised when a research should not be (re-)spawned.

    Two triggering cases, both handled identically by callers:

    1. A live thread already exists in the active-research dict for this
       ``research_id`` — typically a retry after a prior attempt's
       post-spawn ``UserActiveResearch`` commit failed.
    2. The ``ResearchHistory.status`` is non-QUEUED (``IN_PROGRESS`` from
       a prior attempt's pre-spawn commit that succeeded; terminal
       ``COMPLETED`` / ``FAILED`` / ``SUSPENDED`` from a thread that
       already finished and cleaned itself out of ``_active_research``).
       Re-spawning would either contradict the live thread or re-run a
       finished research.

    Callers that wrap the spawn in ``except Exception`` to clean up orphan
    state on spawn failure MUST catch ``DuplicateResearchError`` separately
    *before* that generic branch and re-raise / return without mutating the
    research's status or deleting the ``UserActiveResearch`` row — those
    rows belong to the live thread, and marking them FAILED terminates a
    running thread from the user's perspective while it keeps executing.
    """

    pass
