# Tâches d'exportation pour les vidéos de caméra
import zipfile
from celery.utils.log import get_task_logger
import shutil
import os, sys
import posix_ipc
import mmap
import cv2
import numpy as np
from django.http import JsonResponse, HttpResponse
from django.conf import settings
from reduct.time import unix_timestamp_to_iso

from .process import CameraRecordManager, cameraDB

logger = get_task_logger(__name__)

def progress_bar(iteration, total, prefix='', suffix='', length=30, fill='█'):
    percent = ("{0:.1f}").format(100 * (iteration / float(total)))
    filled_length = int(length * iteration // total)
    bar = fill * filled_length + '-' * (length - filled_length)
    sys.stdout.write(f'\r{prefix} |{bar}| {percent}% {suffix}')
    sys.stdout.flush()
    

def delete_file_later(path):
    try:
        if os.path.exists(path):
            os.remove(path)
    except Exception as e:
        logger.error(f"[cleanup] error deleting {path}: {e}")
        raise


async def remove_video_by_uuid(uuid, start_ts=None, end_ts=None, when=None):
    record_manager = CameraRecordManager(cameraDB)
    await record_manager.remove(uuid, start_ts, end_ts)


async def remove_video(uuid, start_ts, end_ts, when=None):
    try:
        await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
        return JsonResponse({'state': 'ok'}, status=200)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)


async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
    try:
        record_manager = CameraRecordManager(cameraDB)
        total_size = await record_manager.size(uuid, start_ts, end_ts)

        # segment de mémoire partagée pour stocker les frames
        shm_size = int(total_size * 1.5)
        shm_name = f"/video_frames_{uuid}"
        try:
            shm = posix_ipc.SharedMemory(shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size)
        except posix_ipc.ExistentialError:
            existing = posix_ipc.SharedMemory(shm_name, flags=0)
            existing.close_fd()
            try:
                existing.unlink()
            except posix_ipc.ExistentialError:
                pass
            shm = posix_ipc.SharedMemory(shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size)
        mm = mmap.mmap(shm.fd, shm_size)

        queries = record_manager.query(uuid, start_ts, end_ts)
        offset = 0
        frame_sizes = []
        total = 0
        async for record in queries:
            frame_bytes = await record.read_all()
            frame_size = len(frame_bytes)

            mm[offset:offset + frame_size] = frame_bytes
            frame_sizes.append(frame_size)
            offset += frame_size
            total += 1

        if not frame_sizes:
            return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)

        video_path = os.path.join(settings.MEDIA_ROOT, f"output.{opencv_video_type}")
        fourcc = cv2.VideoWriter_fourcc(* opencv_fourcc_format)

        # Lit les frames depuis la mémoire partagée
        current_offset = 0
        i = 0
        for size in frame_sizes:
            frame_bytes = mm[current_offset:current_offset + size]
            nparr = np.frombuffer(frame_bytes, np.uint8)
            frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
            if 'video' not in locals():
                height, width, _ = frame.shape
                video = cv2.VideoWriter(video_path, fourcc, frame_rate, (width, height))
            video.write(frame)
            current_offset += size
            
            progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
            i+=1             
            
        video.release()

        # Nettoie la mémoire partagée
        shm.unlink()

        # Vérifier que le fichier existe
        if not os.path.exists(video_path):
            logger.error(f"Fichier non créé: {video_path}")
            return JsonResponse({'error': 'Erreur création vidéo'}, status=500)

        # Lit le fichier vidéo généré
        with open(video_path, 'rb') as f:
            video_bytes = f.read()

        # Retourne la vidéo en réponse
        response = HttpResponse(video_bytes, content_type='video/mp4')
        response['Content-Disposition'] = f'attachment; filename="{video_path}"'
        response['Content-Length'] = os.path.getsize(video_path)


        # Supprime le fichier temporaire
        os.remove(video_path)
        return response

    except Exception as e:
        logger.error(f"shm_download_video: {e}")
        return JsonResponse({'error': str(e)}, status=500)

