"""
Cross-Correlation Sum Analysis Module

This module provides functionality for summing and analyzing cross-correlation data
from multiple observations to determine planetary radial velocity parameters.
The analysis includes Kp (planetary radial velocity semi-amplitude) determination
and signal-to-noise ratio calculations.
"""

import os
import subprocess
import traceback
from pathlib import Path
from pickle import dump, load
from tkinter import messagebox

from pandas import read_csv

from GUIBRUSHR.GUI.LAYOUT.MyPanel import MyPanel
from GUIBRUSHR.GUI.WIDGET.MyButton import MyButton
from GUIBRUSHR.GUI.WIDGET.MyCheckBox import MyCheckBox
from GUIBRUSHR.GUI.WIDGET.MyEntry import MyEntry
from GUIBRUSHR.GUI.WIDGET.MyFigure import MyFigure
from GUIBRUSHR.GUI.WIDGET.MyDropDown import MyDropdown
from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
from GUIBRUSHR.General_Constants.FunctionsAndConstants.general_functions import (
    get_csv_value, create_path_night
)
from GUIBRUSHR.General_Constants.Classes.HelpButton import HelpButton


class FrameSumCC(MyPanel):
    """
    Cross-Correlation Sum Analysis Panel

    This class provides a GUI interface for performing cross-correlation analysis
    on multiple observation nights to determine planetary atmospheric parameters.
    It supports both retrieval mode (multi-night tar extraction) and manual mode
    (night-based processing).
    """

    def __init__(self, parent, color, row, column, path_default, frame_input,
                 window, width_GUI, height_GUI, filters_and_table, widget_nights, **kwargs):
        """
        Initialize the FrameSumCC panel.

        Args:
            parent: Parent widget
            color: Panel background color
            row: Grid row position
            column: Grid column position
            path_default: Default file path
            frame_input: Input frame widget
            window: Main window reference
            width_GUI: GUI width
            height_GUI: GUI height
            filters_and_table: Filter and table widget (for retrieval mode)
            widget_nights: Night selection widget (for manual mode)
            **kwargs: Additional keyword arguments
        """
        # Initialize instance variables
        self.path_folder_targets = frame_input.frame_path.path_folder_targets.get_text()
        self.filters_and_table = filters_and_table
        self.widget_nights = widget_nights
        self.global_exc = 0
        self.frame_input = frame_input
        self.path_default = path_default
        self.window = window
        self.color = color
        self.p = None

        # Determine target dropdown based on mode
        if filters_and_table is not None:
            self.target_dropdown = filters_and_table.frame_filter.dropdown_target
        else:
            self.target_dropdown = self.widget_nights.dropdown_target

        # Initialize parent panel
        super().__init__(parent, color, row, column, **kwargs)

        # Create layout panels
        self.row1 = MyPanel(self, color, 0, 1)

        # Create control widgets
        self._create_control_widgets()

        # Set up exchange directory for background process communication
        self._setup_exchange_directory()

        # Mark initialization as complete
        self.global_exc = 1

    def _create_control_widgets(self):
        """Create the control widgets for the panel."""
        # Help text dictionary
        help_text = {
            "M-filter": "When enabled, applies a median filter to the cross-correlation profiles to remove low-frequency variations and enhance signal detection. The filter subtracts a uniform-filtered version (size=40) from each profile, emphasizing high-frequency features like narrow absorption/emission lines.",

            "Range inf. kp": "Lower bound of the Kp search range in km/s. Kp is the semi-amplitude of the planetary radial velocity orbit. This value defines the minimum Kp to test during the analysis. Recommended starting value: 0 km/s.",

            "Range sup. kp": "Upper bound of the Kp search range in km/s. This value defines the maximum Kp to test during the analysis. Set this based on expected planetary mass and orbital parameters. For hot Jupiters: typically 100-300 km/s. Recommended: 300 km/s.",

            "Step kp": "Step size in km/s for scanning through the Kp range. Smaller steps provide finer resolution but increase computation time. Recommended: 1-3 km/s for initial searches, 0.1-0.5 km/s for refinement.",

            "Ecc. Orbit": "Enable this for eccentric orbits to use the full radvel Keplerian velocity calculation instead of the circular orbit approximation (Kp * sin(2pi * phase)). When enabled, the analysis uses the target's eccentricity and argument of periastron parameters to compute accurate radial velocities.",

            "Min RV std estimation": "Minimum radial velocity offset (in km/s) from the best V_rest for noise standard deviation estimation. Defines the inner boundary of the out-of-trail regions used to calculate noise. Recommended: 25 km/s.",

            "Max RV std estimation": "Maximum radial velocity offset (in km/s) from the best V_rest for noise standard deviation estimation. Defines the outer boundary of the out-of-trail regions used to calculate noise. Recommended: 75 km/s."
        }

        # Create help button
        self.help_button = HelpButton(
            self.row1, 0, 0,
            "Cross-Correlation Sum Parameters Help",
            help_text,
            columnspan=1
        )

        # M-filter checkbox for optional filtering
        self.checkbox_Mfilter = MyCheckBox(self.row1, 0, 1, "M-filter")

        # Kp range and step controls
        self.min_kp = MyEntry(self.row1, 0, 2, "0",
                             label_text="Range inf. kp:", columnspan=2)
        self.max_kp = MyEntry(self.row1, 0, 4, "300",
                             label_text="Range sup. kp:", columnspan=2)
        self.step_kp = MyEntry(self.row1, 0, 6, "3",
                              label_text="Step kp:", columnspan=2)

        # Eccentricity checkbox for eccentric orbits
        self.checkbox_eccentricity = MyCheckBox(self.row1, 0, 8, "Ecc. Orbit")

        # Colormap selection dropdown
        cmap_names = [h[1] for h in ConstantVariables.HUES]
        self.dropdown_cmap = MyDropdown(
            self.row1, 1, 1, cmap_names, initial_value=22,
            label_text="CMAP:", columnspan=2
        )

        self.entry_rv_analysis_limit = MyEntry(self.row1, 1, 3, "100", label_text="RV analysis limit (km/s):")

        # Noise estimation RV range parameters
        self.entry_min_rv_std_estimation = MyEntry(self.row1, 2, 1, "25", label_text="Min RV std estimation (km/s):", columnspan=2)
        self.entry_max_rv_std_estimation = MyEntry(self.row1, 2, 3, "75", label_text="Max RV std estimation (km/s):", columnspan=2)

        # Action buttons
        self.button_sum_cc = MyButton(
            self.row1, 2, 5, 'RUN', '#f5674a',
            self.sum_cc, color_panel=self.color
        )
        self.button_show_previous_cc = MyButton(
            self.row1, 2, 7, 'SHOW LAST PLOT', '#33c4ff',
            self.show_sum_cc, color_panel=self.color
        )

    def sum_cc(self):
        """
        Main entry point for cross-correlation sum analysis.

        Determines whether the run is a retrieval (multi-night tar extraction)
        or a manual/night-based process, gathers the necessary file paths and metadata,
        saves an exchange pkl, and launches sum_cc_background.py as a subprocess.
        """
        if self.widget_nights is not None:
            selected_nights = self.widget_nights.textfield_selection.get_text()
            if selected_nights == "":
                messagebox.showerror("No nights selected", "Please select nights.")
                return
        try:
            # Get target name from dropdown
            target_name = self.target_dropdown.get_value()
            modelel_name = None  # For manual mode, this is set later from the widget

            # Determine processing mode and gather parameters
            if self.widget_nights is None:
                # Retrieval case: multi-night tar extraction
                (processing_mode, result_path, selected_order, target_name,
                 radiative_mode, instrument_paths, observation_dates,
                 instruments, telluric_method) = self._setup_retrieval_mode(target_name)
            else:
                # Night/Manual case
                (processing_mode, result_path, selected_order, radiative_mode,
                 telluric_method, modelel_name, instrument_paths,
                 observation_dates, instruments, error, message) = self._setup_manual_mode(target_name)

                # Check if there were validation errors
                if error:
                    messagebox.showerror("Order Selection Path Error", message)
                    return

            # Get the filtering flag from the checkbox
            use_Mfilter = self.checkbox_Mfilter.get_value()

            # Save exchange pkl and launch background subprocess
            self._save_exchange_pkl(
                result_path, instrument_paths, selected_order, observation_dates,
                instruments, target_name, use_Mfilter, telluric_method,
                radiative_mode, modelel_name, processing_mode
            )
            self.button_sum_cc.button.configure(state="disabled")
            self.p = subprocess.Popen(
                ["python3", str(Path(self.path_default, "GUIBRUSHR", "GUI",
                 "Input_Output_Panels", "Input_Panels", "Frame_Cross_Correlation",
                 "sum_cc_background.py")),
                 str(self.path_exchange), self.path_default],
                stderr=subprocess.STDOUT, universal_newlines=True,
            )
            self.window.after(500, self.check_proc)

        except Exception as e:
            error_msg = str(e) + "\n" + traceback.format_exc()
            MyFigure(title="Error in sum cc", message=error_msg)
            print(error_msg)

    def _setup_retrieval_mode(self, target_name):
        """
        Setup parameters for retrieval mode (multi-night tar extraction).

        Args:
            target_name: Name of the target

        Returns:
            Tuple containing all necessary parameters for retrieval mode
        """
        processing_mode = 0  # 0 means retrieval mode
        table_values = self.filters_and_table.frame_table.table.get_arr_values()

        # Construct the result folder path
        result_path = str(Path(
            self.path_folder_targets, target_name, ConstantVariables.RESULTS_FOLDER,
            table_values[ConstantVariables.DICT_FILTER_TABLE["ID"]["index"]]
        ))

        # Read general information about the run from CSV file
        df_general_info = read_csv(str(Path(result_path, "df_general_info.csv")))
        selected_order = get_csv_value(df_general_info, "order_sel")
        target_name = get_csv_value(df_general_info, "target_name")
        radiative_mode = get_csv_value(df_general_info, "rad_mode")
        telluric_method = get_csv_value(df_general_info, "tell_rm_method")

        # Parse instrument information
        instrument_hr_str = get_csv_value(df_general_info, "instruments_hr_list")
        instrument_list = instrument_hr_str.split("|")

        # Initialize lists for instrument data
        instrument_paths = []
        observation_dates = []
        instruments = []

        # Get username and build temporary tar extraction folder
        username = os.environ.get("USER")
        tar_folder = str(Path(
            self.path_default, "GUIBRUSHR", "Files", "Temp_Files_GUI",
            username, "tar_unpack"
        ))

        # Process each instrument in the list
        for curr_instrument in instrument_list:
            curr_instrument = curr_instrument.split("&")
            # curr_instrument[5] holds the dates separated by underscores
            temp_dates = curr_instrument[5].split("_")

            for tmp in temp_dates:
                observation_dates.append(tmp)
                instruments.append(curr_instrument[0])

                # Build instrument path based on temporary folder and instrument identifier
                instrument_paths.append(str(Path(tar_folder, curr_instrument[0])))
                path_tar_tmp = str(Path(tar_folder, curr_instrument[0], tmp))

                # Remove any previous extracted directory and create a new one
                os.system(f"rm -rf {path_tar_tmp}")
                os.system(f"mkdir -p {path_tar_tmp}")

                # Extract the tar.gz file to the temporary folder
                tar_file = str(Path(result_path, f"{tmp}_{curr_instrument[0]}.tar.gz"))
                os.system(f"tar -xzf {tar_file} --transform='s/.*\\///' -C {path_tar_tmp}")

        return (processing_mode, result_path, selected_order, target_name,
                radiative_mode, instrument_paths, observation_dates,
                instruments, telluric_method)

    def _setup_manual_mode(self, target_name):
        """
        Setup parameters for manual/night mode.

        Args:
            target_name: Name of the target

        Returns:
            Tuple containing all necessary parameters for manual mode (including error and message)
        """
        processing_mode = 1  # manual/night mode
        selected_order = self.widget_nights.textfield_order_selection.get_text()
        selected_nights = self.widget_nights.textfield_selection.get_text()
        radiative_mode = self.widget_nights.dropdown_rad_mode.get_value()
        telluric_method = self.widget_nights.dropdown_tell_rm_method.get_value()
        modelel_name = self.widget_nights.dropdown_cross_corr.get_value()

        # Initialize lists for observation data
        observation_dates = []
        instruments = []
        instrument_paths = []

        # Initialize error tracking
        error = False
        message = "Order selection path validation errors:\n"

        # Process each instrument-night combination
        for instrument_nights in selected_nights.split(";"):
            instrument_nights = instrument_nights.split(":")
            result_path = create_path_night(
                self.path_folder_targets, target_name,
                radiative_mode, instrument_nights[0]
            )
            new_dates = instrument_nights[1].split(",")

            # Validate order selection path exists for each night
            for night in new_dates:
                if not Path(result_path, night, "orders_selection", selected_order).exists():
                    error = True
                    message += f" - Order selection {selected_order} does not exist for path \n   {result_path}/{night}\n"

            for _ in new_dates:
                instrument_paths.append(result_path)
                instruments.append(instrument_nights[0])

            observation_dates = observation_dates + new_dates

        result_path = None  # Not used in manual mode

        return (processing_mode, result_path, selected_order, radiative_mode,
                telluric_method, modelel_name, instrument_paths,
                observation_dates, instruments, error, message)

    def _setup_exchange_directory(self):
        """Create the temporary directory used for pkl-based IPC with the background process."""
        username = os.environ.get("USER")
        self.path_exchange = str(
            Path(self.path_default, "GUIBRUSHR", "Files", "Temp_Files_GUI",
                 username, "Temp_folder")
        )
        Path(self.path_exchange).mkdir(parents=True, exist_ok=True)

    def _save_exchange_pkl(self, path_result, path_instrument, order_sel, dates,
                           instruments, target, mfilter, tell_rm_method, rad_mode,
                           model_name, mode):
        """
        Remove stale result/error pkl files and write the exchange pkl for the
        background subprocess.
        """
        for stale in ["result_sum_cc.pkl", "error_sum_cc.pkl"]:
            p = Path(self.path_exchange, stale)
            if p.exists():
                p.unlink()

        params = {
            "path_result": path_result,
            "path_instrument": path_instrument,
            "order_sel": order_sel,
            "dates": dates,
            "instruments": instruments,
            "target": target,
            "mfilter": mfilter,
            "tell_rm_method": tell_rm_method,
            "rad_mode": rad_mode,
            "model_name": model_name,
            "mode": mode,
            "min_kp": int(self.min_kp.get_value()),
            "max_kp": int(self.max_kp.get_value()),
            "step_kp": int(self.step_kp.get_value()),
            "eccentricity": self.checkbox_eccentricity.get_value(),
            "min_rv_std": float(self.entry_min_rv_std_estimation.get_value()),
            "max_rv_std": float(self.entry_max_rv_std_estimation.get_value()),
            "rv_analysis_limit": int(self.entry_rv_analysis_limit.get_value()),
            "selected_cmap_name": self.dropdown_cmap.get_value(),
            "path_folder_targets": self.path_folder_targets,
            "path_default": self.path_default,
        }
        with open(str(Path(self.path_exchange, "exchange_sum_cc.pkl")), "wb") as f:
            dump(params, f)

    def check_proc(self):
        """
        Poll the background subprocess and update the GUI when it finishes.

        Called repeatedly via window.after(500, ...) until the process exits.
        On success, reads result_sum_cc.pkl and updates the plot widgets.
        On error, reads error_sum_cc.pkl and shows an error dialog.
        """
        if self.p.poll() is not None:
            self.button_sum_cc.button.configure(state="normal")
            error_path = Path(self.path_exchange, "error_sum_cc.pkl")
            result_path = Path(self.path_exchange, "result_sum_cc.pkl")
            if error_path.exists():
                with open(str(error_path), "rb") as fo:
                    message = load(fo)
                MyFigure(title="Error in sum cc", message=message)
            elif result_path.exists():
                with open(str(result_path), "rb") as fo:
                    result = load(fo)
                MyFigure(title="Histogram", type_data="Image", image_path=result["png_hist"])
                MyFigure(title="CC Norm", type_data="Image", image_path=result["png_cc"])
        else:
            self.window.after(500, self.check_proc)

    def show_sum_cc(self):
        """
        Display previously generated cross-correlation sum plots.

        This method loads and displays the most recent cross-correlation
        analysis results without recomputing them.
        """
        target = self.target_dropdown.get_value()

        if self.widget_nights is None:
            # Retrieval mode: get paths from filters and table
            arr_values = self.filters_and_table.frame_table.table.get_arr_values()
            path_result = str(Path(
                self.path_folder_targets, target, ConstantVariables.RESULTS_FOLDER,
                arr_values[ConstantVariables.DICT_FILTER_TABLE["ID"]["index"]]
            ))
            prefix = ""
            path_cc = Path(path_result, "cross_correlation")
        else:
            # Manual mode: construct paths from widget selections
            selected_order = self.widget_nights.textfield_order_selection.get_text()
            selected_nights = self.widget_nights.textfield_selection.get_text()
            if selected_nights == "":
                messagebox.showerror("No nights selected", "Please select nights.")
                return
            telluric_method = self.widget_nights.dropdown_tell_rm_method.get_value()
            modelel_name = self.widget_nights.dropdown_cross_corr.get_value()

            # Parse observation information
            observation_dates = []
            instruments = []
            instrument_paths = []

            for instrument_nights in selected_nights.split(";"):
                instrument_nights = instrument_nights.split(":")
                result_path = create_path_night(
                    self.path_folder_targets, target,
                    self.widget_nights.dropdown_rad_mode.get_value(),
                    instrument_nights[0]
                )
                new_dates = instrument_nights[1].split(",")
                for _ in new_dates:
                    instrument_paths.append(result_path)
                    instruments.append(instrument_nights[0])
                observation_dates = observation_dates + new_dates

            # Determine file prefix
            if len(observation_dates) > 1:
                str_nights = "_".join(observation_dates)
                prefix = "multi_" + str_nights
            else:
                prefix = ""

            path_cc = Path(instrument_paths[0], observation_dates[0],
                          "cross_correlation", telluric_method,
                          selected_order, modelel_name)

        # Construct file paths and display plots
        filepng1 = str(Path(path_cc, prefix, "hist.png"))
        filepng2 = str(Path(path_cc, prefix, "cc_norm.png"))

        MyFigure(title="Histogram", type_data="Image", image_path=filepng1)
        MyFigure(title="CC Norm", type_data="Image", image_path=filepng2)
