"""Perform genome database migrations with Alembic.

This package contains all Alembic configuration and data files. Revision files are located in
``./alembic/versions``.

Note on alembic configuration - seems like normal usage of Alembic involves getting the database URL
from ``alembic.ini``. Since this application has no fixed location for the database we can't use
this method. Instead we are following the
`Sharing a Connection with a Series of Migration Commands and Environments <alembic-recipe_>`_
recipe in Alembic's documentation, where the connectable object is generated programmatically
somehow and then attached to the Alembic configuration object's ``attributes`` dict. The
``run_migrations_offline`` and ``run_migrations_online`` functions in ``alembic/env.py`` are
modified from the version generated by ``alembic init`` to get their connectable object from this
dict instead of creating it based on the contents of ``alembic.ini``. Note that this means we
can't do (online) migration stuff from the standard alembic CLI command, which gets its
connection information only from ``alembic.ini``.

The way to use this setup is instead to create an :class:`alembic.config.Config` instance with
:func:`.get_alembic_config` and use the functions in :mod:`alembic.command`.

.. _alembic-recipe: https://alembic.sqlalchemy.org/en/latest/cookbook.html#sharing-a-connection-with-a-series-of-migration-commands-and-environments
"""

from typing import Optional

from alembic.config import Config
from alembic import command
from alembic.migration import MigrationContext
from alembic.script import ScriptDirectory
from pkg_resources import resource_filename
from sqlalchemy.engine import Connectable


INI_PATH = resource_filename(__name__, 'alembic.ini')


def get_alembic_config(connectable: Optional[Connectable] = None, **kwargs) -> Config:
	"""Get an alembic config object to perform migrations.

	Parameters
	----------
	connectable
		SQLAlchemy connectable specifying database connection info (optional). Assigned to
		``'connectable'`` key of :attr:`alembic.config.Config.attributes`.
	\\**kwargs
		Keyword arguments to pass to :meth:`alembic.config.Config.__init__`.

	Returns
	-------
		Alembic config object.
	"""
	config = Config(INI_PATH, **kwargs)
	config.attributes['connectable'] = connectable

	return config


def current_head() -> str:
	"""Get the current head revision number."""
	conf = get_alembic_config()
	scriptdir = ScriptDirectory.from_config(conf)
	return scriptdir.get_current_head()


def current_revision(connectable: Connectable) -> str:
	"""Get the current revision number of a genome database."""
	with connectable.connect() as conn:
		ctx = MigrationContext.configure(conn)
		return ctx.get_current_revision()


def is_current_revision(connectable: Connectable):
	"""Check if the current revision of a genome database is the most recent (head) revision."""
	head = current_head()
	current = current_revision(connectable)
	return current == head


def upgrade(connectable: Connectable, revision: str = 'head', tag=None, **kwargs):
	"""Run the alembic upgrade command.

	See :func:`alembic.command.upgrade` for more information on how this works.

	Parameters
	----------
	connectable
		SQLAlchemy connectable specifying genome database connection info.
	revision
		Revision to upgrade to. Passed to :func:`alembic.command.upgrade`.
	tag
		Passed to :func:`alembic.command.upgrade`.
	\\**kwargs
		Passed to :func:`.get_alembic_config`.
	"""
	config = get_alembic_config(connectable, **kwargs)
	command.upgrade(config, revision, tag=tag)


def init_db(connectable: Connectable):
	"""
	Initialize the genome database schema by creating all tables and stamping with the latest
	Alembic revision.

	Expects a fresh database that does not already contain any tables for the :mod:`gambit.db.models`
	models and has not had any migrations run on it yet.

	Parameters
	----------
	connectable
		SQLAlchemy connectable specifying database connection info.

	Raises
	------
	RuntimeError
		If the database is already stamped with an Alembic revision.
	sqlalchemy.exc.OperationalError
		If any of the database tables to be created already exist.
	"""
	from gambit.db.models import Base

	conf = get_alembic_config()
	script = ScriptDirectory.from_config(conf)

	with connectable.connect() as conn:
		ctx = MigrationContext.configure(conn)

		# Check there is no current revision stamped
		current = ctx.get_current_revision()
		if current is not None:
			raise RuntimeError(f'Expected uninitialized database, but current alembic revision is {current}')

		# Create tables
		# Set checkfirst=false so that we get an SQL error if any tables already exist
		Base.metadata.create_all(conn, checkfirst=False)

		# Stamp latest alembic version
		ctx.stamp(script, 'head')