# ─────────────────────────────────────────────
##
# ─────────────────────────────────────────────
def remote_mount_available(mount_point: str = "/mnt/exports_cam") -> bool:
    """
    Vérifie que le point de montage Samba est actif et accessible en écriture.
    """
    return os.path.ismount(mount_point) and os.access(mount_point, os.W_OK)


def _copy_to_destinations(source_path: str, filename: str) -> dict:
    """
    Copie le fichier exporté vers les destinations configurées.
    Retourne un dict avec les chemins effectivement écrits.
    """
    results = {"local": None, "remote": None}

    for dest in settings.EXPORT_DESTINATIONS:

        if dest == "local":
            # Déjà sur place, rien à copier
            results["local"] = source_path

        elif dest == "remote":
            remote_path = os.path.join(settings.EXPORT_REMOTE_DIR, filename)
            try:
                if not remote_mount_available(settings.EXPORT_REMOTE_DIR):
                    logger.warning("Partage Samba non disponible, copie ignorée")
                    results["remote_error"] = "Montage indisponible"
                    continue                
                
                # Copie locale vers le point de montage Samba
                shutil.copy2(source_path, remote_path)
                results["remote"] = remote_path
                logger.info("Copie distante OK : %s", remote_path)
            except OSError as exc:
                # Le partage est peut-être absent (machine Windows éteinte)
                logger.error("Copie distante échouée [%s] : %s", remote_path, exc)
                results["remote_error"] = str(exc)

    return results


# ─────────────────────────────────────────────
# Tâche 1 : Export des frames en ZIP d'images
# ─────────────────────────────────────────────
async def export_images_zip(
    uuid: str,
    start_ts: float | None = None,
    end_ts: float | None = None,
    jpeg_quality: int = 85,
    max_zip_size_mb: int = 0,
    max_image_width: int = 0,
    max_image_height: int = 0,
):
    """
    Exporte les frames d'une caméra sous forme d'archive ZIP contenant des JPEG.

    :param uuid: Identifiant de la caméra
    :param start_ts: Timestamp de début (epoch secondes)
    :param end_ts: Timestamp de fin (epoch secondes)
    :param max_zip_size_mb: Taille maximale du ZIP en Mo (0 = illimité)
    :param jpeg_quality: Qualité JPEG 1-100 (défaut 85)
    :param max_image_width: Redimensionnement max largeur en px (0 = non redimensionné)
    :param max_image_height: Redimensionnement max hauteur en px (0 = non redimensionné)
    :return: Chemin du fichier ZIP généré + rapport JSON
    """
    shm = None
    mm = None
    shm_name = f"/img_frames_{uuid}"

    try:
        # --- Chargement des frames en mémoire partagée ---
        record_manager = CameraRecordManager(cameraDB)
        total_size = await record_manager.size(uuid, start_ts, end_ts)
        shm_size = int(total_size * 1.5)

        try:
            shm = posix_ipc.SharedMemory(
                shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
            )
        except posix_ipc.ExistentialError:
            existing = posix_ipc.SharedMemory(shm_name, flags=0)
            existing.close_fd()
            try:
                existing.unlink()
            except posix_ipc.ExistentialError:
                pass
            shm = posix_ipc.SharedMemory(
                shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
            )

        mm = mmap.mmap(shm.fd, shm_size)

        queries = record_manager.query(uuid, start_ts, end_ts)
        if not start_ts:
            start_ts = record_manager.oldest_ts
        if not end_ts:
            end_ts = record_manager.latest_ts

        offset = 0
        frame_sizes = []
        ts = []
        total = 0
        async for record in queries:
            ts.append(record.timestamp)
            frame_bytes = await record.read_all()
            frame_size = len(frame_bytes)

            mm[offset:offset + frame_size] = frame_bytes
            frame_sizes.append(frame_size)
            offset += frame_size
            total += 1

        if not frame_sizes:
            return {"status": "error", "message": "Aucune frame trouvée"}

        # --- Génération du ZIP ---
        max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
        ts_s = unix_timestamp_to_iso(start_ts)
        zip_filename = f"{uuid}_{ts_s}.zip"
        zip_path = os.path.join(settings.EXPORTS_LOCAL_PATH, 'images', zip_filename)
        os.makedirs(os.path.dirname(zip_path), exist_ok=True)

        skipped = 0
        written = 0
        current_offset = 0
        encode_params = [cv2.IMWRITE_JPEG_QUALITY, jpeg_quality]

        i = 0
        with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
            for idx, size in enumerate(frame_sizes):
                # Vérification de la taille du ZIP
                if max_zip_bytes and os.path.getsize(zip_path) >= max_zip_bytes:
                    skipped += len(frame_sizes) - idx
                    logger.warning(
                        "export_images_zip: limite %d Mo atteinte, %d frames ignorées",
                        max_zip_size_mb,
                        len(frame_sizes) - idx,
                    )
                    break

                frame_bytes = mm[current_offset:current_offset + size]
                nparr = np.frombuffer(frame_bytes, np.uint8)
                frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

                if frame is None:
                    current_offset += size
                    skipped += 1
                    continue

                # Redimensionnement optionnel
                if max_image_width > 0 or max_image_height > 0:
                    frame = _resize_frame(frame, max_image_width, max_image_height)

                # Encodage JPEG en mémoire puis ajout au ZIP
                ok, buf = cv2.imencode(".jpg", frame, encode_params)
                if ok:
                    #zf.writestr(f"frame_{idx:06d}.jpg", buf.tobytes())
                    ts_iso = unix_timestamp_to_iso(ts[idx])
                    #logger.info(f"export_images_zip: adding frame {idx} with timestamp {ts_iso} to ZIP")
                    zf.writestr(f"{uuid}_{ts_iso}.jpg", buf.tobytes())
                    written += 1
                    
                progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
                i+=1                   
                current_offset += size
        
        ## Copie vers les destinations (local + Samba)    
        #destinations = _copy_to_destinations(zip_path, zip_filename)   
        return {
            "status": "success",
            "zip_path": zip_path,
            "frames_written": written,
            "frames_skipped": skipped,
            "jpeg_quality": jpeg_quality,
            #"destinations": destinations,
        }

    except Exception as exc:
        logger.error("export_images_zip [%s]: %s", uuid, exc, exc_info=True)
        return {"status": "error", "message": str(exc)}

    finally:
        if mm:
            mm.close()
        if shm:
            try:
                shm.unlink()
            except posix_ipc.ExistentialError:
                pass


