import yaml
import uuid
import logging
import numpy as np
from ..utils import (
    expand_camera_selector, 
    parse_frame_output_spec, 
    parse_frame_outputs,
    generate_rtsp_strings
)

_logger = logging.getLogger(__name__)

class StreamingConfigManager:
    """
    Handles loading, validation, and processing of streaming configurations.
    """
    def __init__(self, config):
        self.cfg = self._load_raw_config(config)
        self._validate_basic_structure()
        
        # Core parameters
        param = self.cfg.get('parameter', {}) or {}
        self.candidate = bool(param.get('CANDIDATE', False))
        self.gst_enable = bool(param.get('GSTREAMER', False))
        self.backend = param.get('BACKEND', None)
        if self.backend is None:
            self.backend = 'gst' if self.gst_enable else 'opencv'
        self.codec = param.get('CODEC', 'h264')
        
        # FFmpeg parameters
        ffmpeg_param = param.get('FFMPEG', {}) or {}
        self.ffmpeg_fps = int(ffmpeg_param.get('FPS', 5))
        self.ffmpeg_hwaccel = bool(ffmpeg_param.get('HWACCEL', True))
        self.ffmpeg_fallback = bool(ffmpeg_param.get('FALLBACK', False))
        self.ffmpeg_zero_copy = bool(ffmpeg_param.get('ZERO_COPY', True))
        
        # Frame outputs
        self.frame_outputs = self._parse_frame_outputs(param)
        
        # Initialize cameras
        self.cameras = {}
        self._id_to_item_id = {}
        self._id_to_idx = {}
        self._init_cameras()
        
        # Derived data
        if self.frame_outputs:
            self._derive_camera_branches()
            self._validate_frame_outputs()
            self._prepare_output_mappings()
        else:
            # Default fallback if no frame_outputs or WIDTH/HEIGHT specified
            w = int(param.get('WIDTH') or 1920)
            h = int(param.get('HEIGHT') or 1080)
            default_branches = {1: {'WIDTH': w, 'HEIGHT': h, 'COLOR': 'BGR', 'LETTERBOX': False}}
            for item_id in self.cameras.keys():
                # Ensure we have branch config (at least one)
                self.cameras[item_id]['branches'] = self.cameras[item_id].get('branches') or default_branches.copy()
            
            self.output_mappings = {}
            self.config = [[(h, w)]]
            
        # Generate RTSP strings using utils
        generate_rtsp_strings(self.cameras, self.backend)

    def _load_raw_config(self, config):
        if isinstance(config, str):
            with open(config, 'r') as f:
                return yaml.safe_load(f)
        elif isinstance(config, dict):
            return config
        else:
            raise TypeError('Config must be provided as a yaml file path or dictionary.')

    def _validate_basic_structure(self):
        if 'cameras' not in self.cfg or not isinstance(self.cfg['cameras'], list):
            raise ValueError("The 'cameras' section must be a list and cannot be empty.")

    def _parse_frame_outputs(self, param):
        frame_outputs_raw = param.get('frame_outputs', None)
        if not frame_outputs_raw:
            w, h = param.get('WIDTH'), param.get('HEIGHT')
            if w and h:
                # Use (height, width) order to match parse_frame_output_spec
                return {1: [f"*:({int(h)},{int(w)}),bgr"]}
            return {}
        return parse_frame_outputs(frame_outputs_raw)

    def _init_cameras(self):
        for idx, cam in enumerate(self.cfg['cameras']):
            item_id = cam.get('item_id', str(uuid.uuid4()))
            cam['item_id'] = item_id
            
            cam_id = cam.get('_id', idx)
            cam['_id'] = cam_id
            
            if cam_id in self._id_to_item_id:
                _logger.warning(f"Duplicate _id found: {cam_id}. Overwriting...")
            self._id_to_item_id[cam_id] = item_id
            self._id_to_idx[cam_id] = idx
            
            if 'codec' not in cam:
                cam['codec'] = self.codec
            
            if 'rtsp' not in cam:
                ip = cam.get('ip')
                cam['rtsp'] = [{'ip': ip, 'user': cam.get('user'), 'pw': cam.get('pw'), 'codec': cam['codec']}]
            else:
                # Ensure each RTSP entry inherits the camera's codec and credentials if not specified
                for rtsp_entry in cam['rtsp']:
                    if 'codec' not in rtsp_entry:
                        rtsp_entry['codec'] = cam.get('codec', self.codec)
                    if 'user' not in rtsp_entry:
                        rtsp_entry['user'] = cam.get('user')
                    if 'pw' not in rtsp_entry:
                        rtsp_entry['pw'] = cam.get('pw')
            
            # Initial state
            cam['status'] = False
            cam['rtsp_strings'] = []
            cam['rtsp_reconnect'] = None
            cam['cap'] = None
            cam['cap_info'] = {}
            self.cameras[item_id] = cam

    def _derive_camera_branches(self):
        camera_configs = {}
        for output_id, specs in self.frame_outputs.items():
            for spec_str in specs:
                selector, w, h, color, lb = parse_frame_output_spec(spec_str)
                cam_ids = expand_camera_selector(selector, self._id_to_item_id)
                config_tuple = (w, h, color, lb)
                for cid in cam_ids:
                    item_id = self._id_to_item_id[cid]
                    if item_id not in camera_configs: camera_configs[item_id] = []
                    if config_tuple not in camera_configs[item_id]:
                        camera_configs[item_id].append(config_tuple)
        
        w_def = int(self.cfg.get('parameter', {}).get('WIDTH') or 1920)
        h_def = int(self.cfg.get('parameter', {}).get('HEIGHT') or 1080)

        for item_id in self.cameras.keys():
            configs = camera_configs.get(item_id, [])
            if not configs:
                # If camera not in any frame_outputs, give it a default branch
                branches = {1: {'WIDTH': w_def, 'HEIGHT': h_def, 'COLOR': 'BGR', 'LETTERBOX': False}}
            else:
                branches = {}
                for bid, cfg in enumerate(configs, start=1):
                    branches[bid] = {'WIDTH': cfg[0], 'HEIGHT': cfg[1], 'COLOR': cfg[2], 'LETTERBOX': cfg[3]}
            self.cameras[item_id]['branches'] = branches

    def _validate_frame_outputs(self):
        for output_id, specs in self.frame_outputs.items():
            for spec_str in specs:
                selector, w, h, color, lb = parse_frame_output_spec(spec_str)
                cam_ids = expand_camera_selector(selector, self._id_to_item_id)
                config_tuple = (w, h, color, lb)
                for cid in cam_ids:
                    item_id = self._id_to_item_id[cid]
                    branches = self.cameras[item_id].get('branches', {})
                    if not any((b['WIDTH'], b['HEIGHT'], b['COLOR'], b['LETTERBOX']) == config_tuple for b in branches.values()):
                        raise ValueError(f"Config {config_tuple} not found for camera {item_id}")

    def _prepare_output_mappings(self):
        self.output_mappings = {}
        self.config = []
        for output_id in sorted(self.frame_outputs.keys()):
            specs = self.frame_outputs[output_id]
            self.output_mappings[output_id] = []
            shapes = []
            for spec_str in specs:
                selector, w, h, color, lb = parse_frame_output_spec(spec_str)
                cam_ids = expand_camera_selector(selector, self._id_to_item_id)
                config_tuple = (w, h, color, lb)
                shapes.append((h, w))
                
                src_list = []
                for cid in cam_ids:
                    item_id = self._id_to_item_id[cid]
                    branches = self.cameras[item_id].get('branches', {})
                    for bid, bc in branches.items():
                        if (bc['WIDTH'], bc['HEIGHT'], bc['COLOR'], bc['LETTERBOX']) == config_tuple:
                            src_list.append((item_id, bid))
                            break
                
                self.output_mappings[output_id].append({
                    'h': h, 'w': w, 'sources': src_list
                })
            self.config.append(shapes)