import pandas as pd
import numpy as np

from core.ports import ParserPort
from adapters.pconfig import get_og_by_name


class SvodkaCAParser(ParserPort):
    """Парсер для сводки СА"""

    name = "Сводка СА"

    def extract_all_tables(self, file_path, sheet_name=0):
        """Извлекает все таблицы из Excel файла"""
        df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
        df_filled = df.fillna('')
        df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)

        non_empty_rows = ~(df_clean.eq('').all(axis=1))
        non_empty_cols = ~(df_clean.eq('').all(axis=0))

        row_indices = non_empty_rows[non_empty_rows].index.tolist()
        col_indices = non_empty_cols[non_empty_cols].index.tolist()

        if not row_indices or not col_indices:
            return []

        row_blocks = self._get_contiguous_blocks(row_indices)
        col_blocks = self._get_contiguous_blocks(col_indices)

        tables = []
        for r_start, r_end in row_blocks:
            for c_start, c_end in col_blocks:
                block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
                if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
                    continue

                if self._is_header_row(block.iloc[0]):
                    block.columns = block.iloc[0]
                    block = block.iloc[1:].reset_index(drop=True)
                else:
                    block = block.reset_index(drop=True)
                    block.columns = [f"col_{i}" for i in range(block.shape[1])]

                tables.append(block)

        return tables

    def _get_contiguous_blocks(self, indices):
        """Группирует индексы в непрерывные блоки"""
        if not indices:
            return []
        blocks = []
        start = indices[0]
        for i in range(1, len(indices)):
            if indices[i] != indices[i-1] + 1:
                blocks.append((start, indices[i-1]))
                start = indices[i]
        blocks.append((start, indices[-1]))
        return blocks

    def _is_header_row(self, series):
        """Определяет, похожа ли строка на заголовок"""
        series_str = series.astype(str).str.strip()
        non_empty = series_str[series_str != '']
        if len(non_empty) == 0:
            return False

        def is_not_numeric(val):
            try:
                float(val.replace(',', '.'))
                return False
            except (ValueError, TypeError):
                return True

        not_numeric_count = non_empty.apply(is_not_numeric).sum()
        return not_numeric_count / len(non_empty) > 0.6

    def _get_og_by_name(self, name):
        """Функция для получения ID по имени (упрощенная версия)"""
        # Упрощенная версия - возвращаем имя как есть
        if not name or not isinstance(name, str):
            return None
        return name.strip()

    def parse_sheet(self, file_path, sheet_name, inclusion_list):
        """Собственно функция парсинга отчета СА"""
        # === Извлечение и фильтрация ===
        tables = self.extract_all_tables(file_path, sheet_name)

        # Фильтруем таблицы: оставляем только те, где первая строка содержит нужные заголовки
        filtered_tables = []
        for table in tables:
            if table.empty:
                continue
            first_row_values = table.iloc[0].astype(str).str.strip().tolist()
            if any(val in inclusion_list for val in first_row_values):
                filtered_tables.append(table)

        tables = filtered_tables

        # === Итоговый список таблиц датафреймов ===
        result_list = []

        for table in tables:
            if table.empty:
                continue

            # Получаем первую строку (до удаления)
            first_row_values = table.iloc[0].astype(str).str.strip().tolist()

            # Находим, какой элемент из inclusion_list присутствует
            matched_key = None
            for val in first_row_values:
                if val in inclusion_list:
                    matched_key = val
                    break  # берём первый совпадающий заголовок

            if matched_key is None:
                continue  # на всякий случай (хотя уже отфильтровано)

            # Удаляем первую строку (заголовок) и сбрасываем индекс
            df_cleaned = table.iloc[1:].copy().reset_index(drop=True)

            # Пропускаем, если таблица пустая
            if df_cleaned.empty:
                continue

            # Первая строка становится заголовком
            new_header = df_cleaned.iloc[0]  # извлекаем первую строку как потенциальные названия столбцов

            # Преобразуем заголовок: только первый столбец может быть заменён на "name"
            cleaned_header = []

            # Обрабатываем первый столбец отдельно
            first_item = new_header.iloc[0] if isinstance(new_header, pd.Series) else new_header[0]
            first_item_str = str(first_item).strip() if pd.notna(first_item) else ""
            if first_item_str == "" or first_item_str == "nan":
                cleaned_header.append("name")
            else:
                cleaned_header.append(first_item_str)

            # Остальные столбцы добавляем без изменений (или с минимальной очисткой)
            for item in new_header[1:]:
                # Опционально: приводим к строке и убираем лишние пробелы, но не заменяем на "name"
                item_str = str(item).strip() if pd.notna(item) else ""
                cleaned_header.append(item_str)

            # Применяем очищенные названия столбцов
            df_cleaned = df_cleaned[1:]  # удаляем строку с заголовком
            df_cleaned.columns = cleaned_header
            df_cleaned = df_cleaned.reset_index(drop=True)

            if matched_key.endswith('**'):
                cleaned_key = matched_key[:-2]  # удаляем последние **
            else:
                cleaned_key = matched_key

            # Добавляем новую колонку с именем параметра
            df_cleaned["table"] = cleaned_key

            # Проверяем, что колонка 'name' существует
            if 'name' not in df_cleaned.columns:
                print(
                    f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
                continue  # или обработать по-другому
            else:
                # Применяем функцию get_id_by_name к каждой строке в колонке 'name'
                df_cleaned['id'] = df_cleaned['name'].apply(get_og_by_name)

            # Удаляем строки, где id — None, NaN или пустой
            df_cleaned = df_cleaned.dropna(subset=['id'])  # dropna удаляет NaN
            # Дополнительно: удаляем None (если не поймал dropna)
            df_cleaned = df_cleaned[df_cleaned['id'].notna() & (df_cleaned['id'].astype(str) != 'None')]

            # Добавляем в словарь
            result_list.append(df_cleaned)

        # === Объединение и сортировка по id (индекс) и table ===
        if result_list:
            combined_df = pd.concat(result_list, axis=0)

            # Сортируем по индексу (id) и по столбцу 'table'
            combined_df = combined_df.sort_values(by=['id', 'table'], axis=0)

            # Устанавливаем id как индекс
            # combined_df.set_index('id', inplace=True)

            return combined_df
        else:
            return None

    def parse(self, file_path: str, params: dict) -> dict:
        """Парсинг файла сводки СА"""
        # === Точка входа. Нужно выгрузить три таблицы: План, Факт и Норматив ===
        # Выгружаем План в df_ca_plan
        inclusion_list_plan = {
            "ТиП, %",
            "Топливо итого, тонн",
            "Топливо итого, %",
            "Топливо на технологию, тонн",
            "Топливо на технологию, %",
            "Топливо на энергетику, тонн",
            "Топливо на энергетику, %",
            "Потери итого, тонн",
            "Потери итого, %",
            "в т.ч. Идентифицированные безвозвратные потери, тонн**",
            "в т.ч. Идентифицированные безвозвратные потери, %**",
            "в т.ч. Неидентифицированные потери, тонн**",
            "в т.ч. Неидентифицированные потери, %**"
        }

        df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan)  # ЭТО ДАТАФРЕЙМ ПЛАНА В СВОДКЕ ЦА
        print(f"\n--- Объединённый и отсортированный План: {df_ca_plan.shape} ---")

        # Выгружаем Факт
        inclusion_list_fact = {
            "ТиП, %",
            "Топливо итого, тонн",
            "Топливо итого, %",
            "Топливо на технологию, тонн",
            "Топливо на технологию, %",
            "Топливо на энергетику, тонн",
            "Топливо на энергетику, %",
            "Потери итого, тонн",
            "Потери итого, %",
            "в т.ч. Идентифицированные безвозвратные потери, тонн",
            "в т.ч. Идентифицированные безвозвратные потери, %",
            "в т.ч. Неидентифицированные потери, тонн",
            "в т.ч. Неидентифицированные потери, %"
        }

        df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact)  # ЭТО ДАТАФРЕЙМ ФАКТА В СВОДКЕ ЦА
        print(f"\n--- Объединённый и отсортированный Факт: {df_ca_fact.shape} ---")

        # Выгружаем План в df_ca_normativ
        inclusion_list_normativ = {
            "Топливо итого, тонн",
            "Топливо итого, %",
            "Топливо на технологию, тонн",
            "Топливо на технологию, %",
            "Топливо на энергетику, тонн",
            "Топливо на энергетику, %",
            "Потери итого, тонн",
            "Потери итого, %",
            "в т.ч. Идентифицированные безвозвратные потери, тонн**",
            "в т.ч. Идентифицированные безвозвратные потери, %**",
            "в т.ч. Неидентифицированные потери, тонн**",
            "в т.ч. Неидентифицированные потери, %**"
        }

        # ЭТО ДАТАФРЕЙМ НОРМАТИВА В СВОДКЕ ЦА
        df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)

        print(f"\n--- Объединённый и отсортированный Норматив: {df_ca_normativ.shape} ---")

        df_dict = {
            "plan": df_ca_plan,
            "fact": df_ca_fact,
            "normativ": df_ca_normativ
        }
        return df_dict

    def data_dict_to_json(self, data_dict):
        ''' Служебная функция для парсинга словаря в json. '''
        def convert_types(obj):
            if isinstance(obj, (np.integer, np.int64)):
                return int(obj)
            elif isinstance(obj, (np.floating, np.float64)):
                return float(obj) if not np.isnan(obj) else None
            elif isinstance(obj, (np.ndarray,)):
                return obj.tolist()
            elif pd.isna(obj):
                return None
            elif isinstance(obj, (pd.Timestamp, np.datetime64)):
                return obj.isoformat()
            else:
                return obj

        # Новый словарь для JSON
        serializable_dict = {}

        for source, table_dict in data_dict.items():  # source: 'plan', 'fact', 'normativ'
            serializable_dict[source] = {}
            for table_name, df in table_dict.items():  # table_name: 'ТиП, %' и т.п., df: DataFrame
                if isinstance(df, pd.DataFrame):
                    records = df.to_dict(orient='records')
                    cleaned_records = []
                    for record in records:
                        cleaned_record = {
                            str(k): convert_types(v) for k, v in record.items()
                        }
                        cleaned_records.append(cleaned_record)
                    serializable_dict[source][table_name] = cleaned_records
                else:
                    # На всякий случай, если попался не DataFrame
                    serializable_dict[source][table_name] = None

        return serializable_dict

    def get_data(self, df_dict, df_type, table_values):
        ''' Служебная функция. Выборка данных из датафрейма. '''
        df = df_dict.get(df_type, {})

        if 'table' not in df.columns:
            raise KeyError("В датафрейме отсутствует колонка 'table'")

        filtered_df = df[df['table'].isin(table_values)].copy()
        result_dict = {key: group for key, group in filtered_df.groupby('table')}
        return result_dict

    def get_value(self, df: pd.DataFrame, params: dict):

        modes = params.get("modes")
        tables = params.get("tables")
        if not isinstance(modes, list):
            raise ValueError("Поле 'modes' должно быть списком")
        if not isinstance(tables, list):
            raise ValueError("Поле 'tables' должно быть списком")
        # Собираем данные
        data_dict = {}
        for mode in modes:
            data_dict[mode] = self.get_data(df, mode, tables)
        return self.data_dict_to_json(data_dict)
