"""
Custom wrapper for sqlalchemy
"""

import os
import time
import importlib
from alembic import command
from alembic.config import Config
from alembic.util.exc import AutogenerateDiffsDetected, CommandError
from sqlalchemy import create_engine, text, select, func
from sqlalchemy.orm import Session, RelationshipProperty
from sqlalchemy.exc import NoResultFound, IntegrityError
from loguru import logger
from .resource import Resource
from .search_filters import SearchFilters

class SQLAdapter:
    """
    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"]

        # Import all models to retrieve table metadata
        importlib.import_module(os.environ["MODEL_MODULE"])
        self.__table_metadata = Resource.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:
            try:
                with self.__session.begin_nested():
                    self.__session.add(resource)
                    self.__session.flush()
            except IntegrityError as e:
                logger.info(
                    f"Creating the requested {type(resource).__name__} would violate "
                    f"uniqueness constraints. Operation aborted."
                )

                from ticketing_service_core.events import DuplicateResourceException
                raise DuplicateResourceException(
                    f"Creating the requested {type(resource).__name__} would violate "
                    f"uniqueness constraints."
                ) from e

            return True

        return None

    def search(
        self,
        model,
        filters: SearchFilters,
        sort_field: str = "id",
        sort_order: str = "asc",
        page_size: int = 0,
        page_number: int = 0
    ):
        """
        Return a list of resources matching the specified search filters
        """
        statement = select(model)
        for criteria in filters.criteria:
            field = getattr(model, criteria["field"])
            is_relationship = isinstance(field.property, RelationshipProperty)

            attribute = field
            if is_relationship:
                attribute = getattr(field.property.mapper.class_, "id")

            if criteria["comparison"] == "==":
                comparison = attribute == criteria["value"]
            elif criteria["comparison"] == "!=":
                comparison = attribute != criteria["value"]
            elif criteria["comparison"] == "<":
                comparison = attribute < criteria["value"]
            elif criteria["comparison"] == "<=":
                comparison = attribute <= criteria["value"]
            elif criteria["comparison"] == ">":
                comparison = attribute > criteria["value"]
            elif criteria["comparison"] == ">=":
                comparison = attribute >= criteria["value"]
            elif criteria["comparison"] == "~":
                comparison = attribute.in_(criteria["value"])
            else:
                comparison = attribute == criteria["value"]

            if criteria["comparison"] == "orphaned":
                statement = statement.where(~field.has())
            elif is_relationship:
                statement = statement.where(
                    field.any(comparison) if field.property.uselist else field.has(comparison)
                )
            else:
                statement = statement.where(comparison)

        # Handle sorting
        if hasattr(model, sort_field):
            sort_attribute = getattr(model, sort_field)
            statement = statement.order_by(sort_attribute.asc() if sort_order == "asc" else sort_attribute.desc())

        # Handle pagination
        if page_size:
            statement = statement.limit(page_size)

            if page_number:
                statement = statement.offset((page_number-1) * page_size)

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

        # Get total records count
        count_statement = select(func.count()).select_from(model) # pylint: disable=not-callable
        if statement.whereclause is not None:
            count_statement = count_statement.where(statement.whereclause)
        total_count = self.__session.execute(count_statement).scalar()

        return resources, total_count

    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 as e:
            logger.info(f"There is no {model.__name__} with the ID {id_number}.")

            from ticketing_service_core.events import MissingResourceException
            raise MissingResourceException(
                f"There is no {model.__name__} with the ID {id_number}."
            ) from e

    def save(self, resource):
        resource.modified = int(time.time())
        try:
            with self.__session.begin_nested():
                self.__session.flush()
        except IntegrityError as e:
            logger.info(
                f"Creating the requested {type(resource).__name__} would violate "
                f"uniqueness constraints. Operation aborted."
            )

            from ticketing_service_core.events import DuplicateResourceException
            raise DuplicateResourceException(
                f"Creating the requested {type(resource).__name__} would violate "
                f"uniqueness constraints."
            ) from e

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

            with self.__session.begin_nested():
                self.__session.flush()
        except IntegrityError as e:
            logger.info(
                f"Deleting the requested {type(resource).__name__} would violate "
                f"foreign key constraints. Operation aborted."
            )

            from ticketing_service_core.events import IndelibleResourceException
            raise IndelibleResourceException(
                f"Deleting the requested {type(resource).__name__} would violate "
                f"foreign key constraints."
            ) from e

        return resource

    def generate_revision(self):
        """
        Generate 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}. Generating revision.")
            command.revision(alembic_config, autogenerate=True)
        except CommandError:
            logger.info("The database has pending revisions that must be applied first.")

    def upgrade(self, revision: str = "head"):
        """
        Upgrade the database to the specified revision, if it exists
        """

        # 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)

        # Apply the specified revision
        logger.info(f"Upgrading database to revision: {revision}.")
        command.upgrade(alembic_config, revision)