# ─────────────────────────────────────────────
# Tâche 2 : Export des frames en vidéo MP4
# ─────────────────────────────────────────────
#@shared_task(bind=True)
async def export_video_mp4(
    uuid: str,
    start_ts: float | None = None,
    end_ts: float | None = None,
    frame_rate: int = 5,
    opencv_fourcc_format='mp4v',
    opencv_video_type='mp4',
    max_video_size_mb: int = 0,
    max_width: int = 0,
    max_height: int = 0,
):
    """
    Exporte les frames d'une caméra en fichier MP4 via OpenCV.

    :param uuid: Identifiant de la caméra
    :param start_ts: Timestamp de début (epoch secondes)
    :param end_ts: Timestamp de fin (epoch secondes)
    :param frame_rate: Images par seconde (défaut 5)
    :param max_video_size_mb: Taille maximale du MP4 en Mo (0 = illimité)
    :param max_width: Redimensionnement max largeur en px (0 = non redimensionné)
    :param max_height: Redimensionnement max hauteur en px (0 = non redimensionné)
    :return: Chemin du fichier MP4 généré + rapport JSON
    """
    shm = None
    mm = None
    video = None
    shm_name = f"/vid_frames_{uuid}"

    try:      
        # --- Chargement des frames en mémoire partagée ---
        record_manager = CameraRecordManager(cameraDB)
        total_size = await record_manager.size(uuid, start_ts, end_ts)
        shm_size = int(total_size * 1.5)

        try:
            shm = posix_ipc.SharedMemory(
                shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
            )
        except posix_ipc.ExistentialError:
            existing = posix_ipc.SharedMemory(shm_name, flags=0)
            existing.close_fd()
            try:
                existing.unlink()
            except posix_ipc.ExistentialError:
                pass
            shm = posix_ipc.SharedMemory(
                shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
            )

        mm = mmap.mmap(shm.fd, shm_size)
        queries = record_manager.query(uuid, start_ts, end_ts)
        
        if not start_ts:
            start_ts = record_manager.oldest_ts
        if not end_ts:
            end_ts = record_manager.latest_ts        
    
        offset = 0
        frame_sizes = []
        total = 0
        async for record in queries:
            frame_bytes = await record.read_all()
            frame_size = len(frame_bytes)

            mm[offset:offset + frame_size] = frame_bytes
            frame_sizes.append(frame_size)
            offset += frame_size
            total +=1

        if not frame_sizes:
            return {"status": "error", "message": "Aucune frame trouvée"}


        # --- Génération du MP4 ---
        max_video_bytes = max_video_size_mb * 1024 * 1024 if max_video_size_mb > 0 else 0
        
        ts_s = unix_timestamp_to_iso(start_ts)
        video_path = os.path.join(
            settings.EXPORTS_LOCAL_PATH, 'videos', f"{uuid}_{ts_s}.{opencv_video_type}"
        )
        os.makedirs(os.path.dirname(video_path), exist_ok=True)

        fourcc = cv2.VideoWriter_fourcc(*opencv_fourcc_format)
        skipped = 0
        written = 0
        current_offset = 0

        i=0
        for idx, size in enumerate(frame_sizes):
            # Vérification de la taille du MP4 en cours
            if (
                max_video_bytes
                and video is not None
                and os.path.exists(video_path)
                and os.path.getsize(video_path) >= max_video_bytes
            ):
                skipped += len(frame_sizes) - idx
                logger.warning(
                    "export_video_mp4: limite %d Mo atteinte, %d frames ignorées",
                    max_video_size_mb,
                    len(frame_sizes) - idx,
                )
                break

            frame_bytes = mm[current_offset:current_offset + size]
            nparr = np.frombuffer(frame_bytes, np.uint8)
            frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

            if frame is None:
                current_offset += size
                skipped += 1
                continue

            # Redimensionnement optionnel
            if max_width > 0 or max_height > 0:
                frame = _resize_frame(frame, max_width, max_height)

            # Initialisation du VideoWriter sur la première frame valide
            if video is None:
                h, w, _ = frame.shape
                video = cv2.VideoWriter(video_path, fourcc, frame_rate, (w, h))

            video.write(frame)
            written += 1
            current_offset += size

            progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
            i+=1

        if video:
            video.release()

        if not os.path.exists(video_path):
            return {"status": "error", "message": f"Fichier {opencv_video_type} non créé"}
        
        ## Copie vers les destinations (local + Samba)
        #filename = os.path.basename(video_path)
        #destinations = _copy_to_destinations(video_path, filename)
        return {
            "status": "success",
            "video_path": video_path,
            "frames_written": written,
            "frames_skipped": skipped,
            "frame_rate": frame_rate,
            "file_size_mb": round(os.path.getsize(video_path) / 1024 / 1024, 2),
            #"destinations": destinations,
        }

    except Exception as exc:
        logger.error("export_video_mp4 [%s]: %s", uuid, exc, exc_info=True)
        return {"status": "error", "message": str(exc)}

    finally:
        if mm:
            mm.close()
        if shm:
            try:
                shm.unlink()
            except posix_ipc.ExistentialError:
                pass


# ─────────────────────────────────────────────
# Utilitaire commun
# ─────────────────────────────────────────────
def _resize_frame(
    frame: np.ndarray, max_width: int, max_height: int
) -> np.ndarray:
    """
    Redimensionne une frame en conservant le ratio si un max est dépassé.
    max_width ou max_height à 0 signifie sans contrainte sur cet axe.
    """
    h, w = frame.shape[:2]
    scale = 1.0

    if max_width > 0 and w > max_width:
        scale = min(scale, max_width / w)
    if max_height > 0 and h > max_height:
        scale = min(scale, max_height / h)

    if scale < 1.0:
        new_w = int(w * scale)
        new_h = int(h * scale)
        frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)

    return frame



