import logging
from typing import List, Optional

import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype, is_period_dtype, is_string_dtype

from upgini.errors import ValidationError


class DateTimeSearchKeyConverter:
    def __init__(self, date_column: str, date_format: Optional[str] = None, logger: Optional[logging.Logger] = None):
        self.date_column = date_column
        self.date_format = date_format
        if logger is not None:
            self.logger = logger
        else:
            self.logger = logging.getLogger()
            self.logger.setLevel("FATAL")
        self.generated_features: List[str] = []

    @staticmethod
    def _int_to_opt(i: int) -> Optional[int]:
        if i == -9223372036855:
            return None
        else:
            return i

    def convert(self, df: pd.DataFrame) -> pd.DataFrame:
        df = df.copy()
        if is_string_dtype(df[self.date_column]):
            try:
                df[self.date_column] = pd.to_datetime(df[self.date_column], format=self.date_format)
            except ValueError as e:
                raise ValidationError(e)
        elif is_period_dtype(df[self.date_column]):
            df[self.date_column] = pd.to_datetime(df[self.date_column].astype("string"))
        elif is_numeric_dtype(df[self.date_column]):
            msg = f"Unsupported type of date column {self.date_column}. Convert to datetime please."
            self.logger.warn(msg)
            raise ValidationError(msg)

        # If column with date is datetime then extract seconds of the day and minute of the hour
        # as additional features
        seconds = "datetime_seconds"
        df[seconds] = (df[self.date_column] - df[self.date_column].dt.floor("D")).dt.seconds

        if (df[seconds] != 0).any():
            self.logger.info("Time found in date search key. Add extra features based on time")
            seconds_in_day = 60 * 60 * 24
            orders = [1, 2, 24, 48]
            for order in orders:
                sin_feature = f"datetime_time_sin_{order}"
                cos_feature = f"datetime_time_cos_{order}"
                df[sin_feature] = np.round(np.sin(2 * np.pi * order * df[seconds] / seconds_in_day), 10)
                df[cos_feature] = np.round(np.cos(2 * np.pi * order * df[seconds] / seconds_in_day), 10)
                self.generated_features.append(sin_feature)
                self.generated_features.append(cos_feature)

        df.drop(columns=seconds, inplace=True)

        df[self.date_column] = df[self.date_column].dt.floor("D").view(np.int64) // 1_000_000
        df[self.date_column] = df[self.date_column].apply(self._int_to_opt).astype("Int64")

        return df
