"""
Custom wrapper for sqlalchemy
"""

import os
import importlib
from alembic import command
from alembic.config import Config
from alembic.util.exc import AutogenerateDiffsDetected
from sqlalchemy import create_engine, text, select
from sqlalchemy.orm import Session
from sqlalchemy.exc import NoResultFound
from loguru import logger
from .search_filters import SearchFilters

class SQLAdministrator:
    """
    sqlalchemy wrapper containing functionality for managing database connections
    and performing CRUD operations
    """

    def __init__(self):
        self.__connection = None
        self.__session = None

        self.__host = os.environ["DB_HOSTNAME"]
        self.__port = os.environ["DB_PORT"]
        self.__username = os.environ["DB_USER"]
        self.__password = os.environ["DB_PASSWORD"]
        self.__database = os.environ["DB_NAME"]

        self.__dsn = (
            f"mysql://{self.__username}:{self.__password}"
            f"@{self.__host}:{self.__port}/{self.__database}"
        )
        self.__engine = create_engine(self.__dsn)

        self.__alembic_root = os.environ["ALEMBIC_ROOT"]
        self.__table_metadata = getattr(importlib.import_module("model"), "Base").metadata

    def connect(self):
        """
        Establish a new database connection using the configured DSN if one has not
        already been established.
        """
        if not self.__connection:
            self.__connection = self.__engine.connect()
            self.__session = Session(self.__engine)

        return self.__connection

    def close(self):
        """
        Terminate the active database connection if one exists
        """
        if self.__connection:
            self.__session.close()
            self.__connection.close()

            self.__session = None
            self.__connection = None

    def commit(self):
        """
        Commit chznges made during the active session, if one exists
        """
        if self.__session:
            self.__session.commit()

    def rollback(self):
        """
        Rollback chznges made during the active session, if one exists
        """
        if self.__session:
            self.__session.rollback()

    def execute(self, query):
        """
        Execute an arbitrary SQL command over the open connection
        """
        if self.__connection:
            result = self.__connection.execute(text(query))
            return result.mappings().all()

        return None

    def create(self, resource):
        """
        Persist the specified resource to the database
        """
        if self.__session:
            self.__session.add(resource)
            self.__session.commit()

            return True

        return None

    def search(self, model, filters: SearchFilters):
        """
        Return a list of resources matching the specified search filters
        """
        query = select(model)
        for criteria in filters.criteria:
            if criteria["comparison"] == "==":
                query = query.where(criteria["field"] == criteria["value"])
            elif criteria["comparison"] == "!=":
                query = query.where(criteria["field"] != criteria["value"])
            elif criteria["comparison"] == "<":
                query = query.where(criteria["field"] < criteria["value"])
            elif criteria["comparison"] == "<=":
                query = query.where(criteria["field"] <= criteria["value"])
            elif criteria["comparison"] == ">":
                query = query.where(criteria["field"] > criteria["value"])
            elif criteria["comparison"] == ">=":
                query = query.where(criteria["field"] >= criteria["value"])

        resources = []
        for resource in self.__session.scalars(query):
            resources.append(resource)

        return resources

    def retrieve(self, model, id_number):
        """
        Retrieve the instance of the specified model identified by the provided id
        """
        try:
            resource = self.__session.scalars(
                select(model).where(model.id == id_number)
            ).one()

            return resource
        except NoResultFound:
            return None

    def delete(self, model, id_number):
        """
        Remove the instance of the specified model identified by the provided id
        from the database
        """
        resource = self.retrieve(model, id_number)
        if resource:
            self.__session.delete(resource)

        return resource

    def migrate(self):
        """
        Generate and apply a new database revision if the project's model has changed
        """

        # Initialise alembic configuration
        alembic_config = Config(os.environ["ALEMBIC_INI"])
        alembic_config.set_main_option("script_location", str(self.__alembic_root))
        alembic_config.set_main_option("sqlalchemy.url", self.__dsn)

        # Load data definitions
        alembic_config.attributes["target_metadata"] = self.__table_metadata

        # Generate a new revision
        try:
            command.check(alembic_config)
        except AutogenerateDiffsDetected as e:
            logger.info(f"Model change detected: {e}. Proceeding with migration.")
            command.revision(alembic_config, autogenerate=True)
            command.upgrade(alembic_config, "head")
