from ....configurator import Config
from ....logging import logging
from ....tables import Table

import cx_Oracle
from typing import LiteralString, Any



class Oracle:
    __slots__ = ('__connection', '__cursor')

    @logging(prefix=__name__)
    def __init__(
        self,
        db: str = None,
        username: str = None,
        password: str = None
    ):
        """
            Create db connection.

            :param db: 'tns name'
            :param username: 'username'
            :param password: 'password'
        """
        try:
            # default -----
            if db is None:
                try: db = Config.databases.oracle.db
                except: pass

            if username is None:
                try: username = Config.databases.oracle.username
                except: pass

            if password is None:
                try: password = Config.databases.oracle.password
                except: pass

            if db is None or username is None or password is None:
                raise Exception(f'Connection parameters for "oracle" db not found')

            # init -----
            self.__connection = cx_Oracle.connect(
                username, password, db,
                encoding='utf-8'
            )
            self.__cursor = self.__connection.cursor()

        except cx_Oracle.DatabaseError as dbExc:
            self.close(logger=self.__init__.logger)
            raise Exception(dbExc.args[0].message)
        pass


    @logging(prefix=__name__)
    def close(self):
        """
        Close db connection.

        :return:
        """
        try:
            # init -----
            if self.__cursor:
                self.__cursor.close()

            if self.__connection:
                self.__connection.close()

        except cx_Oracle.DatabaseError as dbExc:
            raise Exception(dbExc.args[0].message)
        pass


    @logging(prefix=__name__)
    def sql(
        self,
        stmt: LiteralString,
        params: dict[str,Any] = None
    ) -> Table|int:
        """
        Execute Oracle SQL statement.

        :param stmt: 'SQL stmt'
        :param params:
            {
                'parameter[0].name': 'parameter[0].value',
                'parameter[1].name': 'parameter[1].value',
                ...
            }
        :return:
            {
                'columns': ['column[0].name', 'column[1].name', ...],
                'data': [
                    {'column[0].name': 'row[0].column[0].value', 'column[1].name': 'row[0].column[1].value', ...},
                    {'column[0].name': 'row[1].column[0].value', 'column[1].name': 'row[1].column[1].value', ...},
                    ...
                ]
            }
        """

        try:
            # default -----
            if params is None:
                params = {}

            # exec stmt -----
            self.__cursor.execute(stmt, params)

            # result -----
            result = {}

            if self.__cursor.description:  # select
                # columns -----
                result['columns'] = [
                    column[0]
                    for column in self.__cursor.description
                ]

                @logging(prefix=__name__)
                def __value(var: Any, typ: Any) -> Any:
                    if typ in (cx_Oracle.CLOB, cx_Oracle.NCLOB) and var is not None:
                        return var.read()
                    return var

                # data -----
                result['data'] = [
                    {
                        result['columns'][column]: __value(
                            var=row[column],
                            typ=self.__cursor.description[column][1],
                            logger=self.sql.logger
                        )
                        for column in range(len(result['columns']))
                    }
                    for row in self.__cursor.fetchall()
                ]

                result = Table(table=result, logger=self.sql.logger)

            else:  # DML stmt
                result = self.__cursor.rowcount  # rows

            return result

        except cx_Oracle.DatabaseError as dbExc:
            self.close(logger=self.sql.logger)
            raise Exception(dbExc.args[0].message)
        pass


    @logging(prefix=__name__)
    def plsql(
        self,
        stmt: LiteralString,
        params: dict[str,Any] = None,
        params_out=None
    ) -> dict[str,Any]:
        """
        Execute PL/SQL statement.

        :param stmt: 'PL/SQL stmt'
        :param params:
            {
                'parameter[0].name': 'parameter[0].value',  # in
                'parameter[1].name': 'parameter[1].value',  # in-out
                ...
            }
        :param params_out:
            {
                'parameter[1].name': parameter[1].type(),  # in-out
                'parameter[2].name': parameter[2].type(),  # out
                ...
            }
        :return:
            {
                'parameter[1].name': 'parameter[1].value',  # in-out
                'parameter[2].name': 'parameter[2].value',  # out
                ...
            }
        """

        try:
            # default -----
            if params is None:
                params = {}

            if params_out is None:
                params_out = {}

            # init out-params -----
            params_out = {
                k: self.__cursor.var(type(v))
                for k, v in params_out.items()
            }

            # set value for in-out params -----
            for k, v in params_out.items():
                if k in params:
                    v.setvalue(0, params[k])

            # params aggregation: out(+in-out) params -----
            params.update(params_out)

            # exec stmt -----
            self.__cursor.execute(stmt, params)

            # result -----
            result = {
                k: params[k].getvalue()
                for k in params_out.keys()
            }

            return result

        except cx_Oracle.DatabaseError as dbExc:
            self.close(logger=self.plsql.logger)
            raise Exception(dbExc.args[0].message)
        pass


    @logging(prefix=__name__)
    def commit(self):
        """
        Commit current transaction to the database.

        :return:
        """

        try:
            if self.__connection:
                self.__connection.commit()
        except cx_Oracle.DatabaseError as dbExc:
            raise Exception(dbExc.args[0].message)
        pass


    @logging(prefix=__name__)
    def rollback(self):
        """
        Rollback current transaction to the database.

        :return:
        """

        try:
            if self.__connection:
                self.__connection.rollback()
        except cx_Oracle.DatabaseError as dbExc:
            raise Exception(dbExc.args[0].message)
        pass

    pass
