from abc import ABC, abstractmethod
from typing import Any, Dict, Sequence, Tuple
import collections
from .config import _EXECUTE_PARAM_TYPE, Operation, BaseTableConfig

# sql syntax may diff for different db, each backend should implement it's sql_builder
class SqlBuilder(ABC):
    """
    Build select, update, delete sql for relational database.
    It's used by relational cache to manipulate data.
    """

    def build_select_all_table_operation(self) -> Operation:
        """
        Generate sql to select all tables in the database.
        Table name as first selected column.
        """
        raise NotImplementedError()

    def build_select_table_columns_operation(self, table: str, filter_uniq=False) -> Operation:
        """
        Generate sql to select all columns for given table.
        Column name as first selected result.
        :param table:           the queried table
        :param filter_uniq:     return only id column
        """
        raise NotImplementedError()

    def get_column_name_from_result(self, result: Sequence[Any]) -> str:
        """
        Get column name from result of operation generated by `build_select_table_columns_operation()`.
        """
        # assume first result is column name.
        return result[0]

    @abstractmethod
    def build_create_table_operation(self, *configs: BaseTableConfig, check_exists=True):
        """
        Generate sql to create a table.
        """

    @abstractmethod
    def build_select_operation(self, config: BaseTableConfig, key: Sequence[Any] = None, limit: int = -1,
                               select_columns: Sequence[str] = None, condition_columns: Sequence[str] = None,
                               distinct=False) -> Operation:
        """
        Generate sql to select row for given key.
        :param config:          scope configuration
        :param key:             uniq key for the row, maybe `None` to select all rows in the table
        :param limit:           limit number
        :param select_columns:  columns to be selected; if not set, select all in config
        :param condition_columns:  columns for where condition; if not set, use uniq columns
        :param distinct:        whether distinct select columns
        """

    @abstractmethod
    def build_update_operation(self, config: BaseTableConfig, key: Sequence[Any], value: Dict[str, Any]) -> Operation:
        """
        Generate sql to update or insert row for given key with given value.
        :param config:      scope configuration
        :param key:         uniq key for the row
        :param value:       column values for given uniq key
        """

    @abstractmethod
    def build_delete_operation(self, config: BaseTableConfig, key: Sequence[Any] = None) -> Operation:
        """
        Generate sql to delete row for given key.
        :param config:      scope configuration
        :param key:         uniq key for the row, maybe `None` to delete all rows in the table, be careful
        """

