"""
Transaction control through context managers.

We use the savepoint API because it's the most flexible.
We don't use standard ``BEGIN..COMMIT/ROLLBACK`` because
managing the interactions between that and savepoints would be annoying.

Basic Usage
===========

At its simplest, it works the same as how the :mod:`stdlib module <sqlite3>`
uses connections for transactions, but instead with a separate object.

For the following examples, let's use a simple setup:

.. literalinclude:: /../examples/savepoints_basic.py
    :start-after: # region: setup
    :end-before: # endregion setup

Using a :class:`~coolqlite.transactions.Savepoint` context manager
from :meth:`~coolqlite.Connection.with_savepoint`
will "commit" the data if it exits without error.

.. literalinclude:: /../examples/savepoints_basic.py
    :start-after: # region: success
    :end-before: # endregion success

If there's an exception, the transaction will
be rolled back.

.. literalinclude:: /../examples/savepoints_basic.py
    :start-after: # region: errors
    :end-before: # endregion errors
"""

from enum import Enum
import typing
import sqlite3 as sql3
from typing import TYPE_CHECKING, Any, Literal, Self

from coolqlite._common import _DONT_INIT_YOURSELF_OR_THINGS_WILL_BREAK, CoolqliteError


if TYPE_CHECKING:
    from coolqlite._connection import Connection


class _SavepointStatus(Enum):
    JUST_BORN = "just born"
    ACTIVE = "active"
    DEAD = "dead"


class IncorrectTransactionControlError(CoolqliteError):
    """Something incorrect happened when using savepoints.
    This should only happen if using the savepoint context manager
    incorrectly, or otherwise doing something that would break
    a savepoint correctness guarantee.
    """

    pass


class Savepoint:
    def __init__(
        self,
        private,
        id: str,
        connec: Connection[Any],
        parent: Savepoint | None,
    ):
        assert private is _DONT_INIT_YOURSELF_OR_THINGS_WILL_BREAK
        self._id = id
        self._status: _SavepointStatus = _SavepointStatus.JUST_BORN
        self._connec = connec
        self._parent = parent

    def __enter__(self) -> Self:
        match self._status:
            case _SavepointStatus.JUST_BORN:
                self._status = _SavepointStatus.ACTIVE
                return self
            case _SavepointStatus.ACTIVE:
                raise IncorrectTransactionControlError(
                    "tried to enter the same savepoint more than once"
                )
            case _SavepointStatus.DEAD:
                raise IncorrectTransactionControlError(
                    "tried to enter an already-used savepoint"
                )

    # TODO: with this simplified one, when
    # do we actually need to track liveliness?

    def __exit__(self, _exc_type, exc_val: BaseException | None, _tb) -> Literal[False]:
        # I guess we also might want an exception for
        # "I know we're invalidated just get me out of here".
        dead = self._status is _SavepointStatus.DEAD
        match (dead, exc_val):
            case (True, _):
                return False
            case (False, None):
                self._do_release()
                return False
            case (False, sql3.IntegrityError()) if (
                not self._connec._connec.in_transaction
            ):
                # this was a ON CONFLICT ROLLBACK,
                # so all savepoints are now invalidated.
                self._connec._invalidate_all_savepoints_due_to_on_conflict_rollback()
                return False
            case (False, _):
                # either a third-party exception
                # or a TransactionControl that wasn't for us.
                # in either case, rollback-to-before
                # but keep it going.
                self._do_rollback_before()
                return False
            case unreachable:  # pragma: no cover
                typing.assert_never(unreachable)

    def _do_release(self):
        self._connec._release(self._id)
        self._status = _SavepointStatus.DEAD

    def _do_rollback_before(self):
        self._connec._rollback_to_before(self._id)
        self._status = _SavepointStatus.DEAD
