from typing import List

import sqlalchemy

from openlog.datamodel.assay.generic_assay import (
    AssayDatabaseDefinition,
    AssayDefinition,
    GenericAssay,
)


class AssayInterface:
    """
    Interface for assay use.

    Check for interface implementation :

    - :meth:`can_import_assay`
    - :meth:`can_import_assay_data`

    Add assay :

    - :meth:`default_assay_schema`
    - :meth:`add_assay_table`
    - :meth:`import_assay_data`

    Get available assays

    - :meth:`get_all_available_assay_definitions`
    - :meth:`get_assay_table_definition`
    - :meth:`get_assay`

    Import assay data:

    - :meth:`import_assay_data`

    Check assay availability for collar:

    - :meth:`is_assay_available_for_collar`
    - :meth:`is_assay_available_for_collars`

    By default, all functions are not implemented and raises InvalidInterface exception.

    """

    class ImportException(Exception):
        pass

    class ReadException(Exception):
        pass

    class InvalidInterface(Exception):
        pass

    def can_import_assay(self) -> bool:
        """
        Return True if connection can add assay table

        Returns:
            bool: False, must be implemented in interface
        """
        return False

    def default_assay_schema(self) -> str:
        """
        Return default schema for assay table creation

        Returns:
            str: "", must be implemented in interface
        """
        return ""

    def _cast_column_type(
        self, assay_table: sqlalchemy.Table, assay_definition: AssayDefinition
    ):
        """
        Cast sqlalchemy.Table's columns depending backend engine.
        """
        return assay_table

    def _convert_composite_string(
        self, params: list, assay_definition: AssayDefinition
    ) -> list:
        """
        Convert string values to composite type depending backend engine.
        """
        return params

    def add_assay_table(
        self, assay_definition: AssayDefinition, assay_table: AssayDatabaseDefinition
    ) -> None:
        """
        Add a table in database for assay import

        Args:
            assay_definition: AssayDefinition used to describe assay contents
            assay_table: AssayDatabaseDefinition  used to decribe how to insert assay into database
        """
        raise AssayInterface.InvalidInterface()

    def get_all_available_assay_definitions(self) -> List[AssayDefinition]:
        """
        Return all assay definitions available in connection

        Returns:
            List[AssayDefinition]: empty list by default must be implemented in interface

        """
        return []

    def get_assay_table_definition(
        self, assay_variable: str
    ) -> AssayDatabaseDefinition:
        """
        Get AssayDatabaseDefinition from assay variable

        Args:
            assay_variable: assay variable name

        Returns:
            AssayDatabaseDefinition: assay database definition
        """
        raise AssayInterface.InvalidInterface()

    def can_import_assay_data(self) -> bool:
        """
        Return True if connection can import assay data

        Returns:
            bool: False, must be implemented in interface
        """
        return False

    def import_assay_data(self, assays: List[GenericAssay]) -> None:
        """
        Import assay into connection

        raises :class:`AssayInterface.InvalidInterface` if not implemented

        Args:
            assays: assay list to be imported
        """
        raise AssayInterface.InvalidInterface()

    def get_assay(self, hole_id: str, variable: str) -> GenericAssay:
        """
        Return GenericAssay for assay data use

        Args:
            hole_id (str): The collar id of the hole
            variable (str): variable name of assay

        Returns:
            GenericAssay: generic assay with method to access assay data
        """
        raise AssayInterface.InvalidInterface()

    def is_assay_available_for_collars(self, hole_ids: [str], variable: str) -> bool:
        """
        Check if assay is available for a collar list

        Args:
            hole_ids: list of collar id
            variable: variable name of assay

        Returns:
            bool: True if assay is available or no collar defined, False otherwise

        """
        result = len(hole_ids) == 0
        for hole_id in hole_ids:
            result |= self.is_assay_available_for_collar(hole_id, variable)
        return result

    def is_assay_available_for_collar(self, hole_id: str, variable: str) -> bool:
        """
        Check if assay is available for a collar

        Args:
            hole_id: The collar id of the hole
            variable: variable name of assay

        Returns:
            True if assay is available, False otherwise

        """
        return True
