from typing import Optional, Any
import numpy as np
import pandas as pd
from loguru import logger

from base.base import BasePointPassportFormer, BaseIdealDataBuilder, PointPassport, GraphicPassport, Settings, UsefulGraphData


class PassportFormer(BasePointPassportFormer):

    def form_passports(self, data: list[pd.DataFrame]) -> None:
        """
        Формирует паспорта для каждого DataFrame из списка.
        В случае ошибки логируется сообщение и возвращается пустой список.
        """
        try:
            return_data = [self._build_from_df_only(dataframe) for dataframe in data]
        except Exception as e:
            logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
            return_data = []
        finally:
            self._mediator.notify(self, return_data)
            
    def update_settings(self, settings: Settings):
        self._settings = settings
    
    def form_customer_passport(self, data: list[pd.DataFrame, dict]) -> None:
        """
        Формирует паспорт заказчика на основе DataFrame и словаря событий.
        В случае ошибки логируется сообщение и возвращается пустой список.
        """
        try:
            dataframe, events, ME_coords = data
            point_quantity = len(events["Squeeze"][0])
            self._modify_coord_settings(ME_coords)
            data_passport = self._form_graphic_passport(dataframe, events, point_quantity)
            return_data = [data_passport]
        except Exception as e:
            logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
            return_data = []
        finally:
            self._mediator.notify(self, return_data)
    
    def _modify_coord_settings(self, ME_coords:tuple) -> None:
        part_pos, open_pos = ME_coords
        self._settings.operator["distance_l_2"] = open_pos
        l1 = self._settings.operator["distance_l_1"]
        self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range (len(part_pos))]
        

    @staticmethod
    def _find_indexes(signal: str, dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
        """
        Находит индексы начала и конца этапа для указанного сигнала.
        """
        stage_diff = np.diff(dataframe[signal])
        start_idx = np.where(stage_diff == 1)
        finish_idx = np.where(stage_diff == -1)
        return start_idx[0], finish_idx[0]
    
    @staticmethod
    def _find_events(signal: str,
                     times: pd.Series,
                     dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
        """
        Формирует списки времен начала и окончания событий для указанного сигнала.
        """
        start_idx, finish_idx = PassportFormer._find_indexes(signal, dataframe)
       
        if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
            start_idx = np.insert(start_idx, 0, 0)
        start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
        end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
        if len(start_list) - len(end_list) == 1:
            end_list.append(float(times.iloc[-1]))
        return start_list, end_list
    
    def _generate_events(self,
                         times: pd.Series,
                         dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
        """
        Генерирует словарь событий для каждого этапа и определяет количество точек.
        Если для основного этапа (_clear_stage) не найдено ни одного события,
        логируется ошибка и возвращается пустой список.
        """
        events = {}
        point_quantity = 0
        if self._clear_stage in self._stages:
            start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
            point_quantity = len(start_list)
            if point_quantity == 0:
                logger.error("_generate_events - Не найдены события для этапа '{}'.", self._clear_stage)
                return {}, 0  # Возвращаем пустой словарь и 0 точек
            
            for stage in self._stages:
                s_list, e_list = self._find_events(stage, times, dataframe)
                temp = min(len(s_list), len(e_list))
                if temp < point_quantity:
                    logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
                                   f"Ожидается {point_quantity}, получено {temp}. Заполнение нулями/единицами.")
                    s_list += [0] * (point_quantity - temp)
                    e_list += [1] * (point_quantity - temp)
                events[stage] = [s_list, e_list]
        return events, point_quantity
    
    def _build_ideal_data(self,
                          idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
                          point_settings: Settings = None) -> dict:
        """
        Строит идеальные данные с использованием переданного билдера.
        """
        try:
            self.opt_algorithm = idealDataBuilder(point_settings)
            stage_ideals = {
                "Closing": self._opt_algorithm.get_closingDF(),
                "Squeeze": self._opt_algorithm.get_compressionDF(),
                "Welding": self._opt_algorithm.get_weldingDF(),
                "Relief": self._opt_algorithm.get_openingDF(),
                "Oncomming": self._opt_algorithm.get_oncomingDF(),
                "Ideal cycle": self._opt_algorithm.get_cycle_time(),
                "Ideal timings": self._opt_algorithm.get_ideal_timings()
            }
            return stage_ideals
        except Exception as e:
            logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
            return {}
    
    def _generate_cache_key(self,
                            point_settings: Settings) -> tuple[tuple[tuple[str, Any], ...], tuple[tuple[str, Any], ...]]:
        """
        Преобразует point_settings в хешируемый ключ для кэша.
        """
        try:
            operator_tuple = frozenset(
                (key, value)
                for key, value in point_settings.operator.items()
                if str(key) in self._OptAlgorithm_operator_params
            )
            system_tuple = frozenset(
                (key, value)
                for key, value in point_settings.system.items()
                if str(key) in self._OptAlgorithm_system_params
            )
            return (operator_tuple, system_tuple)
        except Exception as e:
            logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
            return ((), ())
    
    def _build_from_df_only(self, dataframe: pd.DataFrame) -> GraphicPassport:
        """
        Строит GraphicPassport на основе одного DataFrame.
        Если DataFrame содержит необходимые столбцы, события генерируются; 
        иначе используется альтернативное определение point_quantity.
        Если количество точек равно нулю, логируется ошибка и возвращается None.
        """
        try:
            if (dataframe is not None and
                set(self._stages).issubset(set(dataframe.columns.tolist()))):
                events, point_quantity = self._generate_events(dataframe["time"], dataframe)
                point_quantity = len(events["Welding"][0])
                if point_quantity == 0:
                    logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
                    return None
            else:
                events = None
                key = list(self._settings.operator.keys())[0]
                point_quantity = len(self._settings.operator[key])
            self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
            passport = self._form_graphic_passport(dataframe, events, point_quantity)
            return passport
        except Exception as e:
            logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
            return None

    def _form_graphic_passport(self,
                               dataframe: pd.DataFrame,
                               events: dict,
                               point_quantity: int) -> GraphicPassport:
        """
        Формирует графический паспорт с полезными данными и списком PointPassport.
        """
        try:
            system_settings = {key: value[0] for key, value in self._settings.system.items()}
            graphic_passport = GraphicPassport(dataframe, [], self._form_graphic_useful_data(system_settings))

            for i in range(point_quantity):
                point_settings = Settings(self._get_operator_settings_part(i), system_settings)
                timeframe, po_events = self._form_point_events(events, i)
                if timeframe and po_events:
                    point_settings.operator["time_robot_movement"] = po_events["Oncomming"][1] - po_events["Oncomming"][0]
                ideal_data = self._form_point_ideal_data(point_settings)
                useful_data = self._form_point_useful_data(point_settings.operator)
                point_passport = PointPassport(timeframe, po_events, ideal_data, useful_data)
                graphic_passport.points_pocket.append(point_passport)
            return graphic_passport
        except Exception as e:
            logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
            return None

    def _form_graphic_useful_data(self, system_settings: dict) -> dict:
        """
        Формирует словарь полезных данных для графика.
        """
        try:
            tesla_time = sum(self._settings.operator.get("Tesla summary time", []))
            useful_data = UsefulGraphData(
                tesla_time,
                system_settings["Range ME, mm"],
                system_settings["k_hardness_1"]
            )
            return useful_data
        except Exception as e:
            logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
            return {}

    def _form_point_useful_data(self, operator_settings: dict) -> dict:
        """
        Формирует полезные данные для отдельной точки.
        """
        try:
            useful_data = {
                "thickness": operator_settings["object_thickness"],
                "part_pos": operator_settings["part_pos"],
                "force": operator_settings["force_target"]
            }
            return useful_data
        except Exception as e:
            logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
            return {}
    
    def _form_point_ideal_data(self, point_settings: Settings) -> dict:
        """
        Формирует идеальные данные для точки с использованием кэша.
        """
        try:
            cache_key = self._generate_cache_key(point_settings)
            ideal_data = self._ideal_data_cashe.get(
                cache_key,
                self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
            )
            self._ideal_data_cashe[cache_key] = ideal_data
            return ideal_data
        except Exception as e:
            logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
            return {}
    
    def _get_operator_settings_part(self, idx: int) -> dict:
        """
        Извлекает часть настроек оператора для конкретного индекса.
        """
        try:
            operator_settings = {
                key: (value[idx] if idx < len(value) else value[0])
                for key, value in self._settings.operator.items()
            }
            return operator_settings
        except Exception as e:
            logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
            return {}
    
    def _form_point_events(self, events: dict, idx: int) -> list[list, dict]:
        """
        Формирует временной интервал и события для отдельной точки.
        Если событий нет, возвращает (None, None).
        """
        try:
            timeframe, point_events = None, None
            if events is not None:
                idx_shift = idx + 1 if events[self._stages[-1]][0][0] == 0 else idx
                timeframe = [events[self._stages[0]][0][idx], events[self._stages[-1]][1][idx_shift]]
                point_events = {key: [value[0][idx], value[1][idx]] for key, value in events.items()}
            return timeframe, point_events
        except Exception as e:
            logger.error(f"_form_point_events - Ошибка при формировании событий для точки {idx}: {e}")
            return None, None
    

class IdealDataBuilder(BaseIdealDataBuilder):

    def get_closingDF(self) -> pd.DataFrame:
        try:
            return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
        except Exception as e:
            logger.error(f"get_closingDF - Ошибка при получении данных для этапа закрытия: {e}")
            return pd.DataFrame()

    def get_compressionDF(self) -> pd.DataFrame:
        try:
            return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
        except Exception as e:
            logger.error(f"get_compressionDF - Ошибка при получении данных для этапа сжатия: {e}")
            return pd.DataFrame()

    def get_openingDF(self) -> pd.DataFrame:
        try:
            return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
        except Exception as e:
            logger.error(f"get_openingDF - Ошибка при получении данных для этапа открытия: {e}")
            return pd.DataFrame()

    def get_oncomingDF(self) -> pd.DataFrame:
        try:
            return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
        except Exception as e:
            logger.error(f"get_oncomingDF - Ошибка при получении данных для этапа движения: {e}")
            return pd.DataFrame()

    def get_weldingDF(self) -> pd.DataFrame:
        try:
            data = []
            X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
            X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
            points_num = 5
            for i in range (points_num+1):
                data.append({
                    "time": self.welding_time*i/points_num,
                    "Position FE": X1,
                    "Position ME": X2,
                    "Rotor Speed FE": V1,
                    "Rotor Speed ME": V2,
                    "Force": F
                })
            return pd.DataFrame(data)
        except Exception as e:
            logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
            return pd.DataFrame()

    def get_ideal_timings(self) -> list[float]:
        try:
            data = self.Ts
            ideal_timings = [
                data['tclose'],
                data['tgrow'],
                self.welding_time,
                self.getMarkOpen(),
                data['tmovement']
            ]
            return ideal_timings
        except Exception as e:
            logger.error(f"get_ideal_timings - Ошибка при получении идеальных временных интервалов: {e}")
            return []

    def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
        """
        Получает данные до указанного времени с шагом, определяемым mul.
        """
        try:
            data = []
            for i in range(0, int(end_timestamp * self.mul) + 1):
                time = i / self.mul
                X1, X2, V1, V2, F = func(time)
                data.append({
                    "time": time,
                    "Position FE": X1 * 1000,
                    "Position ME": X2 * 1000,
                    "Rotor Speed FE": V1 * 1000,
                    "Rotor Speed ME": V2 * 1000,
                    "Force": F
                })
            X1, X2, V1, V2, F = func(end_timestamp)
            data.append({
                "time": end_timestamp,
                "Position FE": X1 * 1000,
                "Position ME": X2 * 1000,
                "Rotor Speed FE": V1 * 1000,
                "Rotor Speed ME": V2 * 1000,
                "Force": F
            })
            return pd.DataFrame(data)
        except Exception as e:
            logger.error(f"_get_data - Ошибка при получении данных с end_timestamp={end_timestamp}: {e}")
            return pd.DataFrame()
