import os
import gc
import glob
import random
import tempfile
import cv2
import time
import yaml
import uuid
import logging
import threading
import numpy as np
import queue

from .manager import StreamingConfigManager, ConnectionManager

cv2.setNumThreads(0)
_logger = logging.getLogger(__name__)

class MultiStreaming:
    """
    A class to handle multi-camera streaming with automatic reconnection, frame resizing and backend abstraction.
    """
    def __init__(
        self, 
        config=None, 
        reconnect_interval=30,
        logger=None,
        verbose=True
    ):
        self.reconnect_interval = reconnect_interval
        if not verbose:
            _logger.setLevel(logging.ERROR)
        self.logger = logger or _logger
        
        if config:
            self.manager = StreamingConfigManager(config)
            # Pull core properties from manager for accessibility
            self.cameras = self.manager.cameras
            self.backend = self.manager.backend
            self.frame_outputs = self.manager.frame_outputs
            self.output_mappings = self.manager.output_mappings
            self.config = self.manager.config
            
            # Additional parameters
            self.candidate = self.manager.candidate
            self.ffmpeg_fps = self.manager.ffmpeg_fps
            self.ffmpeg_hwaccel = self.manager.ffmpeg_hwaccel
            self.ffmpeg_fallback = self.manager.ffmpeg_fallback
            self.ffmpeg_zero_copy = self.manager.ffmpeg_zero_copy
        else:
            raise ValueError('Configuration must be provided.')
        
        self.running = True
        self.mutex = threading.Lock()
        self.threads = []
        
        # Initialize frames structure: {item_id: {branch_id: frame_array}}
        self.frames = {}
        for item_id, cam in self.cameras.items():
            self.frames[item_id] = {}
            for branch_id, branch_config in cam.get('branches', {}).items():
                h = branch_config['HEIGHT']
                w = branch_config['WIDTH']
                self.frames[item_id][branch_id] = np.zeros((h, w, 3), dtype=np.uint8)
        
        self.id_to_idx = {cam['item_id']: i for i, cam in enumerate(self.cameras.values())}

        # --- Connection Manager Setup ---
        ffmpeg_settings = {
            'fps': self.ffmpeg_fps,
            'hwaccel': self.ffmpeg_hwaccel,
            'fallback': self.ffmpeg_fallback,
            'zero_copy': self.ffmpeg_zero_copy
        }
        self.connection_manager = ConnectionManager(
            backend=self.backend,
            ffmpeg_settings=ffmpeg_settings,
            logger=self.logger,
            candidate=self.candidate
        )
        self.connection_manager.start_scheduler(self.mutex)
        
    def run(self):
        for cam in self.cameras.values():
            t = threading.Thread(target=self._run_one, args=(cam,), daemon=True)
            t.start()
            self.threads.append(t)

    def get_frames(self, copy=True):
        with self.mutex:
            if copy:
                frames_src = {iid: {bid: f.copy() for bid, f in bs.items()} for iid, bs in self.frames.items()}
            else:
                frames_src = self.frames
            
            if not self.frame_outputs: return frames_src
            return self.organize_outputs(frames_src)

    def _run_one(self, cam):
        self.connection_manager.init_camera(cam)
        cap = cam.get('cap')
        reconnect_time = time.time() - self.reconnect_interval
        fail = 0
        jitter = random.uniform(1, self.reconnect_interval / 5)
        
        try:
            while self.running:
                # 1. Check if the scheduler just finished our reconnection
                if cam.get('cap') is not cap:
                    _logger.info(f"Cam {cam['_id']}: New connection assigned by scheduler. Resuming...")
                    cap = cam['cap']
                    fail = 0
                    reconnect_time = time.time()
                    jitter = random.uniform(1, self.reconnect_interval / 5)

                # 2. Wait if we are in the reconnection queue
                if cam.get('pending_reconnect'):
                    time.sleep(1)
                    continue

                # 3. Try to read a frame
                if cap and cap.isOpened() and self._read_and_process(cam, cap):
                    fail = 0 
                    continue
                
                # 4. Handle Failure
                fail += 1
                with self.mutex:
                    cam['status'] = False
                
                # 5. Request reconnection from scheduler
                if fail > 10 and (time.time() - reconnect_time > self.reconnect_interval+jitter):
                    self.connection_manager.request_reconnect(cam)
                time.sleep(0.2)
        finally:
            if cap: cap.release()

    def _read_and_process(self, cam, cap):
        """Read from capture and update internal frames. Returns True on success."""
        # Generic call: all backends now support get_branches()
        ret, frames_dict = cap.get_branches()
        if not ret: return False
        
        with self.mutex:
            iid = cam['item_id']
            for bid, frame in frames_dict.items():
                if bid in self.frames.get(iid, {}):
                    self.frames[iid][bid] = frame
            cam['status'] = True
        return True

    def organize_outputs(self, frames_by_camera_branch):
        """Organize frames into batched numpy arrays or lists based on pre-calculated mappings."""
        final_outputs = []
        
        for output_id in sorted(self.output_mappings.keys()):
            mapping_specs = self.output_mappings[output_id]
            spec_arrays = []
            
            for m in mapping_specs:
                frames = [frames_by_camera_branch[iid][bid] for iid, bid in m['sources']]
                
                if frames:
                    spec_arrays.append(np.stack(frames, axis=0))
                else:
                    spec_arrays.append(np.zeros((0, m['h'], m['w'], 3), dtype=np.uint8))
            
            # If a single spec was requested, return the array; otherwise return the list of arrays
            final_outputs.append(spec_arrays[0] if len(spec_arrays) == 1 else spec_arrays)
            
        return final_outputs[0] if len(final_outputs) == 1 else tuple(final_outputs)

    def get_status(self):
        with self.mutex: 
            return [cam['status'] for cam in self.cameras.values()]

    def release(self):
        self.running = False
        self.connection_manager.stop()
        time.sleep(0.1)
        for cam in self.cameras.values():
            if cam['cap']: cam['cap'].release()
        for t in self.threads: t.join()
        self.threads.clear()
