from pathlib import Path
from typing import Any, Dict, List, Union

from PyQt6.QtCore import QThread, pyqtSignal

from app.data_structures.processing import (
    DetectionResult,
    UnprocessedVideoResult,
    VideoProcessingResult,
)
from app.data_structures.time_interval import DatetimeInterval
from app.exceptions import CustomBaseError
from app.loggers import processing as logger
from app.services.detection_processor import DetectionProcessor
from app.services.image_saver import ImageSaver
from app.services.report_generator import ReportGenerator
from app.services.report_saver import ReportSaver


class ProcessingManager(QThread):
    """
    Wrapper for DetectionProcessor class for processing videos in separate thread.
    """

    setNextProgressState = pyqtSignal(dict)
    updateProgressStatus = pyqtSignal(dict)
    sendResult = pyqtSignal(object)
    nextFile = pyqtSignal()

    def __init__(
        self,
        file_index: int,
        file_path: Path,
        detection_processor: DetectionProcessor,
        image_saver: ImageSaver,
    ) -> None:
        super().__init__()
        self.file_index = file_index
        self.file_path = file_path
        self._detection_processor = detection_processor
        self._image_saver = image_saver

    def _dispatch_data_from_detection_processor(
        self, data_type: str, data: Dict[str, Any]
    ) -> None:
        """
        Dispatch data from detection processor to proper methods.
        :param data_type: data type identifier
        :param data: data to dispatch
        :return:
        """
        if data_type == "stage":
            self.setNextProgressState.emit(data)
        elif data_type in (
            "motion_detection_status",
            "cutting_status",
            "object_detection_status",
        ):
            self.updateProgressStatus.emit(data)

    def _prepare_and_save_detected_objects_images(
        self, results: List[DetectionResult]
    ) -> None:
        """
        Prepare and save detected objects images. Saved images paths are added to result objects.
        :param results: list of detection results
        :return:
        """
        for result in results:
            if not result.objects_image:
                continue
            image_path = self._image_saver.save(
                image=result.objects_image.image,
                file_name=result.objects_image.file_name,
            )
            result.image_path = image_path

    def _send_result(
        self, result: Union[VideoProcessingResult, UnprocessedVideoResult]
    ) -> None:
        """
        Send result to main thread.
        :param result: result of video processing
        :return:
        """
        logger.info("Sending result to main thread")
        self.sendResult.emit(result)

    def _next_file(self) -> None:
        """
        Send signal to main thread to process next file.
        :return:
        """
        logger.info("Send signal to main thread to process next file")
        self.nextFile.emit()

    def _configure_signal_handlers(self) -> None:
        self._detection_processor.sendData.connect(
            self._dispatch_data_from_detection_processor
        )

    def run(self) -> None:
        """
        Entry point for processing video in separate thread. This method is called by QThread.start() method.
        :return:
        """
        self._configure_signal_handlers()
        logger.info(f"Started processing video {self.file_path}")
        try:
            detections = self._detection_processor.start_process(
                file_index=self.file_index, file_path=self.file_path
            )
        except CustomBaseError as e:
            logger.exception(f"Error while processing video {self.file_path}: {e}")
            processing_result = UnprocessedVideoResult(
                video_path=self.file_path,
                error_message=f"{e.title}: {e.message}",
            )
            self._send_result(processing_result)
            self._next_file()
            return
        logger.info(f"Finished processing video {self.file_path}")
        if self._detection_processor._configurations.object_detection:
            self._prepare_and_save_detected_objects_images(detections)
        processing_result = VideoProcessingResult(
            video_path=self.file_path,
            detection_results=detections,
        )
        self._send_result(processing_result)
        self._next_file()


def prepare_and_save_report(
    results_folder: Path,
    results: List[VideoProcessingResult],
    processing_time_interval: DatetimeInterval,
    object_detection_enabled: bool,
) -> Path:
    """
    Prepare and save report from detection results.
    :param results_folder: folder where report will be saved
    :param results: detection results
    :param object_detection_enabled: flag indicating if object detection was enabled
    :param processing_time_interval: time interval of processing
    :return: path to saved report
    """
    report = _prepare_report(
        results=results,
        processing_time_interval=processing_time_interval,
        object_detection_enabled=object_detection_enabled,
    )
    report_path = _save_report(results_folder, report)
    return report_path


def _prepare_report(
    results: List[VideoProcessingResult],
    processing_time_interval: DatetimeInterval,
    object_detection_enabled: bool,
) -> str:
    """
    Generate report text from detection results.
    :param results: detection results
    :param object_detection_enabled: flag indicating if object detection was enabled
    :return: report text
    """
    report_generator = ReportGenerator(
        results=results,
        processing_time_interval=processing_time_interval,
        object_detection_enabled=object_detection_enabled,
    )
    report = report_generator.generate()
    return report


def _save_report(results_folder: Path, report: str) -> Path:
    """
    Save report to file.
    :param results_folder: folder where report will be saved
    :param report: report text
    :return: path to saved report
    """
    report_saver = ReportSaver(results_folder)
    report_path = report_saver.save(report)
    return report_path
