

import pandas as pd
import os
import json
import zipfile
import tempfile
import shutil
import logging
from typing import Dict, Any, List, Optional
from core.ports import ParserPort
from adapters.pconfig import SINGLE_OGS, replace_id_in_path, find_header_row, data_to_json

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


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

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

    def __init__(self):
        super().__init__()
        self._register_default_getters()

    def _register_default_getters(self):
        """Регистрация геттеров для Сводки ПМ"""
        self.register_getter(
            name="single_og",
            method=self._get_single_og,
            required_params=["id", "codes", "columns"],
            optional_params=["search"],
            description="Получение данных по одному ОГ из сводки ПМ"
        )
        
        self.register_getter(
            name="total_ogs", 
            method=self._get_total_ogs,
            required_params=["codes", "columns"],
            optional_params=["search"],
            description="Получение данных по всем ОГ из сводки ПМ"
        )

    def determine_getter(self, get_params: dict) -> str:
        """Определение геттера для сводки ПМ"""
        # Для svodka_pm определяем геттер из параметра mode
        getter_name = get_params.pop("mode", None)
        if not getter_name:
            # Если режим не указан, берем первый доступный
            available_getters = list(self.getters.keys())
            if available_getters:
                getter_name = available_getters[0]
                logger.warning(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
            else:
                raise ValueError("Парсер не имеет доступных геттеров")
        
        return getter_name

    def parse(self, file_path: str, params: dict) -> Dict[str, pd.DataFrame]:
        """Парсинг ZIP архива со сводками ПМ и возврат словаря с DataFrame"""
        # Проверяем расширение файла
        if not file_path.lower().endswith('.zip'):
            raise ValueError(f"Ожидается ZIP архив: {file_path}")
        
        # Создаем временную директорию для разархивирования
        temp_dir = tempfile.mkdtemp()
        
        try:
            # Разархивируем файл
            with zipfile.ZipFile(file_path, 'r') as zip_ref:
                zip_ref.extractall(temp_dir)
                logger.info(f"📦 Архив разархивирован в: {temp_dir}")
            
            # Посмотрим, что находится в архиве
            logger.debug(f"🔍 Содержимое архива:")
            for root, dirs, files in os.walk(temp_dir):
                level = root.replace(temp_dir, '').count(os.sep)
                indent = ' ' * 2 * level
                logger.debug(f"{indent}{os.path.basename(root)}/")
                subindent = ' ' * 2 * (level + 1)
                for file in files:
                    logger.debug(f"{subindent}{file}")
            
            # Создаем словари для хранения данных как в оригинале
            df_pm_facts = {}  # Словарь с данными факта, ключ - ID ОГ
            df_pm_plans = {}  # Словарь с данными плана, ключ - ID ОГ
            
            # Ищем файлы в архиве (адаптируемся к реальной структуре)
            fact_files = []
            plan_files = []
            
            for root, dirs, files in os.walk(temp_dir):
                for file in files:
                    if file.lower().endswith(('.xlsx', '.xlsm')):
                        full_path = os.path.join(root, file)
                        if 'fact' in file.lower() or 'факт' in file.lower():
                            fact_files.append(full_path)
                        elif 'plan' in file.lower() or 'план' in file.lower():
                            plan_files.append(full_path)
            
            logger.info(f"📊 Найдено файлов факта: {len(fact_files)}")
            logger.info(f"📊 Найдено файлов плана: {len(plan_files)}")
            
            # Обрабатываем найденные файлы
            for fact_file in fact_files:
                # Извлекаем ID ОГ из имени файла
                filename = os.path.basename(fact_file)
                # Ищем паттерн типа svodka_fact_pm_SNPZ.xlsm
                if 'svodka_fact_pm_' in filename:
                    og_id = filename.replace('svodka_fact_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
                    if og_id in SINGLE_OGS:
                        logger.info(f'📊 Загрузка факта: {fact_file} (ОГ: {og_id})')
                        df_pm_facts[og_id] = self._parse_svodka_pm(fact_file, 'Сводка Нефтепереработка')
                        logger.info(f"✅ Факт загружен для {og_id}")
            
            for plan_file in plan_files:
                # Извлекаем ID ОГ из имени файла
                filename = os.path.basename(plan_file)
                # Ищем паттерн типа svodka_plan_pm_SNPZ.xlsm
                if 'svodka_plan_pm_' in filename:
                    og_id = filename.replace('svodka_plan_pm_', '').replace('.xlsx', '').replace('.xlsm', '')
                    if og_id in SINGLE_OGS:
                        logger.info(f'📊 Загрузка плана: {plan_file} (ОГ: {og_id})')
                        df_pm_plans[og_id] = self._parse_svodka_pm(plan_file, 'Сводка Нефтепереработка')
                        logger.info(f"✅ План загружен для {og_id}")
            
            # Инициализируем None для ОГ, для которых файлы не найдены
            for og_id in SINGLE_OGS:
                if og_id == 'BASH':
                    continue
                if og_id not in df_pm_facts:
                    df_pm_facts[og_id] = None
                if og_id not in df_pm_plans:
                    df_pm_plans[og_id] = None
            

            
            # Возвращаем словарь с данными (как в оригинале)
            result = {
                'df_pm_facts': df_pm_facts,
                'df_pm_plans': df_pm_plans
            }
            
            logger.info(f"🎯 Обработано ОГ: {len([k for k, v in df_pm_facts.items() if v is not None])} факт, {len([k for k, v in df_pm_plans.items() if v is not None])} план")
            
            return result
            
        finally:
            # Удаляем временную директорию
            shutil.rmtree(temp_dir, ignore_errors=True)
            logger.debug(f"🗑️ Временная директория удалена: {temp_dir}")

    def _parse_svodka_pm(self, file_path: str, sheet_name: str, header_num: Optional[int] = None) -> pd.DataFrame:
        """Парсинг отчетов одного ОГ для БП, ПП и факта"""
        try:
            # Автоопределение header_num, если не передан
            if header_num is None:
                header_num = 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,
                engine='openpyxl'  # Явно указываем движок
            )
        except Exception as e:
            raise ValueError(f"Ошибка при чтении файла {file_path}: {str(e)}")

        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]

        # Читаем весь лист
        df_full = pd.read_excel(
            file_path,
            sheet_name=sheet_name,
            header=header_num,
            usecols=None,
            index_col=None,
            engine='openpyxl'  # Явно указываем движок
        )

        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:
            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
                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

        return df_final

    def _get_svodka_value(self, df_svodka: pd.DataFrame, og_id: str, code: int, search_value: Optional[str] = None):
        """Служебная функция для простой выборке по сводке"""
        logger.debug(f"🔍 Ищем код '{code}' для ОГ '{og_id}' в DataFrame с {len(df_svodka)} строками")
        logger.debug(f"🔍  Первая строка данных: {df_svodka.iloc[0].tolist()}")
        logger.debug(f"🔍  Доступные индексы: {list(df_svodka.index)}")
        logger.debug(f"🔍  Доступные столбцы: {list(df_svodka.columns)}")

        # Проверяем, есть ли код в индексе
        if code not in df_svodka.index:
            logger.warning(f"⚠️ Код '{code}' не найден в индексе")
            return 0

        # Получаем позицию строки с кодом
        code_row_loc = df_svodka.index.get_loc(code)
        logger.debug(f"🔍  Код '{code}' в позиции {code_row_loc}")

        # Определяем позиции для поиска
        if search_value is None:
            # Ищем все позиции кроме "Итого" и None (первый столбец с заголовком)
            target_positions = []
            for i, col_name in enumerate(df_svodka.iloc[0]):
                if col_name != 'Итого' and col_name is not None:
                    target_positions.append(i)
        else:
            # Ищем позиции в первой строке, где есть нужное название
            target_positions = []
            for i, col_name in enumerate(df_svodka.iloc[0]):
                if col_name == search_value:
                    target_positions.append(i)
            
            logger.debug(f"🔍  Найдены позиции для '{search_value}': {target_positions[:5]}...")
            logger.debug(f"🔍  Позиции в первой строке: {target_positions[:5]}...")

        logger.debug(f"🔍  Ищем столбцы с названием '{search_value}'")
        logger.debug(f"🔍  Целевые позиции: {target_positions[:10]}...")

        if not target_positions:
            logger.warning(f"⚠️ Позиции '{search_value}' не найдены")
            return 0

        # Извлекаем значения из найденных позиций
        values = []
        for pos in target_positions:
            # Берем значение из пересечения строки с кодом и позиции столбца
            value = df_svodka.iloc[code_row_loc, pos]
            
            # Если это Series, берем первое значение
            if isinstance(value, pd.Series):
                if len(value) > 0:
                    # Берем первое не-NaN значение
                    first_valid = value.dropna().iloc[0] if not value.dropna().empty else 0
                    values.append(first_valid)
                else:
                    values.append(0)
            else:
                values.append(value)


        
        # Преобразуем в числовой формат
        numeric_values = pd.to_numeric(values, errors='coerce')
        logger.debug(f"🔍  Числовые значения (первые 5): {numeric_values.tolist()[:5]}")
        
        # Попробуем альтернативное преобразование
        try:
            # Если pandas не может преобразовать, попробуем вручную
            manual_values = []
            for v in values:
                if pd.isna(v) or v is None:
                    manual_values.append(0)
                else:
                    try:
                        # Пробуем преобразовать в float
                        manual_values.append(float(str(v).replace(',', '.')))
                    except (ValueError, TypeError):
                        manual_values.append(0)
            
            logger.debug(f"🔍  Ручное преобразование (первые 5): {manual_values[:5]}")
            numeric_values = pd.Series(manual_values)
        except Exception as e:
            logger.warning(f"⚠️ Ошибка при ручном преобразовании: {e}")
            # Используем исходные значения
            numeric_values = pd.Series([0 if pd.isna(v) or v is None else v for v in values])

        # Агрегация данных (NaN игнорируются)
        if search_value is None:
            # Возвращаем массив всех значений (игнорируя NaN)
            if len(numeric_values) > 0:
                # Фильтруем NaN значения и возвращаем как список
                valid_values = numeric_values.dropna()
                if len(valid_values) > 0:
                    return valid_values.tolist()
                else:
                    return []
            else:
                return []
        else:
            # Возвращаем массив всех значений (игнорируя NaN)
            if len(numeric_values) > 0:
                # Фильтруем NaN значения и возвращаем как список
                valid_values = numeric_values.dropna()
                if len(valid_values) > 0:
                    return valid_values.tolist()
                else:
                    return []
            else:
                return []

    def _get_svodka_og(self, og_id: str, codes: List[int], columns: List[str], search_value: Optional[str] = None):
        """Служебная функция получения данных по одному ОГ"""
        result = {}

        # Получаем данные из сохраненных словарей (через self.df)
        if not hasattr(self, 'df') or self.df is None:
            logger.error("❌ Данные не загружены. Сначала загрузите ZIP архив.")
            return {col: {str(code): None for code in codes} for col in columns}

        # Извлекаем словари из сохраненных данных
        df_pm_facts = self.df.get('df_pm_facts', {})
        df_pm_plans = self.df.get('df_pm_plans', {})

        # Получаем данные для конкретного ОГ
        fact_df = df_pm_facts.get(og_id)
        plan_df = df_pm_plans.get(og_id)

        logger.debug(f"🔍 ===== НАЧАЛО ОБРАБОТКИ ОГ {og_id} =====")
        logger.debug(f"🔍 Коды: {codes}")
        logger.debug(f"🔍 Столбцы: {columns}")
        logger.debug(f"🔍 Получены данные для {og_id}: факт={'✅' if fact_df is not None else '❌'}, план={'✅' if plan_df is not None else '❌'}")

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

            if col in ['ПП', 'БП']:
                if plan_df is None:
                    logger.warning(f"❌ Невозможно обработать '{col}': нет данных плана для {og_id}")
                else:
                    logger.debug(f"🔍  ===== ОБРАБАТЫВАЕМ '{col}' ИЗ ДАННЫХ ПЛАНА =====")
                    for code in codes:
                        logger.debug(f"🔍  --- Код {code} для {col} ---")
                        val = self._get_svodka_value(plan_df, og_id, code, col)
                        col_result[str(code)] = val
                    logger.debug(f"🔍  ===== ЗАВЕРШИЛИ ОБРАБОТКУ '{col}' =====")

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

            result[col] = col_result

        return result

    def _get_single_og(self, params: Dict[str, Any]) -> str:
        """API функция для получения данных по одному ОГ"""
        # Если на входе строка — парсим как JSON
        if isinstance(params, str):
            try:
                params = json.loads(params)
            except json.JSONDecodeError as e:
                raise ValueError(f"Некорректный JSON: {e}")

        # Проверяем структуру
        if not isinstance(params, dict):
            raise TypeError("Конфиг должен быть словарём или JSON-строкой")

        og_id = params.get("id")
        codes = params.get("codes")
        columns = params.get("columns")
        search = params.get("search")

        if not isinstance(codes, list):
            raise ValueError("Поле 'codes' должно быть списком")
        if not isinstance(columns, list):
            raise ValueError("Поле 'columns' должно быть списком")

        data = self._get_svodka_og(og_id, codes, columns, search)
        json_result = data_to_json(data)
        return json_result

    def _get_total_ogs(self, params: Dict[str, Any]) -> str:
        """API функция для получения данных по всем ОГ"""
        # Если на входе строка — парсим как JSON
        if isinstance(params, str):
            try:
                params = json.loads(params)
            except json.JSONDecodeError as e:
                raise ValueError(f"❌Некорректный JSON: {e}")

        # Проверяем структуру
        if not isinstance(params, dict):
            raise TypeError("Конфиг должен быть словарём или JSON-строкой")

        codes = params.get("codes")
        columns = params.get("columns")
        search = params.get("search")

        if not isinstance(codes, list):
            raise ValueError("Поле 'codes' должно быть списком")
        if not isinstance(columns, list):
            raise ValueError("Поле 'columns' должно быть списком")

        total_result = {}

        for og_id in SINGLE_OGS:
            if og_id == 'BASH':
                continue

            try:
                data = self._get_svodka_og(og_id, codes, columns, search)
                total_result[og_id] = data
            except Exception as e:
                logger.error(f"❌ Ошибка при обработке {og_id}: {e}")
                total_result[og_id] = None

        json_result = data_to_json(total_result)
        return json_result