"""
Адаптеры парсеров
"""
import pandas as pd
import numpy as np
import re
from typing import Dict

from core.ports import ParserPort
from adapters.pconfig import get_og_by_name, OG_IDS, replace_id_in_path, data_to_json, get_object_by_name

# Заглушка для get_object_by_name, если config.py не доступен
# try:
#     from pconfig import get_object_by_name
# except ImportError:
#     def get_object_by_name(name):
#         """Заглушка для получения ID по имени"""
#         if not name or not isinstance(name, str):
#             return None
#         return name.strip()


class SvodkaPMParser(ParserPort):
    """Парсер для сводок ПМ (план и факт)"""

    name = "Сводки ПМ"

    def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
        """Определения индекса заголовка в excel по ключевому слову"""
        # Читаем первые max_rows строк без заголовков
        df_temp = pd.read_excel(
            file,
            sheet_name=sheet,
            header=None,
            nrows=max_rows
        )

        # Ищем строку, где хотя бы в одном столбце встречается искомое значение
        for idx, row in df_temp.iterrows():
            if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
                print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
                return idx  # 0-based index — то, что нужно для header=

        raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")

    def parse_svodka_pm(self, file, sheet, header_num=None):
        ''' Собственно парсер отчетов одного ОГ для БП, ПП и факта '''
        # Автоопределение header_num, если не передан
        if header_num is None:
            header_num = self.find_header_row(file, sheet, search_value="Итого")

        # Читаем заголовки header_num и 1-2 строки данных, чтобы найти INDICATOR_ID
        df_probe = pd.read_excel(
            file,
            sheet_name=sheet,
            header=header_num,
            usecols=None,
            nrows=2,
        )

        if df_probe.shape[0] == 0:
            raise ValueError("Файл пуст или не содержит данных.")

        first_data_row = df_probe.iloc[0]

        # Находим столбец с 'INDICATOR_ID'
        indicator_cols = first_data_row[first_data_row == 'INDICATOR_ID']
        if len(indicator_cols) == 0:
            raise ValueError('Не найден столбец со значением "INDICATOR_ID" в первой строке данных.')

        indicator_col_name = indicator_cols.index[0]
        print(f"Найден INDICATOR_ID в столбце: {indicator_col_name}")

        # Читаем весь лист
        df_full = pd.read_excel(
            file,
            sheet_name=sheet,
            header=header_num,
            usecols=None,
            index_col=None
        )

        if indicator_col_name not in df_full.columns:
            raise ValueError(f"Столбец {indicator_col_name} отсутствует при полной загрузке.")

        # Перемещаем INDICATOR_ID в начало и делаем индексом
        cols = [indicator_col_name] + [col for col in df_full.columns if col != indicator_col_name]
        df_full = df_full[cols]
        df_full.set_index(indicator_col_name, inplace=True)

        # Обрезаем до "Итого" + 1
        header_list = [str(h).strip() for h in df_full.columns]
        try:
            itogo_idx = header_list.index("Итого")
            num_cols_needed = itogo_idx + 2
        except ValueError:
            print('Столбец "Итого" не найден. Оставляем все столбцы.')
            num_cols_needed = len(header_list)

        num_cols_needed = min(num_cols_needed, len(header_list))
        df_final = df_full.iloc[:, :num_cols_needed]

        # === Удаление полностью пустых столбцов ===
        df_clean = df_final.replace(r'^\s*$', pd.NA, regex=True)
        df_clean = df_clean.where(pd.notnull(df_clean), pd.NA)
        non_empty_mask = df_clean.notna().any()
        df_final = df_final.loc[:, non_empty_mask]

        # === Обработка заголовков: Unnamed и "Итого" → "Итого" ===
        new_columns = []
        last_good_name = None
        for col in df_final.columns:
            col_str = str(col).strip()

            # Проверяем, является ли колонка пустой/некорректной
            is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'

            # Проверяем, начинается ли на "Итого"
            if col_str.startswith('Итого'):
                current_name = 'Итого'
                last_good_name = current_name  # обновляем last_good_name
                new_columns.append(current_name)
            elif is_empty_or_unnamed:
                # Используем последнее хорошее имя
                new_columns.append(last_good_name)
            else:
                # Имя, полученное из exel
                last_good_name = col_str
                new_columns.append(col_str)

        df_final.columns = new_columns

        print(f"Окончательное количество столбцов: {len(df_final.columns)}")
        return df_final

    def parse(self, file_path: str, params: dict) -> dict:
        import zipfile
        pm_dict = {
            "facts": {},
            "plans": {}
        }
        excel_fact_template = 'svodka_fact_pm_ID.xlsm'
        excel_plan_template = 'svodka_plan_pm_ID.xlsx'
        with zipfile.ZipFile(file_path, 'r') as zip_ref:
            file_list = zip_ref.namelist()
            for name, id in OG_IDS.items():
                if id == 'BASH':
                    continue  # пропускаем BASH

                current_fact = replace_id_in_path(excel_fact_template, id)
                fact_candidates = [f for f in file_list if current_fact in f]
                if len(fact_candidates) == 1:
                    print(f'Загрузка {current_fact}')
                    with zip_ref.open(fact_candidates[0]) as excel_file:
                        pm_dict['facts'][id] = self.parse_svodka_pm(excel_file, 'Сводка Нефтепереработка')
                    print(f"✅ Факт загружен: {current_fact}")
                else:
                    print(f"⚠️ Файл не найден (Факт): {current_fact}")
                    pm_dict['facts'][id] = None

                current_plan = replace_id_in_path(excel_plan_template, id)
                plan_candidates = [f for f in file_list if current_plan in f]
                if len(plan_candidates) == 1:
                    print(f'Загрузка {current_plan}')
                    with zip_ref.open(plan_candidates[0]) as excel_file:
                        pm_dict['plans'][id] = self.parse_svodka_pm(excel_file, 'Сводка Нефтепереработка')
                    print(f"✅ План загружен: {current_plan}")
                else:
                    print(f"⚠️ Файл не найден (План): {current_plan}")
                    pm_dict['plans'][id] = None

        return pm_dict

    # def parse_old_sigle_sarnpz(self, file_path: str, params: dict) -> pd.DataFrame:
    #     """Парсинг файла сводки факта СарНПЗ"""
    #     sheet_name = params.get('sheet_name', 'Сводка Нефтепереработка')
    #     header_num = params.get('header_num')

    #     # Автоопределение header_num, если не передан
    #     if header_num is None:
    #         header_num = self.find_header_row(file_path, sheet_name, search_value="Итого")

    #     # Читаем заголовки header_num и 1-2 строки данных, чтобы найти INDICATOR_ID
    #     df_probe = pd.read_excel(
    #         file_path,
    #         sheet_name=sheet_name,
    #         header=header_num,
    #         usecols=None,
    #         nrows=2,
    #     )

    #     if df_probe.shape[0] == 0:
    #         raise ValueError("Файл пуст или не содержит данных.")

    #     first_data_row = df_probe.iloc[0]

    #     # Находим столбец с 'INDICATOR_ID'
    #     indicator_cols = first_data_row[first_data_row == 'INDICATOR_ID']
    #     if len(indicator_cols) == 0:
    #         raise ValueError('Не найден столбец со значением "INDICATOR_ID" в первой строке данных.')

    #     indicator_col_name = indicator_cols.index[0]
    #     print(f"Найден INDICATOR_ID в столбце: {indicator_col_name}")

    #     # Читаем весь лист
    #     df_full = pd.read_excel(
    #         file_path,
    #         sheet_name=sheet_name,
    #         header=header_num,
    #         usecols=None,
    #         index_col=None
    #     )

    #     if indicator_col_name not in df_full.columns:
    #         raise ValueError(f"Столбец {indicator_col_name} отсутствует при полной загрузке.")

    #     # Перемещаем INDICATOR_ID в начало и делаем индексом
    #     cols = [indicator_col_name] + [col for col in df_full.columns if col != indicator_col_name]
    #     df_full = df_full[cols]
    #     df_full.set_index(indicator_col_name, inplace=True)

    #     # Обрезаем до "Итого" + 1
    #     header_list = [str(h).strip() for h in df_full.columns]
    #     try:
    #         itogo_idx = header_list.index("Итого")
    #         num_cols_needed = itogo_idx + 2
    #     except ValueError:
    #         print('Столбец "Итого" не найден. Оставляем все столбцы.')
    #         num_cols_needed = len(header_list)

    #     num_cols_needed = min(num_cols_needed, len(header_list))
    #     df_final = df_full.iloc[:, :num_cols_needed]

    #     # === Удаление полностью пустых столбцов ===
    #     df_clean = df_final.replace(r'^\s*$', pd.NA, regex=True)
    #     df_clean = df_clean.where(pd.notnull(df_clean), pd.NA)
    #     non_empty_mask = df_clean.notna().any()
    #     df_final = df_final.loc[:, non_empty_mask]

    #     # === Обработка заголовков: Unnamed и "Итого" → "Итого" ===
    #     new_columns = []
    #     last_good_name = None
    #     for col in df_final.columns:
    #         col_str = str(col).strip()

    #         # Проверяем, является ли колонка пустой/некорректной
    #         is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'

    #         # Проверяем, начинается ли на "Итого"
    #         if col_str.startswith('Итого'):
    #             current_name = 'Итого'
    #             last_good_name = current_name  # обновляем last_good_name
    #             new_columns.append(current_name)
    #         elif is_empty_or_unnamed:
    #             # Используем последнее хорошее имя
    #             new_columns.append(last_good_name)
    #         else:
    #             # Имя, полученное из excel
    #             last_good_name = col_str
    #             new_columns.append(col_str)

    #     df_final.columns = new_columns

    #     print(f"Окончательное количество столбцов: {len(df_final.columns)}")
    #     return df_final

    def get_svodka_value(self, df_svodka, id, code, search_value=None):
        ''' Служебная функция для простой выборке по сводке '''
        row_index = id

        mask_value = df_svodka.iloc[0] == code
        if search_value is None:
            mask_name = df_svodka.columns != 'Итого'
        else:
            mask_name = df_svodka.columns == search_value

        # Убедимся, что маски совпадают по длине
        if len(mask_value) != len(mask_name):
            raise ValueError(
                f"Несоответствие длин масок: mask_value={len(mask_value)}, mask_name={len(mask_name)}"
            )

        final_mask = mask_value & mask_name  # булевая маска по позициям столбцов
        col_positions = final_mask.values   # numpy array или Series булевых значений

        if not final_mask.any():
            print(f"Нет столбцов с '{code}' в первой строке и именем, не начинающимся с '{search_value}'")
            return 0
        else:
            if row_index in df_svodka.index:
                # Получаем позицию строки
                row_loc = df_svodka.index.get_loc(row_index)

                # Извлекаем значения по позициям столбцов
                values = df_svodka.iloc[row_loc, col_positions]

                # Преобразуем в числовой формат
                numeric_values = pd.to_numeric(values, errors='coerce')

                # Агрегация данных (NaN игнорируются)
                if search_value is None:
                    return numeric_values
                else:
                    return numeric_values.iloc[0]
            else:
                return None

    def get_svodka_og(self, pm_dict, id, codes, columns, search_value=None):
        ''' Служебная функция получения данных по одному ОГ '''
        result = {}

        fact_df = pm_dict['facts'][id]
        plan_df = pm_dict['plans'][id]

        # Определяем, какие столбцы из какого датафрейма брать
        for col in columns:
            col_result = {}

            if col in ['ПП', 'БП']:
                if plan_df is None:
                    print(f"❌ Невозможно обработать '{col}': нет данных плана для {id}")
                else:
                    for code in codes:
                        val = self.get_svodka_value(plan_df, code, col, search_value)
                        col_result[code] = val

            elif col in ['ТБ', 'СЭБ', 'НЭБ']:
                if fact_df is None:
                    print(f"❌ Невозможно обработать '{col}': нет данных факта для {id}")
                else:
                    for code in codes:
                        val = self.get_svodka_value(fact_df, code, col, search_value)
                        col_result[code] = val
            else:
                print(f"⚠️ Неизвестный столбец: '{col}'. Пропускаем.")
                col_result = {code: None for code in codes}

            result[col] = col_result

        return result

    def get_svodka_total(self, pm_dict, codes, columns, search_value=None):
        ''' Служебная функция агрегации данные по всем ОГ '''
        total_result = {}

        for name, og_id in OG_IDS.items():
            if og_id == 'BASH':
                continue

            # print(f"📊 Обработка: {name} ({og_id})")
            try:
                data = self.get_svodka_og(
                    pm_dict,
                    og_id,
                    codes,
                    columns,
                    search_value
                )
                total_result[og_id] = data
            except Exception as e:
                print(f"❌ Ошибка при обработке {name} ({og_id}): {e}")
                total_result[og_id] = None

        return total_result

    def get_value(self, df, params):
        og_id = params.get("id")
        codes = params.get("codes")
        columns = params.get("columns")
        search = params.get("search")
        mode = params.get("mode", "total")
        if not isinstance(codes, list):
            raise ValueError("Поле 'codes' должно быть списком")
        if not isinstance(columns, list):
            raise ValueError("Поле 'columns' должно быть списком")
        data = None
        if mode == "single":
            if not og_id:
                raise ValueError("Отсутствует идентификатор ОГ")
            data = self.get_svodka_og(df, og_id, codes, columns, search)
        elif mode == "total":
            data = self.get_svodka_total(df, codes, columns, search)
        json_result = data_to_json(data)
        return json_result

    # def get_value_old(self, df: pd.DataFrame, params: dict):
    #     """Получение значения из сводки по ID и коду"""
    #     indicator_id = params.get('indicator_id')
    #     code = params.get('code')
    #     search_value = params.get('search_value')

    #     row_index = indicator_id

    #     mask_value = df.iloc[0] == code
    #     if search_value is None:
    #         mask_name = df.columns != 'Итого'
    #     else:
    #         mask_name = df.columns == search_value

    #     # Убедимся, что маски совпадают по длине
    #     if len(mask_value) != len(mask_name):
    #         raise ValueError(
    #             f"Несоответствие длин масок: mask_value={len(mask_value)}, mask_name={len(mask_name)}"
    #         )

    #     final_mask = mask_value & mask_name  # булевая маска по позициям столбцов
    #     col_positions = final_mask.values   # numpy array или Series булевых значений

    #     if not final_mask.any():
    #         print(f"Нет столбцов с '{code}' в первой строке и именем, не начинающимся с '{search_value}'")
    #         return 0
    #     else:
    #         if row_index in df.index:
    #             # Получаем позицию строки
    #             row_loc = df.index.get_loc(row_index)

    #             # Извлекаем значения по позициям столбцов
    #             values = df.iloc[row_loc, col_positions]

    #             # Преобразуем в числовой формат
    #             numeric_values = pd.to_numeric(values, errors='coerce')

    #             # Агрегация данных (NaN игнорируются)
    #             if search_value is None:
    #                 return numeric_values
    #             else:
    #                 return numeric_values.iloc[0]
    #         else:
    #             return None

    def get_schema(self) -> dict:
        """Возвращает схему входных параметров для парсера"""
        return {
            "parse_params": {
            },
            "get_params": {
                "id": {
                    "type": "string",
                    "description": "Идентификатор МА для запрашиваемого ОГ (игнорируется для get_total_ogs)",
                    "required": True,
                    "example": "SNPZ"
                },
                "codes": {
                    "type": "array[integer]",
                    "description": "Коды выбираемых строк",
                    "required": True,
                    "example": [78, 79]
                },
                "columns": {
                    "type": "array[string]",
                    "description": "Выбираемые столбцы",
                    "required": True,
                    "example": ["ПП", "СЭБ"]
                },
                "search": {
                    "type": "string",
                    "description": "Опциональное значение для выборки колонок, может быть только Итого или null",
                    "required": False,
                    "example": "Итого"
                }
            }
        }


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)

    def get_schema(self) -> dict:
        """Возвращает схему входных параметров для парсера"""
        return {
            "parse_params": {
            },
            "get_params": {
                "modes": {
                    "type": "array[string]",
                    "description": "plan, fact или normativ",
                    "required": True
                },
                "tables": {
                    "type": "array[string]",
                    "description": "Названия таблиц как есть",
                    "required": True
                }
            }
        }


