# radar_data_reader/core/file_reader.py

"""
Worker process logic for reading and parsing a stream of data blocks from radar files.
This version uses a fast, marker-based scan to identify data blocks within standard .out files.
"""
import multiprocessing as mp
import logging
from pathlib import Path
from typing import Iterator, Tuple, List, Optional, Dict
import queue
import cProfile
import pstats
from collections import defaultdict
import json

import numpy as np


from . import data_structures as ds
from .struct_parser import parse_block
from .struct_utils import convert_ctypes_to_dict
from ..core.export_profiles import ExportProfile
from ..utils import logger

log = logger.get_logger(__name__)

# --- Block Markers based on IDL script analysis ---
LEGACY_BLOCK_MARKER = 0x5A5A5A5A
FW_BLOCK_MARKER = 0x7A7A7A7A

# --- Offsets within the block headers ---
LEGACY_NAME_OFFSET_WORDS = 17
LEGACY_SIZE_OFFSET_WORDS = 5
FW_NAME_OFFSET_WORDS = 2
FW_SIZE_OFFSET_WORDS = 5
FW_HEADER_SIZE_WORDS = 8

WORKER_LOG_FILE = "worker_process.log"


def _setup_worker_logging() -> logging.Logger:
    """Configures a dedicated logger for the worker process."""
    worker_log = logging.getLogger("radar_data_reader.worker")
    worker_log.setLevel(logging.DEBUG)
    worker_log.propagate = False
    if worker_log.hasHandlers():
        worker_log.handlers.clear()
    file_handler = logging.FileHandler(WORKER_LOG_FILE, mode="w", encoding="utf-8")
    formatter = logging.Formatter(
        "%(asctime)s [%(levelname)-8s] (Worker-PID:%(process)d): %(message)s",
        "%H:%M:%S",
    )
    file_handler.setFormatter(formatter)
    worker_log.addHandler(file_handler)
    worker_log.info(f"Dedicated worker logging configured. Output will be in {WORKER_LOG_FILE}")
    return worker_log


def run_worker_process(
    filepath: Path,
    command_queue: mp.Queue,
    result_queue: mp.Queue,
    profile: ExportProfile,
    enable_profiling: bool = False,
    debug_json_dump: bool = False,
):
    """Main entry point for the worker process."""
    log = _setup_worker_logging()

    profiler = None
    if enable_profiling:
        log.info("Python worker profiling is ENABLED.")
        profiler = cProfile.Profile()
        profiler.enable()
    else:
        log.info("Python worker profiling is DISABLED.")

    debug_data = defaultdict(list) if debug_json_dump else None
    if debug_json_dump:
        log.info("Structure debug dump is ENABLED. JSON files will be created upon completion.")

    stats: Dict[str, int] = defaultdict(int)
    file_handler = log.handlers[0] if log.handlers else None

    try:
        log.info(f"[Worker-{mp.current_process().pid}] Started for file: {filepath.name}")
        reader = RadarFileReader(filepath, log)
        if not reader.load_and_find_blocks():
            result_queue.put({"type": "error", "message": f"Failed to load data from {filepath.name}."})
            return

        total_blocks = len(reader.block_metadata or [])
        stats["total_blocks_scanned"] = total_blocks
        result_queue.put({"type": "start", "total": total_blocks})

        interrupted = False
        batch_generator = reader.process_and_generate_batches(stats, debug_json_dump, debug_data)

        for batch, blocks_done_count in batch_generator:
            try:
                if not command_queue.empty() and command_queue.get_nowait() == "STOP":
                    log.warning(f"[Worker-{mp.current_process().pid}] Stop command received.")
                    interrupted = True
                    break
            except queue.Empty:
                pass
            result_queue.put({"type": "data_batch_fragment", "data": batch, "blocks_done": blocks_done_count})

        # After processing, write debug files if enabled
        if debug_json_dump and debug_data:
            log.info(f"Dumping debug data to JSON files in {filepath.parent}")
            for block_name, data_list in debug_data.items():
                # Sanitize block name for use in filename
                safe_block_name = block_name.replace("/", "_").replace("\\", "_")
                dump_path = filepath.parent / f"{safe_block_name}_debug_dump.json"
                try:
                    with open(dump_path, "w", encoding="utf-8") as f:
                        json.dump(data_list, f, indent=4)
                    log.info(f"Successfully wrote {len(data_list)} records to {dump_path}")
                except Exception as e:
                    log.error(f"Failed to write debug dump for {block_name}: {e}")

        result_queue.put({"type": "complete", "interrupted": interrupted, "stats": dict(stats)})
        log.info(f"[Worker-{mp.current_process().pid}] Processing finished.")

    except Exception as e:
        log.error(f"[Worker-{mp.current_process().pid}] Unhandled exception: {e}", exc_info=True)
        result_queue.put({"type": "error", "message": f"Worker failed: {e}"})

    finally:
        log.info("Worker process shutting down.")
        if profiler:
            profiler.disable()
            pid = mp.current_process().pid
            profile_filename = f"profile_output_{pid}.prof"
            profiler.dump_stats(profile_filename)
            log.info(f"Profiling data saved to {profile_filename}")
        if file_handler:
            log.removeHandler(file_handler)
            file_handler.close()


