"""
Контроллер для управления слоями изображений
Отвечает за бизнес-логику работы со слоями: композитинг, обработку, трансформации
"""

from PySide6.QtCore import QObject, Signal, Qt
from typing import List, Dict, Optional
from datetime import datetime  # ← ДОБАВИТЬ ЭТУ СТРОКУ
import numpy as np
from pathlib import Path

from models.image_model import ImageModel, ImageLayer
from utils.image_processor import ImageProcessor
from utils.metadata_parser import MetadataParser


class LayerController(QObject):
    """
    Контроллер для управления слоями изображений
    Single Responsibility: только логика работы со слоями
    """

    # Сигналы для оповещения View
    layer_loaded = Signal(object)      # ImageLayer
    layer_updated = Signal(int)        # layer_id
    composite_updated = Signal(np.ndarray)  # скомпозированное изображение
    processing_started = Signal(str)   # сообщение
    processing_finished = Signal(bool) # успех/неудача

    def __init__(self, image_model: ImageModel):
        super().__init__()
        self.image_model = image_model
        self._processing_thread = None

        # Подключаемся к сигналам модели
        self.image_model.layer_added.connect(self.on_layer_added)
        self.image_model.layer_removed.connect(self.on_layer_removed)
        self.image_model.layer_visibility_changed.connect(self.on_visibility_changed)
        self.image_model.layer_opacity_changed.connect(self.on_opacity_changed)

    def load_layer_from_file(self, filepath: Path, source_id: int = None,
                             wavelength: str = None) -> Optional[int]:
        """
        Загружает слой из файла

        Args:
            filepath: Путь к JP2 файлу
            source_id: ID источника (опционально)
            wavelength: Длина волны (опционально)

        Returns:
            ID созданного слоя или None
        """
        self.processing_started.emit(f"Загрузка: {filepath.name}")

        try:
            # Загружаем изображение
            img_data = ImageProcessor.load_jp2(str(filepath))
            if img_data is None:
                self.processing_finished.emit(False)
                return None

            # Извлекаем метаданные
            metadata = MetadataParser.extract_metadata(str(filepath))

            # Извлекаем информацию из метаданных если не передана
            if source_id is None and metadata:
                # Пытаемся определить инструмент из метаданных
                instrument = metadata.get('INSTRUME', '')
                wavelength_val = metadata.get('WAVELNTH', '')

                # Можно добавить логику определения source_id по инструменту
                source_id = self._guess_source_id(instrument, wavelength_val)

            if wavelength is None and metadata:
                wavelength = metadata.get('WAVELNTH', 'Unknown')

            # Получаем дату из метаданных или из имени файла
            date = self._extract_date_from_metadata(metadata)
            if date is None:
                date = datetime.now()

            # Добавляем слой в модель
            layer_id = self.image_model.add_layer(
                filepath, source_id or 0, date, wavelength or "Unknown",
                img_data, metadata
            )

            self.processing_finished.emit(True)
            return layer_id

        except Exception as e:
            print(f"Ошибка загрузки слоя: {e}")
            self.processing_finished.emit(False)
            return None

    def apply_color_map(self, layer_id: int, color_map: str):
        """
        Применяет цветовую карту к слою

        Args:
            layer_id: ID слоя
            color_map: Название цветовой карты
        """
        layer = self._get_layer_by_id(layer_id)
        if layer and layer.image_data is not None:
            # Применяем цветовую карту к черно-белому изображению
            if len(layer.image_data.shape) == 2 or layer.image_data.shape[2] == 1:
                colored = ImageProcessor.apply_color_map(
                    layer.image_data if len(layer.image_data.shape) == 2 else layer.image_data[:, :, 0],
                    color_map
                )
                layer.image_data = colored
                self.layer_updated.emit(layer_id)
                self._update_composite()

    def adjust_brightness_contrast(self, layer_id: int, brightness: float = 0,
                                   contrast: float = 1.0):
        """
        Регулирует яркость и контраст слоя

        Args:
            layer_id: ID слоя
            brightness: Смещение яркости (-127 до 127)
            contrast: Коэффициент контраста (0.5 до 2.0)
        """
        layer = self._get_layer_by_id(layer_id)
        if layer and layer.image_data is not None:
            img = layer.image_data.astype(float)

            # Применяем контраст и яркость
            img = img * contrast + brightness

            # Клиппируем и конвертируем обратно в uint8
            img = np.clip(img, 0, 255).astype(np.uint8)
            layer.image_data = img

            self.layer_updated.emit(layer_id)
            self._update_composite()

    def auto_adjust_levels(self, layer_id: int, low_percent: float = 0.5,
                           high_percent: float = 99.5):
        """
        Автоматическая настройка уровней (перцентильная нормализация)

        Args:
            layer_id: ID слоя
            low_percent: Нижний процент отсечения
            high_percent: Верхний процент отсечения
        """
        layer = self._get_layer_by_id(layer_id)
        if layer and layer.image_data is not None:
            # Для черно-белых изображений
            if len(layer.image_data.shape) == 2:
                img = layer.image_data
                normalized = ImageProcessor.percent_normalize(img, low_percent, high_percent)
                layer.image_data = normalized
            else:
                # Для цветных - обрабатываем каждый канал отдельно
                for c in range(layer.image_data.shape[2]):
                    channel = layer.image_data[:, :, c]
                    normalized = ImageProcessor.percent_normalize(channel, low_percent, high_percent)
                    layer.image_data[:, :, c] = normalized

            self.layer_updated.emit(layer_id)
            self._update_composite()

    def merge_visible_layers(self) -> Optional[np.ndarray]:
        """
        Объединяет все видимые слои в одно изображение

        Returns:
            Скомпозированное изображение или None
        """
        visible_layers = self.image_model.get_visible_layers()

        if not visible_layers:
            return None

        # Собираем данные и прозрачности
        layers_data = []
        opacities = []

        for layer in visible_layers:
            if layer.image_data is not None:
                layers_data.append(layer.image_data)
                opacities.append(layer.opacity)

        if layers_data:
            # Приводим все слои к одному размеру (берем минимальный)
            min_height = min(img.shape[0] for img in layers_data)
            min_width = min(img.shape[1] for img in layers_data)

            # Обрезаем или ресайзим слои до минимального размера
            resized_layers = []
            for img in layers_data:
                if img.shape[0] != min_height or img.shape[1] != min_width:
                    from PIL import Image
                    pil_img = Image.fromarray(img)
                    pil_img = pil_img.resize((min_width, min_height), Image.Resampling.LANCZOS)
                    resized_layers.append(np.array(pil_img))
                else:
                    resized_layers.append(img)

            # Композитим слои
            composite = ImageProcessor.composite_layers(resized_layers, opacities)
            self.composite_updated.emit(composite)
            return composite

        return None

    def export_composite(self, filepath: Path, format: str = "PNG"):
        """
        Экспортирует скомпозированное изображение в файл

        Args:
            filepath: Путь для сохранения
            format: Формат (PNG, JPEG, TIFF)
        """
        composite = self.merge_visible_layers()
        if composite is not None:
            from PIL import Image
            pil_img = Image.fromarray(composite)
            pil_img.save(filepath, format=format)
            return True
        return False

    def align_layers(self, reference_layer_id: int = None):
        """
        Выравнивает все слои относительно опорного слоя
        Использует фазовую корреляцию для сдвига изображений

        Args:
            reference_layer_id: ID опорного слоя (если None - используется первый видимый)
        """
        visible_layers = self.image_model.get_visible_layers()

        if len(visible_layers) < 2:
            return

        # Определяем опорный слой
        if reference_layer_id:
            reference = self._get_layer_by_id(reference_layer_id)
        else:
            reference = visible_layers[0]

        if not reference or reference.image_data is None:
            return

        self.processing_started.emit("Выравнивание слоев...")

        # Для каждого слоя вычисляем сдвиг относительно опорного
        for layer in visible_layers:
            if layer.id == reference.id or layer.image_data is None:
                continue

            # Вычисляем сдвиг с помощью фазовой корреляции
            shift = self._calculate_shift(reference.image_data, layer.image_data)

            if shift != (0, 0):
                # Сдвигаем изображение
                layer.image_data = self._shift_image(layer.image_data, shift)
                self.layer_updated.emit(layer.id)

        self.processing_finished.emit(True)
        self._update_composite()

    def _calculate_shift(self, ref_img: np.ndarray, target_img: np.ndarray) -> tuple:
        """
        Вычисляет сдвиг между двумя изображениями с помощью фазовой корреляции

        Args:
            ref_img: Опорное изображение
            target_img: Целевое изображение

        Returns:
            Сдвиг в пикселях (dy, dx)
        """
        # Преобразуем в grayscale если цветные
        if len(ref_img.shape) == 3:
            ref_gray = np.mean(ref_img, axis=2)
        else:
            ref_gray = ref_img

        if len(target_img.shape) == 3:
            target_gray = np.mean(target_img, axis=2)
        else:
            target_gray = target_img

        # Вычисляем кросс-корреляцию через FFT
        from scipy import signal
        correlation = signal.correlate2d(ref_gray, target_gray, mode='same')

        # Находим пик корреляции
        y, x = np.unravel_index(np.argmax(correlation), correlation.shape)

        # Центр изображения
        center_y, center_x = np.array(ref_gray.shape) // 2

        # Сдвиг
        shift_y = center_y - y
        shift_x = center_x - x

        return (shift_y, shift_x)

    def _shift_image(self, img: np.ndarray, shift: tuple) -> np.ndarray:
        """
        Сдвигает изображение на заданное количество пикселей

        Args:
            img: Исходное изображение
            shift: Сдвиг (dy, dx)

        Returns:
            Сдвинутое изображение
        """
        from scipy.ndimage import shift as shift_image

        shifted = shift_image(img, shift, mode='constant', cval=0)
        return shifted.astype(img.dtype)

    def _get_layer_by_id(self, layer_id: int) -> Optional[ImageLayer]:
        """Возвращает слой по ID"""
        for layer in self.image_model.get_all_layers():
            if layer.id == layer_id:
                return layer
        return None

    def _guess_source_id(self, instrument: str, wavelength: str) -> int:
        """Пытается определить source_id по инструменту и длине волны"""
        from models.api_model import HelioviewerAPI

        instrument = instrument.upper()
        wavelength = wavelength.upper()

        for source_id, info in HelioviewerAPI.SOURCES.items():
            if info['instrument'].upper() in instrument:
                if wavelength and info['wavelength'].upper() in wavelength:
                    return source_id

        return 0

    def _extract_date_from_metadata(self, metadata: Dict) -> Optional[datetime]:
        """Извлекает дату из метаданных"""
        date_str = metadata.get('DATE-OBS') or metadata.get('DATE-BEG')
        if date_str:
            try:
                # Пробуем разные форматы дат
                formats = [
                    "%Y-%m-%dT%H:%M:%S.%f",
                    "%Y-%m-%dT%H:%M:%S",
                    "%Y-%m-%d %H:%M:%S",
                    "%Y-%m-%d"
                ]
                for fmt in formats:
                    try:
                        return datetime.strptime(date_str, fmt)
                    except ValueError:
                        continue
            except:
                pass
        return None

    def _update_composite(self):
        """Обновляет композитное изображение"""
        composite = self.merge_visible_layers()
        if composite is not None:
            self.composite_updated.emit(composite)

    def on_layer_added(self, layer: ImageLayer):
        """Обработчик добавления слоя"""
        self._update_composite()

    def on_layer_removed(self, layer_id: int):
        """Обработчик удаления слоя"""
        self._update_composite()

    def on_visibility_changed(self, layer_id: int, visible: bool):
        """Обработчик изменения видимости"""
        self._update_composite()

    def on_opacity_changed(self, layer_id: int, opacity: float):
        """Обработчик изменения прозрачности"""
        self._update_composite()