"""Универсальный модуль для логирования метрик производительности.

Этот модуль предоставляет класс `PLogger` для сбора и записи метрик
производительности в CSV файлы с поддержкой потокобезопасной записи
и настраиваемых стратегий ротации логов.
"""
import csv
import threading
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Callable


class PLogger:
    """
    Универсальный сервис для сбора и записи метрик производительности в один CSV файл.
    Обеспечивает потокобезопасную запись и интеллектуальную ротацию логов.
    """

    def __init__(
            self,
            log_path: Path,
            header: List[str],
            max_entries: int,
            rotation_strategy: str = 'fifo',  # 'fifo', 'by_metric', 'by_group_metric'
            metric_column: Optional[str] = None,
            group_column: Optional[str] = None
    ):
        """
        Инициализирует логгер для одного файла.

        Args:
            log_path: Полный путь к файлу лога.
            header: Список заголовков для CSV файла.
            max_entries: Максимальное количество записей в логе перед ротацией.
            rotation_strategy: Стратегия ротации ('fifo', 'by_metric', 'by_group_metric').
            metric_column: Имя колонки, используемой для метрики при ротации.
            group_column: Имя колонки, используемой для группировки при ротации.
        """
        self.log_path: Path = log_path
        self.header: List[str] = ["timestamp"] + header
        self._max_entries: int = max_entries
        self.metric_column: Optional[str] = metric_column
        self.group_column: Optional[str] = group_column
        self._lock: threading.Lock = threading.Lock()

        self._rotation_strategies: Dict[str, Callable] = {
            'fifo': lambda h, lines: 0,
            'by_metric': self.__get_index_to_discard_by_metric,
            'by_group_metric': self.__get_index_to_discard_by_group_metric,
        }
        if rotation_strategy not in self._rotation_strategies:
            raise ValueError(f"Unknown rotation strategy: {rotation_strategy}")
        self._rotation_strategy: Callable = self._rotation_strategies[rotation_strategy]

        self.log_path.parent.mkdir(exist_ok=True, parents=True)
        self.__init_log_file()

    @property
    def max_entries(self) -> int:
        return self._max_entries

    @max_entries.setter
    def max_entries(self, value: int) -> None:
        with self._lock:
            self._max_entries = value

    def log(self, data: Dict[str, Any]) -> None:
        """
        Записывает одну строку данных в лог.

        Args:
            data: Словарь, где ключи - это имена колонок из `header` (кроме 'timestamp').
        """
        with self._lock:
            try:
                # Убираем None значения, чтобы не писать лишнего
                clean_data = {k: v for k, v in data.items() if v is not None}

                header, lines = self.__read_log_data()

                # Формируем строку в правильном порядке
                new_row = [datetime.now().isoformat()]
                # Пропускаем 'timestamp' в header при сопоставлении
                for col in self.header[1:]:
                    new_row.append(str(clean_data.get(col, '')))

                lines.append(new_row)

                # Ротация
                if 0 < self.max_entries < len(lines):
                    lines = self.__rotate(lines, header)

                self.__write_log_data(self.log_path, header, lines)
            except Exception as e:
                print(f"Ошибка при записи в лог {self.log_path.name}: {e}")

    def __init_log_file(self) -> None:
        """Проверяет наличие файла и создает его с заголовком, если он отсутствует."""
        if not self.log_path.exists() or self.log_path.stat().st_size == 0:
            with self.log_path.open('w', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                writer.writerow(self.header)

    def __rotate(self, lines: List[List[str]], header: List[str]) -> List[List[str]]:
        """Применяет выбранную стратегию для удаления лишних записей."""
        while len(lines) > self.max_entries:
            idx = self._rotation_strategy(header, lines)
            lines.pop(idx)
        return lines

    # --- Блок I/O ---
    def __read_log_data(self) -> tuple[List[str], List[List[str]]]:
        if self.log_path.exists() and self.log_path.stat().st_size > 0:
            with self.log_path.open('r', newline='', encoding='utf-8') as f:
                reader = csv.reader(f)
                header = next(reader, [])
                return header, list(reader)
        return self.header, []

    @staticmethod
    def __write_log_data(file_path: Path, header: List[str], lines: List[List[str]]) -> None:
        with file_path.open('w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            writer.writerow(header)
            writer.writerows(lines)

    # --- Блок алгоритмов ротации ---
    def __get_index_to_discard_by_metric(self, header: List[str], lines: List[List[str]]) -> int:
        """Находит индекс записи, метрика которой наиболее близка к соседу."""
        if self.metric_column is None:
            return 0  # Fallback to FIFO if no metric column is specified

        try:
            m_idx = header.index(self.metric_column)
            indexed_metrics = []
            for i, line in enumerate(lines):
                try:
                    indexed_metrics.append((float(line[m_idx]), i))
                except (ValueError, IndexError):
                    continue

            if len(indexed_metrics) < 2:
                return 0

            indexed_metrics.sort()

            min_diff = float('inf')
            target_idx = 0
            for i in range(len(indexed_metrics) - 1):
                diff = indexed_metrics[i + 1][0] - indexed_metrics[i][0]
                if diff < min_diff:
                    min_diff = diff
                    target_idx = min(indexed_metrics[i][1], indexed_metrics[i + 1][1])
            return target_idx

        except (ValueError, IndexError):
            return 0

    def __get_index_to_discard_by_group_metric(self, header: List[str], lines: List[List[str]]) -> int:
        """Группирует по `group_column` и ищет близкие по `metric_column`."""
        if self.group_column is None or self.metric_column is None:
            return 0  # Fallback to FIFO

        try:
            name_idx = header.index(self.group_column)
            groups = defaultdict(list)
            for i, line in enumerate(lines):
                groups[line[name_idx]].append(i)

            largest_group_name = max(groups, key=lambda k: len(groups[k]))
            group_indices = groups[largest_group_name]

            if len(group_indices) < 2:
                # Если в самой большой группе меньше 2х элементов, то просто удаляем самый старый из группы
                return group_indices[0]

            group_lines = [lines[i] for i in group_indices]
            sub_idx = self.__get_index_to_discard_by_metric(header, group_lines)
            return group_indices[sub_idx]

        except (ValueError, IndexError):
            return 0
