# radar_data_reader/core/flight_analyzer.py

"""
Contains the business logic for the Flight Analysis feature.
This class handles scanning folders, running the C++ analyzer, parsing results,
and generating summary artifacts.
"""

import multiprocessing as mp
import threading
import csv
import re
from pathlib import Path
from typing import List, Optional
from datetime import timedelta
from collections import defaultdict

try:
    import pandas as pd
except ImportError:
    pd = None

from ..utils import logger
from ..utils.config_manager import ConfigManager
from ..core.cpp_runner import run_cpp_converter

log = logger.get_logger(__name__)

TICK_DURATION_S = 64e-6


class FlightAnalyzer:
    """Manages the multi-step process of analyzing a flight folder."""

    def __init__(self, config_manager: ConfigManager, result_queue: mp.Queue):
        self.config_manager = config_manager
        self.result_queue = result_queue
        self.worker_process: Optional[mp.Process] = None
        self.current_flight_folder_path: Optional[Path] = None
        self.current_flight_name: Optional[str] = None
        self.analysis_options: dict = {}

    def start_analysis(
        self,
        rec_folder_str: str,
        flight_name: str,
        workspace_path: Path,
        analysis_options: dict,
    ) -> threading.Thread:
        self.current_flight_name = flight_name
        self.analysis_options = analysis_options
        analysis_thread = threading.Thread(
            target=self._flight_analysis_orchestrator,
            args=(rec_folder_str, flight_name, workspace_path),
            daemon=True,
        )
        analysis_thread.start()
        return analysis_thread

    def _flight_analysis_orchestrator(self, rec_folder_str: str, flight_name: str, flight_dir: Path):
        """
        Orchestrates the entire flight analysis process.
        This method now groups .rec files by session, runs the C++ analyzer for each group,
        and then combines the results for final processing.
        """
        self.current_flight_folder_path = None
        try:
            flight_dir.mkdir(parents=True, exist_ok=True)
            self.current_flight_folder_path = flight_dir

            cpp_config = self.config_manager.get_cpp_converter_config()
            exe_path = cpp_config.get("cpp_executable_path")
            if not exe_path or not Path(exe_path).is_file():
                raise ValueError(f"C++ executable not found at path: {exe_path}")

            rec_files = sorted(Path(rec_folder_str).glob("*.rec"))
            if not rec_files:
                raise FileNotFoundError("No .rec files found in the specified folder.")

            # Group files by a common base name to handle multiple recording sessions.
            # The base name is determined by splitting the filename before the last underscore,
            # which typically separates the descriptive part from the sequence number.
            file_groups = defaultdict(list)
            for f in rec_files:
                base_name = f.name.rsplit('_', 1)[0]
                file_groups[base_name].append(f)

            log.info(f"Found {len(rec_files)} .rec files, grouped into {len(file_groups)} recording session(s).")

            all_storyboard_dfs = []
            for group_name, group_files in file_groups.items():
                log.info(f"Processing group '{group_name}' with {len(group_files)} file(s)...")
                
                first_rec_path = group_files[0]
                num_files_to_process = len(group_files)

                command_list = [
                    str(exe_path),
                    str(first_rec_path),
                    f"/n={num_files_to_process}",
                    "/p=1",
                    "/a",
                    '/vsave',
                    '/vshow',
                ]

                log.info(f"Running g_reconverter for group '{group_name}': {' '.join(command_list)}")

                # Run the C++ process for the current group.
                # This is a blocking call to ensure sequential processing of groups.
                self.worker_process = mp.Process(
                    target=run_cpp_converter,
                    args=(command_list, self.result_queue, str(flight_dir), True),
                    daemon=True,
                )
                self.worker_process.start()
                self.worker_process.join()
                log.info(f"C++ analysis finished for group '{group_name}'.")

                # After the C++ process completes, parse its output.
                # The output file is expected to be in the flight_dir.
                summary_files = [f for f in flight_dir.glob("pp-*.txt") if "aesa" not in f.name.lower()]
                if not summary_files:
                    log.warning(f"No summary file found for group '{group_name}'. Skipping.")
                    continue

                summary_txt_path = summary_files[0]
                log.info(f"Parsing summary file for group: {summary_txt_path.name}")
                
                storyboard_df_group = self._parse_storyboard_from_txt(summary_txt_path)
                if storyboard_df_group is not None and not storyboard_df_group.empty:
                    all_storyboard_dfs.append(storyboard_df_group)
                
                # Clean up the output file to prevent it from being used by the next iteration.
                try:
                    summary_txt_path.unlink()
                except OSError as e:
                    log.warning(f"Could not delete temporary summary file {summary_txt_path}: {e}")


            if not all_storyboard_dfs:
                raise ValueError("Analysis failed: No storyboard data could be parsed from any file group.")

            # Combine all dataframes from all groups into one.
            log.info(f"Combining {len(all_storyboard_dfs)} storyboard segment(s) into a single flight storyboard.")
            storyboard_df = pd.concat(all_storyboard_dfs, ignore_index=True)
            
            # Sort the combined dataframe by batch number to ensure correct chronological order.
            storyboard_df.sort_values(by="Batch", inplace=True)
            storyboard_df.reset_index(drop=True, inplace=True)

            # --- Start of final analysis steps (previously in handle_final_analysis_steps) ---
            log.info("C++ processing complete for all groups. Starting final Python-side analysis...")

            # Save the final, combined storyboard.
            self._save_storyboard_artifacts(storyboard_df, flight_dir)

            summary_df = self._create_and_save_summary(storyboard_df, flight_dir, self.analysis_options)
            self._create_flight_report_txt(summary_df, flight_dir)

            self.result_queue.put({
                "type": "analysis_summary_data",
                "data": summary_df,
                "flight_folder_path": flight_dir
            })

            log.info("Flight analysis complete. All artifacts saved.")
            self.result_queue.put({"type": "complete", "message": "Analysis successful."})

        except Exception as e:
            log.error(f"Flight analysis orchestrator failed: {e}", exc_info=True)
            self.result_queue.put({"type": "error", "message": str(e)})

    def handle_final_analysis_steps(self):
        """
        This method is now a placeholder.
        The logic has been moved into _flight_analysis_orchestrator to ensure
        sequential execution after all C++ processes are complete.
        """
        log.info("Final analysis steps are now integrated into the main orchestrator.")
        pass

    def _make_columns_unique(self, columns: List[str]) -> List[str]:
        final_cols, counts = [], {}
        for col in columns:
            if col in counts:
                counts[col] += 1
                final_cols.append(f"{col}.{counts[col]}")
            else:
                counts[col] = 1
                final_cols.append(col)
        return final_cols

    def _parse_storyboard_from_txt(self, txt_path: Path) -> Optional["pd.DataFrame"]:
        """Parses a storyboard TXT file into a pandas DataFrame."""
        if pd is None:
            log.error("Pandas library is not installed, cannot parse storyboard.")
            return None
        try:
            with open(txt_path, "r", encoding="utf-8", errors="ignore") as f:
                header_line = f.readline().strip()
                raw_columns = [h.strip() for h in header_line.split(";") if h.strip()]
                unique_column_names = self._make_columns_unique(raw_columns)
            
            storyboard_df = pd.read_csv(
                txt_path,
                sep=';',
                header=0,
                names=unique_column_names,
                on_bad_lines='skip',
                encoding='utf-8',
                encoding_errors='ignore'
            )

            for col in storyboard_df.select_dtypes(include=['object']).columns:
                storyboard_df[col] = storyboard_df[col].str.strip()

            # --- MODIFICATION START ---
            # Correct truncated .re filenames from the C++ tool's output to .rec
            if 'file' in storyboard_df.columns:
                storyboard_df['file'] = storyboard_df['file'].apply(
                    lambda x: x + 'c' if isinstance(x, str) and x.endswith('.re') else x
                )
            # --- MODIFICATION END ---

            numeric_cols = ["Batch", "TTAG"]
            for col in numeric_cols:
                if col in storyboard_df.columns:
                    storyboard_df[col] = pd.to_numeric(storyboard_df[col], errors="coerce")
            
            storyboard_df.dropna(subset=["Batch", "TTAG"], inplace=True)
            storyboard_df["Batch"] = storyboard_df["Batch"].astype(int)
            storyboard_df["TTAG"] = storyboard_df["TTAG"].astype(int)

            if storyboard_df.empty:
                log.warning(f"DataFrame is empty after cleaning {txt_path.name}")
                return None
            
            return storyboard_df

        except Exception as e:
            log.error(f"Failed to read or process summary file {txt_path.name}: {e}")
            return None

    def _save_storyboard_artifacts(self, storyboard_df: "pd.DataFrame", output_dir: Path):
        """Saves the final storyboard DataFrame to CSV and JSON."""
        csv_path = output_dir / "flight_storyboard.csv"
        json_path = output_dir / "flight_storyboard.json"
        
        log.info(f"Saving final combined storyboard to {csv_path}")
        storyboard_df.to_csv(csv_path, index=False)
        
        log.info(f"Saving final combined storyboard to {json_path}")
        storyboard_df.to_json(json_path, orient="records", indent=4)

    def _parse_and_save_storyboard(
        self,
        txt_path: Path,
        output_dir: Path,
    ) -> Optional["pd.DataFrame"]:
        """
        Parses a storyboard file and saves it to CSV and JSON.
        This method now uses helper functions for parsing and saving.
        """
        storyboard_df = self._parse_storyboard_from_txt(txt_path)
        if storyboard_df is None or storyboard_df.empty:
            return None
        
        self._save_storyboard_artifacts(storyboard_df, output_dir)
        return storyboard_df

    def _create_and_save_summary(
        self,
        storyboard_df: "pd.DataFrame",
        output_dir: Path,
        options: dict,
    ) -> "pd.DataFrame":
        df = storyboard_df.copy()
        
        agg_by_scale = options.get("aggregate_by_scale", True)
        agg_by_waveform = options.get("aggregate_by_waveform", True)

        # Costruzione dinamica della chiave di stato
        status_components = []
        
        mode_part = df.get('Mode', pd.Series(index=df.index, dtype=str)).astype(str).str.strip().replace('', 'N/A')
        submode_part = df.get('Mode.3', pd.Series(index=df.index, dtype=str)).astype(str).str.strip().replace('', 'N/A')
        
        # Unisci sempre Mode e Submode
        full_mode_part = mode_part + "-" + submode_part
        status_components.append(full_mode_part)
        
        if agg_by_scale:
            scale_part = df.get('Scal.2', pd.Series(index=df.index, dtype=str)).astype(str).str.strip().replace('', 'N/A')
            status_components.append(scale_part)
            
        if agg_by_waveform:
            wf_part1 = df.get('WF', pd.Series(index=df.index, dtype=str)).astype(str).str.strip().replace('', 'N/A')
            wf_part2 = df.get('WF.2', pd.Series(index=df.index, dtype=str)).astype(str).str.strip().replace('', 'N/A')
            status_components.append("wf-" + wf_part1 + "-" + wf_part2)

        df['status'] = status_components[0]
        for component in status_components[1:]:
            df['status'] = df['status'] + "_" + component

        # --- MODIFICATION START ---
        # Detect segment changes based on status AND large TTAG jumps.
        df['status_changed'] = df['status'].ne(df['status'].shift())
        
        # A TTAG jump is considered a change of segment.
        # A jump of 1,000,000 ticks corresponds to ~64 seconds. This indicates a likely data anomaly
        # or a significant time gap between recordings that should be treated as a new segment.
        TICK_JUMP_THRESHOLD = 1_000_000 
        ttag_diff = df['TTAG'].diff()
        df['ttag_jump'] = ttag_diff.abs() > TICK_JUMP_THRESHOLD
        
        # A new segment starts if the status changes OR if there's a TTAG jump.
        df['status_changed'] = df['status_changed'] | df['ttag_jump']
        # --- MODIFICATION END ---
        
        min_ttag = df['TTAG'].min()
        df['flight_time_s'] = (df['TTAG'] - min_ttag) * TICK_DURATION_S

        change_indices = df[df['status_changed']].index.tolist()
        if not change_indices or change_indices[0] != 0:
            change_indices.insert(0, 0)
        if df.index[-1] + 1 not in change_indices:
            change_indices.append(df.index[-1] + 1)
        
        summary_records = []
        for i in range(len(change_indices) - 1):
            start_loc, end_loc = change_indices[i], change_indices[i+1] - 1
            segment = df.loc[start_loc:end_loc]

            if segment.empty: continue
            
            start_time_s = segment['flight_time_s'].iloc[0]
            end_time_s = segment['flight_time_s'].iloc[-1]

            summary_records.append({
                'Segment (Mode | Scale | WF)': segment['status'].iloc[0],
                'Start Batch': segment['Batch'].iloc[0],
                'End Batch': segment['Batch'].iloc[-1],
                'Batch Count': segment['Batch'].iloc[-1] - segment['Batch'].iloc[0] + 1,
                'Duration (s)': end_time_s - start_time_s,
                'start_time_str': str(timedelta(seconds=int(start_time_s))),
                'end_time_str': str(timedelta(seconds=int(end_time_s))),
                'Start File': segment['file'].iloc[0],
                'End File': segment['file'].iloc[-1],
                '# Files': segment['file'].nunique()
            })
            
        summary_df = pd.DataFrame(summary_records)
        
        csv_path = output_dir / "flight_summary.csv"
        json_path = output_dir / "flight_summary.json"
        
        log.info(f"Saving aggregated summary to {csv_path}")
        summary_df.to_csv(csv_path, index=False)
        log.info(f"Saving aggregated summary to {json_path}")
        summary_df.to_json(json_path, orient="records", indent=4)
        
        return summary_df

    def _create_flight_report_txt(self, summary_df: "pd.DataFrame", output_dir: Path):
        report_path = output_dir / "flight_report.txt"
        log.info(f"Generating human-readable flight report to {report_path}")

        try:
            total_duration = summary_df['Duration (s)'].sum()
            total_batches = summary_df['Batch Count'].sum()
            num_segments = len(summary_df)

            with open(report_path, "w", encoding="utf-8") as f:
                f.write("=" * 80 + "\n")
                f.write(f" FLIGHT ANALYSIS REPORT - {self.current_flight_name} \n")
                f.write("=" * 80 + "\n\n")

                f.write("--- FLIGHT OVERVIEW ---\\n")
                f.write(f"Total Duration: {total_duration:.2f} seconds\\n")
                f.write(f"Total Batches:  {total_batches}\\n")
                f.write(f"Total Segments: {num_segments}\\n\n")

                f.write("--- SEGMENT SUMMARY ---\\n")
                
                report_df = summary_df.copy()
                report_df['Duration (s)'] = report_df['Duration (s)'].map('{:.2f}'.format)
                
                report_df = report_df[[
                    'Segment (Mode | Scale | WF)',
                    'Start Batch', 'End Batch', 'Batch Count',
                    'start_time_str', 'end_time_str', 'Duration (s)',
                    'Start File', 'End File', '# Files'
                ]]
                report_df.rename(columns={
                    'start_time_str': 'Start Time',
                    'end_time_str': 'End Time'
                }, inplace=True)

                df_string = report_df.to_string(index=True)

                f.write(df_string)
                f.write("\n\n" + "=" * 80 + "\n")
                f.write(" End of Report\n")
                
        except Exception as e:
            log.error(f"Failed to generate flight text report: {e}")