class SimpleSqlBuilder(SqlBuilder, ABC):
    """
    A simple implement of SqlBuilder.
    """

    # whether to pass parameters as a list
    list_parameter = False

    # if column type not specified in the config, use this value as default
    # should be overwritten for different backends
    default_column_type = 'text'

    def _get_column_type(self, col: str, config: BaseTableConfig) -> str:
        return config.get_column_type(col) or self.default_column_type

    def _gen_column_def(self, col: str, config: BaseTableConfig) -> str:
        t = self._get_column_type(col, config)
        s = f"{col} {t}"
        # if col in config.uniq_columns:
        # s += " UNIQUE"
        # s += ' PRIMARY KEY'
        return s

    def _gen_column_defines(self, config: BaseTableConfig):
        columns = [self._gen_column_def(x, config) for x in config.columns_with_id]
        columns += [f" PRIMARY KEY ({','.join(config.uniq_columns)})"]
        return ','.join(columns)

    def _gen_create_table_sql(self, config: BaseTableConfig, check_exists=True) -> str:
        sql = "CREATE TABLE"
        if check_exists:
            sql += " IF NOT EXISTS"
        sql += f" {config.table} ({self._gen_column_defines(config)})"
        return sql

    def build_create_table_operation(self, *configs: BaseTableConfig, check_exists=True) -> Operation:
        lines = [self._gen_create_table_sql(x) + ";" for x in configs]
        return Operation(statement="\n".join(lines), many=len(configs) > 1)

    @abstractmethod
    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        """
        Get variable placeholder in operation template.
        :param name:                variable name, used for named and pyformat style
        :param order:               order in the parameters, used for numeric style, start from 1
        :param value:               variable value, for ansi c printf format codes without '%', such as 's' or 'f'
                                    used for format and pyformat style
        """

    def config_variable(self, name: str = None, order: int = None, value: Any = None,
                        variable_mapping: Dict[str, Any] = None) -> str:
        """
        Configure a variable with name, order, value.
        :param name:                variable name, used for named and pyformat style
        :param order:               order in the parameters, used for numeric style, start from 1
        :param value:               variable value, for ansi c printf format codes without '%', such as 's' or 'f'
                                    used for format and pyformat style
        :param variable_mapping:    name -> value, used to save value to variable in this method
        :return:                    a placeholder in operation template
        """
        if variable_mapping is not None:
            variable_mapping[name] = value
        return self.get_variable_placeholder(name=name, order=order, value=value)

    def normalize_variable_values(self, variable_values: Dict[str, Any],
                                  variable_names: Sequence[str] = None) -> _EXECUTE_PARAM_TYPE:
        """
        All parameters are saved in a dict first, in this method, it can be changed to a list.
        Element in variable_names should be included in variable_values.
        """
        if self.list_parameter:
            if variable_names is not None:
                return [variable_values[x] for x in variable_names]
            return list(variable_values.values())
        return variable_values

    def build_select_operation(self, config: BaseTableConfig, key: Sequence[Any] = None, limit: int = -1,
                               select_columns: Sequence[str] = None, condition_columns: Sequence[str] = None,
                               distinct=False) -> Operation:
        if select_columns is None:
            # only select configured columns; if id not configured, ignore it
            select_columns = config.columns
        elif isinstance(select_columns, str):
            select_columns = [select_columns]
        stmt = "SELECT"
        if distinct:
            stmt += f" DISTINCT"
        stmt += f" {','.join(select_columns)} FROM {config.table}"
        variable_values = collections.OrderedDict()
        if key is not None:
            if condition_columns is None:
                # use uniq columns as where condition
                condition_columns = config.uniq_columns
            elif isinstance(condition_columns, str):
                condition_columns = [condition_columns]
            assert len(condition_columns) == len(key)
            condition = []
            for i, (name, k) in enumerate(zip(condition_columns, key), 1):
                placeholder = self.config_variable(name=name, order=i, value=k, variable_mapping=variable_values)
                condition.append(f"{name} = {placeholder}")
            condition = ' AND '.join(condition)
            stmt += f" WHERE {condition}"
        if limit > 0:
            stmt += f" LIMIT {limit}"
        return Operation(statement=stmt, parameters=self.normalize_variable_values(variable_values))

    def _gen_update_statement(self, config: BaseTableConfig, value: Dict[str, Any]) -> Tuple[str, bool]:
        """
        Generate statement for Operation, data of uniq column have added to value.
        :return:    (statement, many or not)
        """
        # sql is writen based of syntax of sqlite, maybe is incapable with other database
        first = ','.join(self.config_variable(name=k, order=i, value=v)
                         for i, (k, v) in enumerate(value.items(), 1))
        second = ','.join(f'{k}={self.config_variable(name=k, order=i, value=v)}'
                          for i, (k, v) in enumerate(value.items(), 1))
        return (f"INSERT INTO {config.table}({','.join(value.keys())})"
                f" VALUES ({first})"
                f" ON CONFLICT({','.join(config.uniq_columns)}) DO UPDATE"
                f" SET {second}"), False

    def _gen_update_variables(self, value: Dict[str, Any]) -> Tuple[Dict[str, Any], Sequence[str]]:
        """
        Generate variable values and name list used when execute().
        :param value:   value with uniq column
        :return:    (variable_values, variable_name_list)
        """
        return value, list(value.keys()) * 2

    def build_update_operation(self, config: BaseTableConfig, key: Sequence[Any], value: Dict[str, Any]) -> Operation:
        # add uniq column to value
        assert len(config.uniq_columns) == len(key)
        value = dict(value)
        for name, k in zip(config.uniq_columns, key):
            value[name] = k

        # gen statement and parameters
        stmt, many = self._gen_update_statement(config=config, value=value)
        variable_values, variable_names = self._gen_update_variables(value)
        parameters = self.normalize_variable_values(variable_values, variable_names)

        return Operation(statement=stmt, parameters=parameters, many=many)

    def build_delete_operation(self, config: BaseTableConfig, key: Sequence[Any] = None) -> Operation:
        stmt = f"DELETE FROM {config.table}"
        variable_values = collections.OrderedDict()
        if key is not None:
            assert len(config.uniq_columns) == len(key)
            condition = []
            for i, (name, k) in enumerate(zip(config.uniq_columns, key), 1):
                placeholder = self.config_variable(name=name, order=1, value=k,
                                                   variable_mapping=variable_values)
                condition.append(f"{name} = {placeholder}")
            condition = ' AND '.join(condition)
            stmt += f" WHERE {condition}"
        return Operation(statement=stmt, parameters=self.normalize_variable_values(variable_values))

class QmarkSqlBuilder(SimpleSqlBuilder):

    list_parameter = True

    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        return "?"

class NumericSqlBuilder(SimpleSqlBuilder):

    list_parameter = True

    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        return f":{order}"

class NamedSqlBuilder(SimpleSqlBuilder):

    list_parameter = False

    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        return f":{name}"

class FormatSqlBuilder(SimpleSqlBuilder):

    list_parameter = True

    def get_value_type(self, value: Any) -> str:
        if isinstance(value, str):
            return 's'
        if isinstance(value, int):
            return 'd'
        if isinstance(value, float):
            return 'f'
        return 's'

    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        return f"%{self.get_value_type(value)}"

class PyformatSqlBuilder(FormatSqlBuilder):

    list_parameter = False

    def get_variable_placeholder(self, name: str = None, order: int = None, value: Any = None) -> str:
        return f"%({name}){self.get_value_type(value)}"

# paramstyle -> builder
DEFAULT_SQL_BUILDERS = {
    "qmark": QmarkSqlBuilder(),
    "numeric": NumericSqlBuilder(),
    "named": NamedSqlBuilder(),
    "format": FormatSqlBuilder(),
    "pyformat": PyformatSqlBuilder(),
}