class MonitoringFuelParser(ParserPort):
    """Парсер для мониторинга топлива"""

    name = "Мониторинг топлива"

    def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
        """Определение индекса заголовка в Excel по ключевому слову"""
        # Читаем первые max_rows строк без заголовков
        df_temp = pd.read_excel(
            file_path,
            sheet_name=sheet,
            header=None,
            nrows=max_rows
        )

        # Ищем строку, где хотя бы в одном столбце встречается искомое значение
        for idx, row in df_temp.iterrows():
            if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
                print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
                return idx + 1  # возвращаем индекс строки (0-based)

        raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")

    def parse_single(self, file, sheet, header_num=None):
        ''' Собственно парсер отчетов одного объекта'''
        # Автоопределение header_num, если не передан
        if header_num is None:
            header_num = self.find_header_row(file, sheet, search_value="Установка")
        # Читаем весь лист, начиная с найденной строки как заголовок
        df_full = pd.read_excel(
            file,
            sheet_name=sheet,
            header=header_num,
            usecols=None,
            index_col=None
        )

        # === Удаление полностью пустых столбцов ===
        df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True)  # заменяем пустые строки на NA
        df_clean = df_clean.dropna(axis=1, how='all')  # удаляем столбцы, где все значения — NA
        df_full = df_full.loc[:, df_clean.columns]  # оставляем только непустые столбцы

        # === Переименовываем нужные столбцы по позициям ===
        if len(df_full.columns) < 2:
            raise ValueError("DataFrame должен содержать как минимум 2 столбца.")

        new_columns = df_full.columns.tolist()

        new_columns[0] = 'name'
        new_columns[1] = 'normativ'
        new_columns[-2] = 'total'
        new_columns[-1] = 'total_1'

        df_full.columns = new_columns

        # Проверяем, что колонка 'name' существует
        if 'name' in df_full.columns:
            # Применяем функцию get_id_by_name к каждой строке в колонке 'name'
            df_full['id'] = df_full['name'].apply(get_object_by_name)

        # Устанавливаем id как индекс
        df_full.set_index('id', inplace=True)
        print(f"Окончательное количество столбцов: {len(df_full.columns)}")
        return df_full

    def parse(self, file_path: str, params: dict) -> dict:
        import zipfile
        df_monitorings = {}  # ЭТО СЛОВАРЬ ДАТАФРЕЙМОВ, ГДЕ КЛЮЧ - НОМЕР МЕСЯЦА, ЗНАЧЕНИЕ - ДАТАФРЕЙМ

        with zipfile.ZipFile(file_path, 'r') as zip_ref:

            file_list = zip_ref.namelist()
            for month in range(1, 13):

                mm = f"{month:02d}"
                file_temp = f'monitoring_SNPZ_{mm}.xlsm'
                candidates = [f for f in file_list if file_temp in f]

                if len(candidates) == 1:
                    file = candidates[0]

                    print(f'Загрузка {file}')
                    with zip_ref.open(file) as excel_file:
                        try:
                            df = self.parse_single(excel_file, 'Мониторинг потребления')
                            df_monitorings[mm] = df

                            print(f"✅ Данные за месяц {mm} загружены")

                        except Exception as e:
                            print(f"Ошибка при загрузке файла {file_temp}: {e}")

                else:
                    print(f"⚠️ Файл не найден: {file_temp}")

        return df_monitorings

    # def parse_old(self, file_path: str, params: dict) -> pd.DataFrame:
    #     """Парсинг файла или директории мониторинга топлива"""
    #     import os

    #     # Проверяем параметр directory_path
    #     directory_path = params.get('directory_path')
    #     if directory_path and os.path.isdir(directory_path):
    #         return self._parse_directory(directory_path, params)
    #     else:
    #         return self._parse_single_file(file_path, params)

    # def _parse_single_file(self, file_path: str, params: dict) -> pd.DataFrame:
    #     """Парсинг одного файла мониторинга топлива"""
    #     sheet = params.get('sheet_name', 'Мониторинг потребления')
    #     header_num = params.get('header_num', None)
    #     search_value = params.get('search_value', 'Установка')

    #     # Автоопределение header_num, если не передан
    #     if header_num is None:
    #         header_num = self.find_header_row(file_path, sheet, search_value)

    #     # Читаем весь лист, начиная с найденной строки как заголовок
    #     df_full = pd.read_excel(
    #         file_path,
    #         sheet_name=sheet,
    #         header=header_num,
    #         usecols=None,
    #         index_col=None
    #     )

    #     # Удаление полностью пустых столбцов
    #     df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True)  # заменяем пустые строки на NA
    #     df_clean = df_clean.dropna(axis=1, how='all')  # удаляем столбцы, где все значения — NA
    #     df_full = df_full.loc[:, df_clean.columns]  # оставляем только непустые столбцы

    #     # Переименовываем нужные столбцы по позициям
    #     if len(df_full.columns) < 2:
    #         raise ValueError("DataFrame должен содержать как минимум 2 столбца.")

    #     new_columns = df_full.columns.tolist()

    #     # Первая колонка -> 'name'
    #     new_columns[0] = 'name'

    #     # Вторая колонка -> 'normativ'
    #     new_columns[1] = 'normativ'

    #     # Предпоследняя колонка -> 'total'
    #     if len(new_columns) >= 2:
    #         new_columns[-2] = 'total'

    #     # Последняя колонка -> 'total_svod'
    #     new_columns[-1] = 'total_svod'

    #     df_full.columns = new_columns

    #     # Проверяем, что колонка 'name' существует
    #     if 'name' in df_full.columns:
    #         # Применяем функцию get_object_by_name к каждой строке в колонке 'name'
    #         df_full['id'] = df_full['name'].apply(get_object_by_name)

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

    #     print(f"Окончательное количество столбцов: {len(df_full.columns)}")
    #     return df_full

    # def _parse_directory(self, directory_path: str, params: dict) -> Dict[str, pd.DataFrame]:
    #     """Парсинг всех файлов из директории"""
    #     import os
    #     import glob

    #     # Паттерн для поиска файлов (как в примере)
    #     file_pattern = os.path.join(directory_path, "monitoring_SNPZ_*.xlsm")
    #     files = glob.glob(file_pattern)

    #     if not files:
    #         raise ValueError(f"Не найдены файлы по паттерну {file_pattern}")

    #     df_dict = {}

    #     for file_path in sorted(files):
    #         try:
    #             print(f"Обрабатываем файл: {os.path.basename(file_path)}")
    #             df = self._parse_single_file(file_path, params)
    #             df_dict[file_path] = df
    #         except Exception as e:
    #             print(f"Ошибка при обработке файла {file_path}: {e}")
    #             continue

    #     if not df_dict:
    #         raise ValueError("Не удалось обработать ни одного файла из директории")

    #     print(f"Успешно обработано {len(df_dict)} файлов")
    #     return df_dict

    def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns):
        ''' Служебная функция. Агрегация данных по среднему по определенным колонкам. '''
        all_data = {}  # Для хранения полных данных (месяцы) по каждой колонке
        means = {}     # Для хранения средних

        for col in columns:
            totals = []  # Список Series (по месяцам) для текущей колонки

            for file_key, df in df_dict.items():
                if col not in df.columns:
                    print(f"Колонка '{col}' не найдена в {file_key}, пропускаем.")
                    continue

                # Берём колонку, оставляем id как индекс
                series = df[col].copy()

                # Определяем имя месяца: извлекаем номер из ключа (например, '03' из '03')
                # Если ключ уже '03', '04' и т.п. — используем как есть
                match = re.search(r'\d{2}', str(file_key))
                month = match.group() if match else file_key
                series.name = month
                totals.append(series)

            if not totals:
                raise ValueError(f"Ни один DataFrame не содержит колонку '{col}'")

            # Объединяем все Series в один DataFrame (id × месяцы)
            df_combined = pd.concat(totals, axis=1)
            all_data[col] = df_combined  # Сохраняем

            # Считаем среднее по строкам (по месяцам), игнорируя NaN
            mean_series = df_combined.mean(axis=1)
            mean_series = mean_series.dropna()  # Убираем id, где нет данных вообще
            means[col] = mean_series

        # Собираем все средние в один DataFrame
        df_means = pd.DataFrame(means)

        return df_means, all_data

    def get_month(self, df_monitorings, month_key):
        ''' Служебная функция. Выгрузить только конкретный месяц '''
        if month_key not in df_monitorings:
            raise KeyError(f"Месяц '{month_key}' не найден в df_monitorings. Доступные: {list(df_monitorings.keys())}")

        df = df_monitorings[month_key]

        # Создаём копию, чтобы не изменять оригинальный словарь
        result_df = df.copy()

        # Удаляем колонку 'name', если она существует
        if 'name' in result_df.columns:
            result_df = result_df.drop(columns=['name'])

        return result_df

    def aggregate_total_by_id(self, df_dict: Dict[str, pd.DataFrame], column: str):
        """Агрегация данных по ID"""
        totals = []

        for file, df in df_dict.items():
            if column not in df.columns:
                print(f"Колонка '{column}' не найдена в {file}, пропускаем.")
                continue

            # Берём колонку и сохраняем как Series с именем месяца
            series = df[column].copy()
            series.name = re.sub(r'\D', '', file)  # Имя Series будет использовано как имя колонки в concat
            totals.append(series)

        if not totals:
            raise ValueError(f"Ни один DataFrame не содержит колонку '{column}'")

        df_combined = pd.concat(totals, axis=1)

        # Считаем среднее по строкам (по месяцам)
        total = df_combined.mean(axis=1)
        total = total.dropna()

        total.name = 'mean'

        return total, df_combined

    def get_value(self, df, params):
        mode = params.get("mode", "total")
        columns = params.get("columns", None)
        month = params.get("month", None)
        data = None
        if mode == "total":
            if not columns:
                raise ValueError("Отсутствуют идентификаторы столбцов")
            df_means, _ = self.aggregate_by_columns(df, columns)
            data = df_means.to_dict(orient='index')
        elif mode == "month":
            if not month:
                raise ValueError("Отсутствуют идентификатор месяца")
            df_month = self.get_month(df, month)
            data = df_month.to_dict(orient='index')

        json_result = data_to_json(data)
        return json_result

    # def get_value_old(self, df, params: dict):
    #     """Получение агрегированного значения по параметрам"""
    #     column = params.get('column', 'normativ')

    #     # Если df - это словарь DataFrames (результат парсинга директории)
    #     if isinstance(df, dict):
    #         total, details = self.aggregate_total_by_id(df, column)
    #         return {
    #             'mean': total.to_dict(),
    #             'details': details.to_dict() if hasattr(details, 'to_dict') else str(details)
    #         }

    #     # Если df - это один DataFrame, возвращаем значение из указанной колонки
    #     if isinstance(df, pd.DataFrame) and column in df.columns:
    #         return df[column].to_dict()

    #     # Если df - None или пустой
    #     if df is None:
    #         return None

    #     return {"error": f"Неожиданный тип данных: {type(df)}"}

    def get_schema(self) -> dict:
        """Возвращает схему входных параметров для парсера"""
        return {
            "parse_params": {
            },
            "get_total_by_columns_params": {
                "columns": {
                    "type": "array[string]",
                    "description": "Название колонок для агрегации (normativ, total)",
                    "required": True
                }
            },
            "get_month_by_code_params": {
                "month": {
                    "type": "string",
                    "description": "Номер месяца строкой с ведущим 0",
                    "required": True
                }
            },
        }
