import logging
from abc import abstractmethod
from typing import List, Any, Optional, Union

from dict_caster.extras import first

from .clickhouse_types import Float
from .output_format import OutputFormat
from .abstract.selectable import Selectable
logger = logging.getLogger(__file__)


def prepare_value(column: Selectable, val: Any):
    if column.nullable and val == '\\N':
        val = None
    elif issubclass(column.clickhouse_type, Float) and val.lower() == 'nan':
        val = 0
    elif hasattr(column.clickhouse_type, 'prepare_return'):
        val = column.clickhouse_type.prepare_return(val)
    return val


class Result:
    def __init__(self, payload: str):
        if not len(payload):
            self.rows = []
        else:
            content = payload.strip()
            self.rows = [params.split("\t") for params in content.split("\n")]

    @abstractmethod
    def fetchall(self):
        ...

    @abstractmethod
    def fetchone(self):
        ...


class CountResult(Result):
    def fetchall(self):
        result = []
        for raw in self.rows:
            result = list(map(int, raw))
        return result

    def fetchone(self) -> int:
        return first(self.fetchall())


class SelectResult(Result):
    def __init__(self, payload: str, columns: List[Selectable], output_format: Optional[OutputFormat] = None):
        super(SelectResult, self).__init__(payload=payload)
        self.output_format = output_format
        self.selectable = columns

    def dict_result(self) -> List[dict]:
        result = []
        for row in self.rows:
            values = {}
            for column, val in zip(self.selectable, row):
                val = prepare_value(column, val)
                values[column.name] = val
            result.append(values)
        return result

    def tuple_result(self) -> List[tuple]:
        result = [[] for column in self.selectable]
        for row in self.rows:
            for column, val, res in zip(self.selectable, row, result):
                val = prepare_value(column, val)
                res.append(val)
        result = list(map(tuple, result))
        return result

    def named_tuple_result(self):
        result = {column.name: [] for column in self.selectable}
        for row in self.rows:
            for column, val in zip(self.selectable, row):
                val = prepare_value(column, val)
                result[column.name].append(val)
        result = {name: tuple(values) for name, values in result.items()}
        return result

    def list_result(self) -> List[list]:
        result = []
        for row in self.rows:
            values = []
            for column, val in zip(self.selectable, row):
                val = prepare_value(column, val)
                values.append(val)
            result.append(values)
        return result

    def fetchall(self) -> Union[list, dict]:
        if self.output_format == OutputFormat.Tuple:
            return self.tuple_result()
        elif self.output_format == OutputFormat.Dict:
            return self.dict_result()
        elif self.output_format == OutputFormat.List:
            return self.list_result()
        elif self.output_format == OutputFormat.NamedTuple:
            return self.named_tuple_result()
        elif self.output_format == OutputFormat.Raw:
            return self.rows
        else:
            return self.list_result()

    def fetchone(self) -> Any:
        return first(self.fetchall())
