from abc import ABCMeta
from logging import Logger

from typing import Callable, Any, Optional, Union


class DbApiTemplate(object):
    __metaclass__ = ABCMeta

    def __init__(self, conn, logger=None):
        # type: (Any, Logger) -> None
        self.conn = conn
        self.logger = logger

    def get_scalar(self, sql):
        def inner_func(cursor):
            # type: (Any) -> int
            return cursor.execute(sql).fetchone()[0]

        return self.execute_with_cursor(inner_func)

    def one(self, sql, params=(), mapper=None):
        def default_mapper(row):
            return row[0]

        used_mapper = default_mapper
        if mapper is not None:
            used_mapper = mapper

        return self.execute(sql, params, used_mapper)[0]

    def first(self, sql, params=(), mapper=None):
        def default_mapper(row):
            return row[0]

        used_mapper = default_mapper
        if mapper is not None:
            used_mapper = mapper

        ret = self.execute(sql, params, used_mapper)[0]
        if len(ret) > 0:
            return ret[0]
        else:
            return None

    def execute(self, sql, params=(), mapper=None):
        # type: (str, tuple, Optional[Callable[[tuple], Any]]) -> Union[int, list]
        if mapper is None:
            return self.execute_scalar(sql, params)
        else:
            return self.execute_query(sql, params, mapper)

    def execute_query(self, sql, params, mapper):
        # type: (str, tuple, Optional[Callable[[tuple], Any]]) -> Union[int, list]
        def inner_func(cursor):
            # type: (tuple) -> list
            ret = []
            self.debug("Executed query: %s\nParameters:%s", sql, params)
            cursor.execute(sql, params)
            for row in cursor.fetchall():
                ret.append(mapper(row))
            return ret

        return self.execute_with_cursor(inner_func)

    def execute_scalar(self, sql, params):
        # type: (str, tuple) -> int
        def inner_func(cursor):
            # type: (Any) -> None
            self.debug("Executing query: %s\nParameters:%s", sql, params)
            cursor.execute(sql, params)
            self.debug("Query executed")
            return cursor.rowcount

        return self.execute_with_cursor(inner_func)

    def execute_with_cursor(self, method):
        # type: (Callable[[tuple], Any]) -> Any
        cursor = None
        try:
            cursor = self.conn.cursor()
            return method(cursor)
        finally:
            if cursor is not None:
                cursor.close()

    def debug(self, msg, *params):
        if self.logger is None:
            return
        self.logger.debug(msg, params)


class UnitOfWork(object):
    def __init__(self, conn):
        # type: (Any) -> None
        self.conn = conn

    def with_transaction(self, method):
        try:
            ret = method()
            self.conn.commit()
            return ret
        except:
            self.conn.rollback()
            raise
