from abc import ABC, abstractmethod
from pathlib import Path
from typing import List, Optional

from pydantic import ValidationError

try:
    import sqlalchemy
    from sqlalchemy import text

    _HAS_SQLALCHEMY = True
except ImportError:
    _HAS_SQLALCHEMY = False


class SQLDriver(ABC):
    """
    Abstract class for SQL drivers. The expose common functionality
    for validating SQL queries.
    """

    @abstractmethod
    def validate_sql(self, query: str) -> List[str]:
        ...

    @abstractmethod
    def get_schema(self) -> str:
        ...


class SimpleSqlDriver(SQLDriver):
    """
    Simple SQL driver which uses sqlvalidator to validate SQL queries.
    Does not understands dialects and is not connected to a database.
    """

    def validate_sql(self, query: str) -> List[str]:
        import sqlvalidator

        sql_query = sqlvalidator.parse(query)
        if not sql_query.is_valid():
            return sql_query.errors
        return sql_query.errors

    def get_schema(self) -> str:
        raise NotImplementedError


class SqlAlchemyDriver(SQLDriver):
    """
    SQL driver which uses sqlalchemy to validate SQL queries.
    It can setup the database schema and check if the queries are valid
    by connecting to the database.
    """

    def __init__(self, schema_file: Optional[str], conn: Optional[str]) -> None:
        if not _HAS_SQLALCHEMY:
            raise ImportError(
                """The functionality requires sqlalchemy to be installed.
                              Please install it using `pip install SqlAlchemy`"""
            )

        if schema_file is not None and conn is None:
            raise ValidationError(
                """schema_file should accompany a sql connection string for
           guardrails to apply it to a database backend.
           Use sqlite for ex: sqlite://"""
            )

        if conn is not None:
            try:
                self._engine = sqlalchemy.create_engine(conn)
                self._conn = self._engine.connect()
            except Exception as ex:
                raise ValueError(ex)

        if schema_file is not None:
            schema = Path(schema_file).read_text()
            if conn.startswith("sqlite"):
                self._conn.connection.executescript(schema)
            else:
                from sqlalchemy import text

                self._conn.execute(text(schema))

    def validate_sql(self, query: str) -> List[str]:
        exceptions: List[str] = []
        try:
            self._conn.execute(text(query))
        except Exception as ex:
            exceptions.append(str(ex))
        return exceptions

    def get_schema(self) -> str:
        # Get table schema using sqlalchemy.inspect
        insp = sqlalchemy.inspect(self._conn)

        schema = {}
        for table in insp.get_table_names():
            schema[table] = {}
            for column in insp.get_columns(table):
                schema[table][column["name"]] = {"type": column["type"]}

            # Get foreign keys
            for fk in insp.get_foreign_keys(table):
                schema[table][fk["constrained_columns"][0]]["foreign_key"] = {
                    "table": fk["referred_table"],
                    "column": fk["referred_columns"][0],
                }

        # Create a nicely formatted schema from the dictionary
        formatted_schema = []
        for table, columns in schema.items():
            formatted_schema.append(f"Table: {table}")
            for column, column_info in columns.items():
                formatted_schema.append(f"    Column: {column}")
                for info, value in column_info.items():
                    formatted_schema.append(f"        {info}: {value}")

        return "\n".join(formatted_schema)


def create_sql_driver(
    schema_file: Optional[str] = None, conn: Optional[str] = None
) -> SQLDriver:
    if schema_file is None and conn is None:
        return SimpleSqlDriver()
    return SqlAlchemyDriver(schema_file=schema_file, conn=conn)
