import os

from onetick.py.otq import otq, _tmp_otq_path, otli, pyomd
from pandas import Timestamp as pd_Timestamp
from onetick.py.types import datetime as otp_datetime, datetime2expr
from datetime import datetime, date
from onetick.py import utils
from onetick.py.configuration import config
from onetick.py.log import get_debug_logger


def is_datetime_type(dt):
    # TODO: move to types, add docs
    return isinstance(dt, (datetime, date, pd_Timestamp, otp_datetime, pyomd.timeval_t))


def is_millisecond_precision(dt):
    """
    dt can be a datetime object, a pd.Datetime object or an otp.dt object.
    We check if it has only millisecond precision or not.
    """
    if (dt.microsecond % 1000) != 0:
        return False
    if isinstance(dt, (pd_Timestamp, otp_datetime)):
        if dt.nanosecond != 0:
            return False
    return True


class TmpOtq:
    """
    Class that represents a storage of temporary queries
    """

    class __iter_str:
        """
        Class that produces unique strings
        """
        DEFAULT_LENGTH = 6
        FIRST_CODE = 97  # 'a'
        LAST_CODE = 122  # 'z'

        def __init__(self, length=None):
            if length is None:
                length = self.DEFAULT_LENGTH
            self.__length = length
            self.__n = 0

        def __num_to_str(self):
            s = ""
            r = self.LAST_CODE - self.FIRST_CODE + 1
            for i in range(0, self.__length):
                s = chr(self.FIRST_CODE + (self.__n // r ** i) % r) + s
            return s

        def get_str(self):
            s = self.__num_to_str()
            self.__n += 1
            return s

    name_generator = __iter_str()

    def __init__(self):
        self.queries = {}

    def add_query(self, query, suffix="", name=None, params=None):
        """
        Adds query with a unique generated name to the storage.

        Parameters
        ----------
        query: otq.GraphQuery
            GraphQuery object that is being stored
        suffix: str
            Suffix that is added to the autogenerated string to form name of the query.
        name: str, optional
            If specified, this ``name`` will be used to save query
            and ``suffix`` parameter will be ignored.
        params: dict, optional
            Can specify additional parameters with which this query will be saved to file.
            Currently, only "running_query_flag" and "symbol_date" are supported

        Returns
        -------
            result: str , name of the query in the file (without THIS:: prefix).
        """
        name = name or self.name_generator.get_str() + suffix
        if name in self.queries:
            raise ValueError(f"There is already a query with name '{name}' in {self.__class__.__name__} storage")
        if params is None:
            params = {}
        self.queries[name] = (query, params)
        return name

    def merge(self, tmp_otq):
        """
        Adds queries from the tmp_otq storage to the current storage.
        As query names are guaranteed to be unique session-wide, no check for collision is necessary.
        Queries with same names will always be the same.
        """
        self.queries.update(tmp_otq.queries)

    def copy(self):
        """
        Creates a copy of the storage
        """
        res = TmpOtq()
        res.merge(self)
        return res

    def save_to_file(self, query=None, query_name="main_query", file_path=None, file_suffix="",
                     start=None, end=None, start_time_expression=None, end_time_expression=None, timezone=None,
                     running_query_flag=None,
                     symbol_date=None):
        """
        Saves all queries from the query dict and one more query (if passed);
        returns absolute path to passed query in file (if passed) or path of resulted file

        Parameters
        ----------
        query: otq.GraphQuery or None
            Additional query that is saved with all the queries from the storage. Usually, this will be the main query
            of the Source object that uses this storage
        query_name: str
            Name with which additional query will be saved. As additional query is not stored in any TmpOtq object,
            it can be saved with a name that is specified completely and not generated to be unique
        file_path: str or None
            Path to the file where all queries will be saved. If None, a TmpFile will be created
        file_suffix: str
            Only used if file_path is None. A suffix that is added to the name of a generated file.
        start: :py:class:`otp.datetime <onetick.py.datetime>`
            start time for the resulting .otq file
        end: :py:class:`otp.datetime <onetick.py.datetime>`
            end time for the resulting .otq file
        start_time_expression: str or None
            start time expression for the resulting .otq file
        end_time_expression: str or None
            end time expression for the resulting .otq file
        timezone: str
            timezone for the resulting .otq file
        symbol_date: :py:class:`otp.datetime <onetick.py.datetime>` or :py:class:`datetime.datetime` or int
            Symbol date for the query or integer in the YYYYMMDD format.
            Will be applied only to the query specified in the ``query`` parameter.

        Returns
        -------
            result: str , contains path to the resulting file with the name of the additional query
            (if "query" parameter is not None) or path to the resulting file otherwise

        """
        if file_path is None:
            base_dir = None
            if os.getenv('OTP_WEBAPI_TEST_MODE'):
                base_dir = _tmp_otq_path()
            kwargs = {}
            if config.otq_debug_mode:
                kwargs['clean_up'] = False
            tmp_file = utils.TmpFile(suffix=file_suffix,
                                     base_dir=base_dir,
                                     **kwargs)
            file_path = tmp_file.path

        queries_dict = dict(self.queries)

        if query is not None:
            if query_name in self.queries.keys():
                query_name = self.name_generator.get_str() + "_" + query_name
            query_params = {}
            if running_query_flag:
                query_params['running_query_flag'] = running_query_flag
            if symbol_date is not None:
                query_params['symbol_date'] = symbol_date
            queries_dict[query_name] = (query, query_params)

        # defining file-wise start/end times and time expressions

        # timezone definitions
        # in onetick and in dateutil can differ (e.g. "GMT-10" is +10:00 offset in onetick and -10:00 offset
        # in dateutil), therefore we always use time expressions to force onetick to make time conversions by itself
        # TODO: create a BDS ticket for the onetick team to fix it somehow
        if is_datetime_type(start):
            if not start_time_expression:
                start_time_expression = datetime2expr(start)
            start = None
        if is_datetime_type(end):
            if not end_time_expression:
                end_time_expression = datetime2expr(end)
            end = None

        # constructing a list of otq.Query objects, which will hold graphs, names, start/end times etc.
        query_list = []
        for stored_query_name in queries_dict.keys():   # noqa
            stored_query = otq.Query(queries_dict[stored_query_name][0])
            stored_query_params = queries_dict[stored_query_name][1]
            if timezone:
                stored_query.set_timezone(timezone)
            stored_query.set_symbols(queries_dict[stored_query_name][0].symbols())
            stored_query.set_query_name(stored_query_name)
            stored_query.set_start_time(start)
            stored_query.set_end_time(end)
            if 'running_query_flag' in stored_query_params.keys():
                stored_query.set_running_query_flag(stored_query_params['running_query_flag'])
            if start_time_expression:
                stored_query.set_start_time_expression(start_time_expression)
            if end_time_expression:
                stored_query.set_end_time_expression(end_time_expression)
            if 'symbol_date' in stored_query_params.keys():
                stored_symbol_date = stored_query_params['symbol_date']
                if stored_symbol_date is not None:
                    stored_symbol_date = int(utils.symbol_date_to_str(stored_symbol_date))
                    stored_query.set_symbol_date(stored_symbol_date)
            query_list.append(stored_query)

        _ = otli.OneTickLib()
        otq_file = otq.OtqFile(query_list)
        otq_file.save_to_file(file_path)
        get_debug_logger().debug(f'otq file saved to: {file_path}')

        if query is not None:
            return file_path + "::" + query_name
        else:
            return file_path
