import pandas as pd
import numpy as np
import logging

from core.ports import ParserPort
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
from app.schemas.svodka_ca import SvodkaCARequest
from adapters.pconfig import get_og_by_name

# Настройка логгера для модуля
logger = logging.getLogger(__name__)


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

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

    def _register_default_getters(self):
        """Регистрация геттеров по умолчанию"""
        # Используем схемы Pydantic как единый источник правды
        register_getter_from_schema(
            parser_instance=self,
            getter_name="get_ca_data",
            method=self._get_data_wrapper,
            schema_class=SvodkaCARequest,
            description="Получение данных по режимам и таблицам"
        )

    def determine_getter(self, get_params: dict) -> str:
        """Определение геттера для сводки СА"""
        # Для svodka_ca определяем режим из данных или используем 'fact' по умолчанию
        if hasattr(self, 'df') and self.df is not None and not self.df.empty:
            modes_in_df = self.df['mode'].unique() if 'mode' in self.df.columns else ['fact']
            # Используем первый найденный режим или 'fact' по умолчанию
            default_mode = modes_in_df[0] if len(modes_in_df) > 0 else 'fact'
        else:
            default_mode = 'fact'
        
        # Устанавливаем режим в параметры, если он не указан
        if 'mode' not in get_params:
            get_params['mode'] = default_mode
        
        # Для svodka_ca всегда используем геттер get_ca_data
        return 'get_ca_data'

    def _get_data_wrapper(self, params: dict):
        """Получение данных по режимам и таблицам"""
        logger.debug(f"🔍  _get_data_wrapper вызван с параметрами: {params}")
        
        # Валидируем параметры с помощью схемы Pydantic
        validated_params = validate_params_with_schema(params, SvodkaCARequest)
        
        modes = validated_params["modes"]
        tables = validated_params["tables"]
        
        logger.debug(f"🔍  Запрошенные режимы: {modes}")
        logger.debug(f"🔍  Запрошенные таблицы: {tables}")
        
        # Проверяем, есть ли данные в data_dict (из парсинга) или в df (из загрузки)
        if hasattr(self, 'data_dict') and self.data_dict is not None:
            # Данные из парсинга
            data_source = self.data_dict
            logger.debug(f"🔍  Используем data_dict с режимами: {list(data_source.keys())}")
        elif hasattr(self, 'df') and self.df is not None and not self.df.empty:
            # Данные из загрузки - преобразуем DataFrame обратно в словарь
            data_source = self._df_to_data_dict()
            logger.debug(f"🔍  Используем df, преобразованный в data_dict с режимами: {list(data_source.keys())}")
        else:
            logger.warning(f"🔍  Нет данных! data_dict={getattr(self, 'data_dict', 'None')}, df={getattr(self, 'df', 'None')}")
            return {}
        
        # Фильтруем данные по запрошенным режимам и таблицам
        result_data = {}
        for mode in modes:
            if mode in data_source:
                result_data[mode] = {}
                available_tables = list(data_source[mode].keys())
                logger.debug(f"🔍  Режим '{mode}' содержит таблицы: {available_tables}")
                for table_name, table_data in data_source[mode].items():
                    # Ищем таблицы по частичному совпадению
                    for requested_table in tables:
                        if requested_table in table_name:
                            result_data[mode][table_name] = table_data
                            logger.debug(f"🔍  Добавлена таблица '{table_name}' (совпадение с '{requested_table}') с {len(table_data)} записями")
                            break  # Найдено совпадение, переходим к следующей таблице
            else:
                logger.warning(f"🔍  Режим '{mode}' не найден в data_source")
        
        logger.debug(f"🔍  Итоговый результат содержит режимы: {list(result_data.keys())}")
        return result_data

    def _df_to_data_dict(self):
        """Преобразование DataFrame обратно в словарь данных"""
        if not hasattr(self, 'df') or self.df is None or self.df.empty:
            return {}
        
        data_dict = {}
        
        # Группируем данные по режимам и таблицам
        for _, row in self.df.iterrows():
            mode = row.get('mode')
            table = row.get('table')
            data = row.get('data')
            
            if mode and table and data is not None:
                if mode not in data_dict:
                    data_dict[mode] = {}
                data_dict[mode][table] = data
        
        return data_dict

    def parse(self, file_path: str, params: dict) -> pd.DataFrame:
        """Парсинг файла и возврат DataFrame"""
        logger.debug(f"🔍  SvodkaCAParser.parse вызван с файлом: {file_path}")
        
        # Парсим данные и сохраняем словарь для использования в геттерах
        self.data_dict = self.parse_svodka_ca(file_path, params)
        
        # Преобразуем словарь в DataFrame для совместимости с services.py
        # Создаем простой DataFrame с информацией о загруженных данных
        if self.data_dict:
            # Создаем DataFrame с информацией о режимах и таблицах
            data_rows = []
            for mode, tables in self.data_dict.items():
                for table_name, table_data in tables.items():
                    if table_data:
                        data_rows.append({
                            'mode': mode,
                            'table': table_name,
                            'rows_count': len(table_data),
                            'data': table_data
                        })
            
            if data_rows:
                df = pd.DataFrame(data_rows)
                self.df = df
                logger.debug(f"🔍  Создан DataFrame с {len(data_rows)} записями")
                return df
        
        # Если данных нет, возвращаем пустой DataFrame
        self.df = pd.DataFrame()
        logger.debug(f"🔍  Возвращаем пустой DataFrame")
        return self.df

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

        df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan)
        logger.debug(f"🔍  Объединённый и отсортированный План: {df_ca_plan.shape if df_ca_plan is not None else 'None'}")

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

        df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact)
        logger.debug(f"🔍  Объединённый и отсортированный Факт: {df_ca_fact.shape if df_ca_fact is not None else 'None'}")

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

        df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
        logger.debug(f"🔍  Объединённый и отсортированный Норматив: {df_ca_normativ.shape if df_ca_normativ is not None else 'None'}")

        # Преобразуем DataFrame в словарь по режимам и таблицам
        data_dict = {}
        
        # Обрабатываем План
        if df_ca_plan is not None and not df_ca_plan.empty:
            data_dict['plan'] = {}
            for table_name, group_df in df_ca_plan.groupby('table'):
                table_data = group_df.drop('table', axis=1)
                data_dict['plan'][table_name] = table_data.to_dict('records')
        
        # Обрабатываем Факт
        if df_ca_fact is not None and not df_ca_fact.empty:
            data_dict['fact'] = {}
            for table_name, group_df in df_ca_fact.groupby('table'):
                table_data = group_df.drop('table', axis=1)
                data_dict['fact'][table_name] = table_data.to_dict('records')
        
        # Обрабатываем Норматив
        if df_ca_normativ is not None and not df_ca_normativ.empty:
            data_dict['normativ'] = {}
            for table_name, group_df in df_ca_normativ.groupby('table'):
                table_data = group_df.drop('table', axis=1)
                data_dict['normativ'][table_name] = table_data.to_dict('records')
        
        logger.debug(f"🔍  Итоговый data_dict содержит режимы: {list(data_dict.keys())}")
        for mode, tables in data_dict.items():
            logger.debug(f"🔍  Режим '{mode}' содержит таблицы: {list(tables.keys())}")
        
        return data_dict

    def extract_all_tables(self, file_path, sheet_name=0):
        """Извлечение всех таблиц из Excel файла"""
        df = pd.read_excel(file_path, sheet_name=sheet_name, header=None, engine='openpyxl')
        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: str, sheet_name: str, inclusion_list: set) -> pd.DataFrame:
        """Парсинг листа Excel"""
        # === Извлечение и фильтрация ===
        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:
                logger.debug(
                    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 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
