"""
Асинхронный парсер данных LyngSat с поддержкой кеширования в Redis.
"""
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import logging
from typing import Optional
from django.core.cache import cache

logger = logging.getLogger(__name__)


def parse_satellite_names(satellite_string: str) -> list[str]:
    """Извлекает все возможные имена спутников из строки."""
    slash_parts = [part.strip() for part in satellite_string.split('/')]
    all_names = []
    for part in slash_parts:
        main_match = re.match(r'^([^(]+)', part)
        if main_match:
            main_name = main_match.group(1).strip()
            if main_name:
                all_names.append(main_name)
        bracket_match = re.search(r'\(([^)]+)\)', part)
        if bracket_match:
            bracket_name = bracket_match.group(1).strip()
            if bracket_name:
                all_names.append(bracket_name)
    seen = set()
    result = []
    for name in all_names:
        if name not in seen:
            seen.add(name)
            result.append(name.strip().lower())
    return result


class AsyncLyngSatParser:
    """
    Асинхронный парсер данных для LyngSat с поддержкой кеширования.
    
    Кеширование:
    - Страницы регионов кешируются на 7 дней
    - Данные спутников кешируются на 1 день
    """

    # Время жизни кеша
    REGION_CACHE_TTL = 60 * 60 * 24 * 7  # 7 дней
    SATELLITE_CACHE_TTL = 60 * 60 * 24  # 1 день
    
    # Префиксы для ключей кеша
    REGION_CACHE_PREFIX = "lyngsat_region"
    SATELLITE_CACHE_PREFIX = "lyngsat_satellite"
    SATELLITE_LIST_CACHE_PREFIX = "lyngsat_sat_list"

    def __init__(
        self,
        flaresolver_url: str = "http://localhost:8191/v1",
        regions: list[str] | None = None,
        target_sats: list[str] | None = None,
        use_cache: bool = True,
    ):
        """
        Инициализация парсера.
        
        Args:
            flaresolver_url: URL FlareSolverr для обхода защиты
            regions: Список регионов для парсинга
            target_sats: Список целевых спутников (в нижнем регистре)
            use_cache: Использовать ли кеширование
        """
        self.flaresolver_url = flaresolver_url
        self.use_cache = use_cache
        self.target_sats = (
            list(map(lambda sat: sat.strip().lower(), target_sats)) if target_sats else None
        )
        self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
        self.BASE_URL = "https://www.lyngsat.com"

    def _get_cache_key(self, prefix: str, identifier: str) -> str:
        """Генерирует ключ для кеша."""
        return f"{prefix}:{identifier}"

    def _get_from_cache(self, key: str) -> Optional[any]:
        """Получает данные из кеша."""
        if not self.use_cache:
            return None
        try:
            data = cache.get(key)
            if data:
                logger.debug(f"Данные получены из кеша: {key}")
            return data
        except Exception as e:
            logger.warning(f"Ошибка при получении из кеша {key}: {e}")
            return None

    def _set_to_cache(self, key: str, value: any, ttl: int) -> None:
        """Сохраняет данные в кеш."""
        if not self.use_cache:
            return
        try:
            cache.set(key, value, timeout=ttl)
            logger.debug(f"Данные сохранены в кеш: {key} (TTL: {ttl}s)")
        except Exception as e:
            logger.warning(f"Ошибка при сохранении в кеш {key}: {e}")

    @classmethod
    def clear_cache(cls, cache_type: str = "all") -> dict:
        """
        Очищает кеш парсера.
        
        Args:
            cache_type: Тип кеша для очистки ("regions", "satellites", "all")
            
        Returns:
            dict: Статистика очистки
        """
        stats = {"cleared": 0, "errors": []}
        
        try:
            from django.core.cache import cache as django_cache
            
            if cache_type in ("regions", "all"):
                # Очищаем кеш регионов
                regions = ["europe", "asia", "america", "atlantic"]
                for region in regions:
                    key = f"{cls.REGION_CACHE_PREFIX}:{region}"
                    try:
                        result = django_cache.delete(key)
                        if result:
                            stats["cleared"] += 1
                            logger.info(f"Очищен кеш региона: {region}")
                        else:
                            logger.debug(f"Кеш региона {region} не найден или уже удален")
                    except Exception as e:
                        error_msg = f"Ошибка при очистке кеша региона {region}: {e}"
                        logger.error(error_msg)
                        stats["errors"].append(error_msg)
            
            if cache_type in ("satellites", "all"):
                # Для очистки кеша спутников используем keys()
                if hasattr(django_cache, 'keys'):
                    try:
                        # Очищаем списки спутников
                        list_keys = django_cache.keys(f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*")
                        if list_keys:
                            if hasattr(django_cache, 'delete_many'):
                                django_cache.delete_many(list_keys)
                            else:
                                for key in list_keys:
                                    django_cache.delete(key)
                            stats["cleared"] += len(list_keys)
                            logger.info(f"Очищено {len(list_keys)} списков спутников")
                        
                        # Очищаем данные спутников
                        sat_keys = django_cache.keys(f"{cls.SATELLITE_CACHE_PREFIX}:*")
                        if sat_keys:
                            if hasattr(django_cache, 'delete_many'):
                                django_cache.delete_many(sat_keys)
                            else:
                                for key in sat_keys:
                                    django_cache.delete(key)
                            stats["cleared"] += len(sat_keys)
                            logger.info(f"Очищено {len(sat_keys)} данных спутников")
                    except Exception as e:
                        error_msg = f"Ошибка при очистке кеша спутников: {e}"
                        logger.error(error_msg)
                        stats["errors"].append(error_msg)
                else:
                    logger.warning("Бэкенд кеша не поддерживает keys()")
                    logger.info("Для полной очистки используйте: redis-cli flushdb")
                
        except Exception as e:
            error_msg = f"Критическая ошибка при очистке кеша: {e}"
            logger.error(error_msg)
            stats["errors"].append(error_msg)
        
        return stats

    @classmethod
    def clear_all_cache(cls) -> dict:
        """Полностью очищает весь кеш LyngSat."""
        stats = {"cleared": 0, "errors": []}
        try:
            from django.core.cache import cache as django_cache
            
            # Для django-redis используем keys() + delete_many()
            if hasattr(django_cache, 'keys'):
                patterns = [
                    f"{cls.REGION_CACHE_PREFIX}:*",
                    f"{cls.SATELLITE_CACHE_PREFIX}:*",
                    f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*",
                ]
                
                all_keys = []
                for pattern in patterns:
                    try:
                        keys = django_cache.keys(pattern)
                        if keys:
                            all_keys.extend(keys)
                            logger.info(f"Найдено {len(keys)} ключей по паттерну: {pattern}")
                    except Exception as e:
                        error_msg = f"Ошибка при поиске ключей {pattern}: {e}"
                        logger.error(error_msg)
                        stats["errors"].append(error_msg)
                
                # Удаляем все найденные ключи
                if all_keys:
                    try:
                        if hasattr(django_cache, 'delete_many'):
                            django_cache.delete_many(all_keys)
                        else:
                            for key in all_keys:
                                django_cache.delete(key)
                        stats["cleared"] = len(all_keys)
                        logger.info(f"Удалено {len(all_keys)} ключей")
                    except Exception as e:
                        error_msg = f"Ошибка при удалении ключей: {e}"
                        logger.error(error_msg)
                        stats["errors"].append(error_msg)
                else:
                    logger.info("Ключи для удаления не найдены")
                    
            elif hasattr(django_cache, 'delete_pattern'):
                # Fallback на delete_pattern
                patterns = [
                    f"{cls.REGION_CACHE_PREFIX}:*",
                    f"{cls.SATELLITE_CACHE_PREFIX}:*",
                    f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*",
                ]
                for pattern in patterns:
                    try:
                        deleted = django_cache.delete_pattern(pattern)
                        if deleted and isinstance(deleted, int):
                            stats["cleared"] += deleted
                            logger.info(f"Очищено {deleted} ключей по паттерну: {pattern}")
                    except Exception as e:
                        error_msg = f"Ошибка при очистке паттерна {pattern}: {e}"
                        logger.error(error_msg)
                        stats["errors"].append(error_msg)
            else:
                # Fallback для других бэкендов кеша
                logger.warning("Бэкенд кеша не поддерживает keys() или delete_pattern()")
                return cls.clear_cache("all")
                
        except Exception as e:
            error_msg = f"Критическая ошибка при полной очистке кеша: {e}"
            logger.error(error_msg)
            stats["errors"].append(error_msg)
        
        return stats

    def parse_metadata(self, metadata: str) -> dict:
        """Парсит метаданные транспондера."""
        if not metadata or not metadata.strip():
            return {
                "standard": None,
                "modulation": None,
                "symbol_rate": None,
                "fec": None,
            }
        normalized = re.sub(r"\s+", "", metadata.strip())
        fec_match = re.search(r"([1-9]/[1-9])$", normalized)
        fec = fec_match.group(1) if fec_match else None
        if fec_match:
            core = normalized[: fec_match.start()]
        else:
            core = normalized
        std_match = re.match(r"(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)", core)
        standard = std_match.group(1) if std_match else None
        rest = core[len(standard) :] if standard else core
        modulation = None
        mod_match = re.match(r"(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)", rest)
        if mod_match:
            modulation = mod_match.group(1)
            rest = rest[len(modulation) :]
        symbol_rate = None
        sr_match = re.search(r"(\d+)$", rest)
        if sr_match:
            try:
                symbol_rate = int(sr_match.group(1))
            except ValueError:
                pass

        return {
            "standard": standard,
            "modulation": modulation,
            "symbol_rate": symbol_rate,
            "fec": fec,
        }

    def extract_date(self, s: str) -> datetime | None:
        """Извлекает дату из строки формата YYMMDD."""
        s = s.strip()
        match = re.search(r"(\d{6})$", s)
        if not match:
            return None
        yymmdd = match.group(1)
        try:
            return datetime.strptime(yymmdd, "%y%m%d").date()
        except ValueError:
            return None

    def convert_polarization(self, polarization: str) -> str:
        """Преобразует код поляризации в понятное название на русском."""
        polarization_map = {
            "V": "Вертикальная",
            "H": "Горизонтальная",
            "R": "Правая",
            "L": "Левая",
        }
        return polarization_map.get(polarization.upper(), polarization)

    def fetch_region_page(self, region: str, force_refresh: bool = False) -> Optional[str]:
        """
        Получает HTML страницу региона с кешированием.
        
        Args:
            region: Название региона
            force_refresh: Принудительно обновить кеш
            
        Returns:
            HTML содержимое страницы или None при ошибке
        """
        cache_key = self._get_cache_key(self.REGION_CACHE_PREFIX, region)
        
        # Проверяем кеш
        if not force_refresh:
            cached_html = self._get_from_cache(cache_key)
            if cached_html:
                logger.info(f"Страница региона {region} получена из кеша")
                return cached_html
        
        # Запрашиваем страницу
        url = f"{self.BASE_URL}/{region}.html"
        logger.info(f"Запрос страницы региона: {url}")
        
        try:
            payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
            response = requests.post(self.flaresolver_url, json=payload, timeout=70)
            
            if response.status_code != 200:
                logger.error(f"Ошибка при запросе {url}: статус {response.status_code}")
                return None
            
            html_content = response.json().get("solution", {}).get("response", "")
            
            if html_content:
                # Сохраняем в кеш
                self._set_to_cache(cache_key, html_content, self.REGION_CACHE_TTL)
                logger.info(f"Страница региона {region} получена и закеширована")
            
            return html_content
            
        except Exception as e:
            logger.error(f"Ошибка при получении страницы {url}: {e}", exc_info=True)
            return None

    def get_satellite_list_from_region(self, region: str, force_refresh: bool = False) -> list[dict]:
        """
        Получает список спутников из региона.
        
        Args:
            region: Название региона
            force_refresh: Принудительно обновить кеш
            
        Returns:
            Список словарей с информацией о спутниках
        """
        # Создаем уникальный ключ кеша с учетом целевых спутников
        # Если target_sats не указаны, используем "all"
        sats_key = "all" if not self.target_sats else "_".join(sorted(self.target_sats))
        cache_key = self._get_cache_key(self.SATELLITE_LIST_CACHE_PREFIX, f"{region}_{sats_key}")
        
        # Проверяем кеш
        if not force_refresh:
            cached_list = self._get_from_cache(cache_key)
            if cached_list:
                logger.info(f"Список спутников региона {region} (фильтр: {sats_key[:50]}) получен из кеша")
                return cached_list
        
        # Получаем HTML страницы
        html_content = self.fetch_region_page(region, force_refresh)
        if not html_content:
            return []
        
        # Парсим список спутников
        satellites = []
        try:
            soup = BeautifulSoup(html_content, "html.parser")
            col_table = soup.find_all("div", class_="desktab")[0]
            tables = col_table.find_next_sibling("table").find_all("table")
            
            trs = []
            for table in tables:
                trs.extend(table.find_all("tr"))
            
            for tr in trs:
                sat_name = tr.find("span").text.replace("ü", "u").strip().lower()
                
                # Фильтруем по целевым спутникам
                if self.target_sats is not None:
                    names = parse_satellite_names(sat_name)
                    if len(names) == 1:
                        sat_name = names[0]
                    else:
                        for name in names:
                            if name in self.target_sats:
                                sat_name = name
                                break
                    
                    if sat_name not in self.target_sats:
                        continue
                
                try:
                    sat_url = tr.find_all("a")[2]["href"]
                except IndexError:
                    sat_url = tr.find_all("a")[0]["href"]
                
                update_date_str = tr.find_all("td")[-1].text
                try:
                    update_date = datetime.strptime(update_date_str, "%y%m%d").date()
                except (ValueError, TypeError):
                    update_date = None
                
                satellites.append({
                    "name": sat_name,
                    "url": sat_url,
                    "update_date": update_date,
                    "region": region
                })
            
            # Сохраняем в кеш
            self._set_to_cache(cache_key, satellites, self.REGION_CACHE_TTL)
            sats_filter = "все" if not self.target_sats else f"{len(self.target_sats)} целевых"
            logger.info(f"Найдено {len(satellites)} спутников в регионе {region} (фильтр: {sats_filter})")
            
        except Exception as e:
            logger.error(f"Ошибка при парсинге списка спутников региона {region}: {e}", exc_info=True)
        
        return satellites

    def fetch_satellite_data(self, sat_name: str, sat_url: str, force_refresh: bool = False) -> Optional[dict]:
        """
        Получает данные одного спутника с кешированием.
        
        Args:
            sat_name: Название спутника
            sat_url: URL страницы спутника
            force_refresh: Принудительно обновить кеш
            
        Returns:
            Словарь с данными спутника или None при ошибке
        """
        cache_key = self._get_cache_key(self.SATELLITE_CACHE_PREFIX, sat_name)
        
        # Проверяем кеш
        if not force_refresh:
            cached_data = self._get_from_cache(cache_key)
            if cached_data:
                logger.info(f"Данные спутника {sat_name} получены из кеша")
                return cached_data
        
        # Запрашиваем данные
        full_url = f"{self.BASE_URL}/{sat_url}"
        logger.info(f"Запрос данных спутника {sat_name}: {full_url}")
        
        try:
            payload = {"cmd": "request.get", "url": full_url, "maxTimeout": 60000}
            response = requests.post(self.flaresolver_url, json=payload, timeout=70)
            
            if response.status_code != 200:
                logger.error(f"Ошибка при запросе {full_url}: статус {response.status_code}")
                return None
            
            html_content = response.json().get("solution", {}).get("response", "")
            
            if not html_content:
                logger.warning(f"Пустой ответ для спутника {sat_name}")
                return None
            
            # Парсим данные
            sources = self.parse_satellite_content(html_content)
            
            satellite_data = {
                "name": sat_name,
                "url": full_url,
                "sources": sources,
                "fetched_at": datetime.now().isoformat()
            }
            
            # Сохраняем в кеш
            self._set_to_cache(cache_key, satellite_data, self.SATELLITE_CACHE_TTL)
            logger.info(f"Данные спутника {sat_name} получены и закешированы ({len(sources)} источников)")
            
            return satellite_data
            
        except Exception as e:
            logger.error(f"Ошибка при получении данных спутника {sat_name}: {e}", exc_info=True)
            return None

    def parse_satellite_content(self, html_content: str) -> list[dict]:
        """Парсит содержимое страницы спутника."""
        data = []
        try:
            sat_soup = BeautifulSoup(html_content, "html.parser")
            big_table = sat_soup.find("table", class_="bigtable")
            
            if not big_table:
                logger.warning("Таблица bigtable не найдена")
                return data
            
            all_tables = big_table.find_all("div", class_="desktab")[:-1]
            
            for table in all_tables:
                trs = table.find_next_sibling("table").find_all("tr")
                for idx, tr in enumerate(trs):
                    tds = tr.find_all("td")
                    if len(tds) < 9 or idx < 2:
                        continue
                    
                    try:
                        freq, polarization = tds[0].find("b").text.strip().split("\xa0")
                        polarization = self.convert_polarization(polarization)
                        meta = self.parse_metadata(tds[1].text)
                        provider_name = tds[3].text
                        last_update = self.extract_date(tds[-1].text)
                        
                        data.append({
                            "freq": freq,
                            "pol": polarization,
                            "metadata": meta,
                            "provider_name": provider_name,
                            "last_update": last_update,
                        })
                    except Exception as e:
                        logger.debug(f"Ошибка при парсинге строки транспондера: {e}")
                        continue
                        
        except Exception as e:
            logger.error(f"Ошибка при парсинге содержимого спутника: {e}", exc_info=True)
        
        return data

    def get_all_satellites_list(self, force_refresh: bool = False) -> list[dict]:
        """
        Получает список всех спутников из всех регионов.
        
        Args:
            force_refresh: Принудительно обновить кеш
            
        Returns:
            Список словарей с информацией о спутниках
        """
        all_satellites = []
        
        for region in self.regions:
            logger.info(f"Получение списка спутников из региона: {region}")
            satellites = self.get_satellite_list_from_region(region, force_refresh)
            all_satellites.extend(satellites)
        
        logger.info(f"Всего найдено спутников: {len(all_satellites)}")
        return all_satellites
