import sys
import inspect

from abc import ABCMeta
from logging import Logger

from typing import Callable, Any, Optional, Union, List, Tuple


class DbApiTemplate(object):
    __metaclass__ = ABCMeta

    def __init__(self, conn, logger=None):
        # type: (Any, Logger) -> None
        self.conn = conn
        self.logger = logger
        self.version = sys.version_info[0]

    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)
        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("Executing query: %s\nParameters: %s", sql, params)
            cursor.execute(sql, params)

            if sys.version_info[0] >= 3:
                has_column_attributes = len(inspect.signature(mapper).parameters) >= 2
            else:
                has_column_attributes = len(inspect.getargspec(mapper)[0]) >= 2

            column_attributes = {}
            for idx in range(len(cursor.description)):
                column_attributes[idx] = cursor.description[idx]

            for row in cursor.fetchall():
                named_tuple = NamedTuple(row, column_attributes)
                if has_column_attributes:
                    value = mapper(named_tuple, column_attributes)
                else:
                    value = mapper(named_tuple)
                ret.append(value)
            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) -> Tuple[int, Any]
            self.debug("Executing query: %s\nParameters:%s", sql, params)
            cursor.execute(sql, params)
            self.debug("Query executed")
            row_count = cursor.rowcount
            ret_data = None
            try:
                row = cursor.fetchone()
                ret_data = row[0]
            except:
                pass
            return row_count, ret_data

        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 execute_many(self, sql, params):
        # type: (str, List[Tuple]) -> None
        self.conn.executemany(sql, params)

    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


class NamedTuple(object):
    def __init__(self, row, column_attributes):
        super(NamedTuple, self).__init__()
        self.row = row
        self.column_attributes = column_attributes
        self.col_map = {}
        for i in range(len(column_attributes)):
            self.col_map[column_attributes[i][0]] = i

    def __len__(self):
        return len(self.row)

    def __getitem__(self, item):
        index = item
        if isinstance(item, str):
            index = self.col_map[item]
        return self.row[index]

    def __str__(self):
        if len(self.row) == 0:
            return "()"
        ret = "("
        for el in self.row:
            ret += repr(el) + ", "
        ret = ret[0:-2]
        ret += ")"
        attrs = ""
        for i in range(len(self.row)):
            attrs += self.column_attributes[i][0] + ", "
        attrs = attrs[0:-1]
        return ret + " - " + attrs