class RadarFileReader:
    """Reads a radar data file and yields structured DataBatch fragments."""
    def __init__(self, file_path: Path, log: logging.Logger):
        self.file_path = file_path
        self.log = log
        self.data_vector: Optional[np.ndarray] = None
        self.block_metadata: List[Tuple[int, int, str]] = []

    def _parse_legacy_block_header(self, start_index: int) -> Optional[Tuple[int, int, str]]:
        try:
            if start_index + LEGACY_NAME_OFFSET_WORDS >= len(self.data_vector): return None
            name_id = self.data_vector[start_index + LEGACY_NAME_OFFSET_WORDS]
            if name_id not in ds.BLOCK_TYPE_MAP: return None
            block_name = ds.BLOCK_TYPE_MAP[name_id]
            payload_size_bytes = self.data_vector[start_index + LEGACY_SIZE_OFFSET_WORDS]
            if not (0 < payload_size_bytes < (20 * 1024 * 1024)): return None
            return start_index, (payload_size_bytes + 3) // 4, block_name
        except IndexError:
            return None

    def _parse_fw_block_header(self, start_index: int) -> Optional[Tuple[int, int, str]]:
        try:
            if start_index + FW_NAME_OFFSET_WORDS >= len(self.data_vector): return None
            name_id = self.data_vector[start_index + FW_NAME_OFFSET_WORDS]
            if name_id not in ds.BLOCK_TYPE_MAP: return None
            block_name = ds.BLOCK_TYPE_MAP[name_id]
            payload_size_bytes = self.data_vector[start_index + FW_SIZE_OFFSET_WORDS]
            if not (0 < payload_size_bytes < (20 * 1024 * 1024)): return None
            payload_start_offset = start_index + FW_HEADER_SIZE_WORDS
            return payload_start_offset, (payload_size_bytes + 3) // 4, block_name
        except IndexError:
            return None

    def load_and_find_blocks(self) -> bool:
        """Scans the file for blocks using a fast, marker-based method."""
        try:
            self.log.info(f"Loading data from {self.file_path}...")
            self.data_vector = np.fromfile(str(self.file_path), dtype="<u4")
            self.log.info(f"Loaded {self.data_vector.size} 32-bit words.")

            legacy_indices = np.where(self.data_vector == LEGACY_BLOCK_MARKER)[0]
            fw_indices = np.where(self.data_vector == FW_BLOCK_MARKER)[0]
            legacy_starts = legacy_indices[np.where(np.diff(legacy_indices) == 1)[0]]
            fw_starts = fw_indices[np.where(np.diff(fw_indices) == 1)[0]]

            all_starts = sorted(list(set(legacy_starts) | set(fw_starts)))
            for start_index in all_starts:
                block_type = self.data_vector[start_index]
                if block_type == LEGACY_BLOCK_MARKER:
                    parsed_meta = self._parse_legacy_block_header(start_index)
                else: # FW_BLOCK_MARKER
                    parsed_meta = self._parse_fw_block_header(start_index)
                if parsed_meta:
                    self.block_metadata.append(parsed_meta)

            if not self.block_metadata:
                self.log.error("Marker-based scan failed to identify any valid data block headers.")
                return False
            self.log.info(f"Successfully identified {len(self.block_metadata)} total valid data blocks.")
            return True
        except Exception as e:
            self.log.error(f"Failed to load or find blocks: {e}", exc_info=True)
            return False

    def process_and_generate_batches(
        self,
        stats: Dict[str, int],
        debug_json_dump: bool = False,
        debug_data: Optional[Dict[str, list]] = None
    ) -> Iterator[Tuple[ds.DataBatch, int]]:
        """Parses blocks and yields DataBatch fragments, associating tracking data correctly."""
        if not self.block_metadata or self.data_vector is None:
            return

        current_batch: Optional[ds.DataBatch] = None
        last_header: Optional[ds.DspHeaderIn] = None
        last_stt_block: Optional[ds.SttBlock] = None

        for block_num, (start_offset, size_words, block_name) in enumerate(self.block_metadata):
            block_data = self.data_vector[start_offset : start_offset + size_words]
            block_id = self.data_vector[start_offset + LEGACY_NAME_OFFSET_WORDS] if block_name not in ["EXP", "PC", "DET", "TIMER", "SOFT"] else 0

            parsed_block = parse_block(block_id, block_data, last_header, block_name_override=block_name)

            if parsed_block is None:
                stats["failed_to_parse_blocks"] += 1
                continue
            
            if debug_json_dump and debug_data is not None:
                try:
                    dict_representation = convert_ctypes_to_dict(parsed_block)
                    debug_data[parsed_block.block_name].append(dict_representation)
                except Exception as e:
                    self.log.error(f"Failed to create debug dictionary for {parsed_block.block_name}: {e}")

            stats[f"found_{parsed_block.block_name.split('_')[0]}"] += 1

            if isinstance(parsed_block, ds.SttBlock):
                last_stt_block = parsed_block
                continue # Non aggiungerlo al batch corrente, verrà associato al prossimo

            if isinstance(parsed_block, ds.DspHeaderIn):
                if current_batch:
                    # Associa i dati di tracking raccolti finora al batch che stiamo per chiudere
                    if last_stt_block:
                        current_batch.stt_data = last_stt_block
                        current_batch.blocks.append(last_stt_block)
                        last_stt_block = None
                    yield current_batch, block_num

                current_batch = ds.DataBatch(batch_id=0) # L'ID finale viene assegnato dal controller
                current_batch.blocks.append(parsed_block)
                last_header = parsed_block
            elif current_batch:
                current_batch.blocks.append(parsed_block)
                # Popola gli altri campi di convenienza
                if isinstance(parsed_block, ds.CdpStsBlock): current_batch.cdp_sts_results = parsed_block
                elif isinstance(parsed_block, ds.TimerBlock): current_batch.timer_data = parsed_block
                elif isinstance(parsed_block, ds.AesaBlock): current_batch.aesa_data = parsed_block
                elif isinstance(parsed_block, ds.D1553Block): current_batch.d1553_data = parsed_block
        
        if current_batch:
            if last_stt_block:
                current_batch.stt_data = last_stt_block
                current_batch.blocks.append(last_stt_block)
            yield current_batch, len(self.block_metadata)