"""
Background cross-correlation computation module.

This script is launched as a subprocess by Frame_run_CC.py to perform
the cross-correlation analysis without blocking the GUI. It reads parameters
from exchange_cc.pkl, runs the computation, and writes results to
result_cc.pkl or errors to error_cc.pkl.

Usage:
    python3 run_cc_background.py <exchange_path> <path_default>
"""

import sys
import os
import traceback
import pickle
from pathlib import Path
from pickle import load, dump

import matplotlib
matplotlib.rcParams['agg.path.chunksize'] = 10000
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.colors import Normalize
import numpy as np
from scipy.interpolate import splrep, splev, UnivariateSpline, interp1d
from scipy import optimize


def gaussian(x, cont, p, c, sg):
    """Gaussian absorption profile: continuum minus Gaussian peak."""
    return cont - p * np.exp(-((x - c) / sg) ** 2)


def gaussian_fixed(x, p, c, sg):
    """Gaussian emission profile without continuum offset."""
    return p * np.exp(-((x - c) / sg) ** 2)


def bigaussian(x, cont, p1, c1, sg1, p2, c2, sg2):
    """Double Gaussian profile on a continuum, used for Doppler shadow fitting."""
    return cont + p1 * np.exp(-((x - c1) / sg1) ** 2) + p2 * np.exp(-((x - c2) / sg2) ** 2)


def shiftRV(pfn_data, rv_array, rv_shifted, rv_bin=1):
    """
    Shift cross-correlation profiles to a common RV frame using spline interpolation.

    Args:
        pfn_data: 2D array of cross-correlation profiles (n_observations x n_rv)
        rv_array: Original radial velocity array
        rv_shifted: RV shifts to apply per observation
        rv_bin: RV bin size for the output grid

    Returns:
        tuple: (pfn_shifted, rv_shift) - shifted profiles and new RV grid
    """
    pfn_rvs = []
    for n, pfn in enumerate(pfn_data):
        pfn_rvs.append(rv_array - rv_shifted[n])
    pfn_rvs = np.asarray(pfn_rvs)

    rvmin = np.amin(pfn_rvs[:, 0])
    rvmax = np.amax(pfn_rvs[:, -1])
    rv_shift = np.arange(int(rvmin), int(rvmax) + rv_bin, rv_bin)

    pfn_shifted = []
    for n, pfn in enumerate(pfn_data):
        spl = UnivariateSpline(pfn_rvs[n], pfn, k=3, s=0, ext="zeros")
        pfn_new = spl(rv_shift)
        pfn_shifted.append(np.asarray(pfn_new))

    pfn_shifted = np.asarray(pfn_shifted)
    return pfn_shifted, rv_shift


def bin_averaging(data, phases, kp_phases, kp_extra_phases, bin_size):
    """
    Bin cross-correlation profiles by orbital phase.

    Args:
        data: 2D array of profiles (n_observations x n_rv)
        phases: Orbital phase for each observation
        kp_phases: Kp velocity component for each observation
        kp_extra_phases: Extra Kp velocity component for each observation
        bin_size: Phase bin width

    Returns:
        tuple: (binned_profiles, binned_phases, binned_kp, binned_kp_extra)
    """
    binned_profiles = []
    binned_phases = []
    binned_kp = []
    binned_kp_extra = []

    phase_bin = np.min(phases)
    while phase_bin <= np.max(phases):
        phase_idx = np.where((phases >= phase_bin) & (phases < phase_bin + bin_size))[0]
        if len(phase_idx) >= 1:
            binned_profiles.append(np.mean(data[phase_idx, :], axis=0))
            binned_phases.append(phase_bin)
            binned_kp.append(np.mean(kp_phases[phase_idx]))
            if len(kp_extra_phases) > 0:
                binned_kp_extra.append(np.mean(kp_extra_phases[phase_idx]))
        phase_bin += (bin_size / 2)

    return (
        np.array(binned_profiles),
        np.array(binned_phases),
        np.array(binned_kp),
        np.array(binned_kp_extra)
    )


def analyze_phase_coverage(night_phase_ranges, bin_size):
    """
    Analyze phase coverage across multiple nights to detect overlaps and gaps.

    Args:
        night_phase_ranges: List of (date, instrument, phase_min, phase_max) tuples
        bin_size: Phase bin width used for gap threshold

    Returns:
        list: List of (gap_start, gap_end) tuples for detected phase gaps
    """
    n = len(night_phase_ranges)

    print("\n--- Phase coverage analysis ---")
    for date, instr, pmin, pmax in night_phase_ranges:
        print(f"  {instr}:{date}  phases [{pmin:.5f}, {pmax:.5f}]")

    for i in range(n):
        for j in range(i + 1, n):
            d_i, inst_i, min_i, max_i = night_phase_ranges[i]
            d_j, inst_j, min_j, max_j = night_phase_ranges[j]
            overlap = min(max_i, max_j) - max(min_i, min_j)
            if overlap > 0:
                print(f"  Overlap: {inst_i}:{d_i} and {inst_j}:{d_j} "
                      f"(overlap = {overlap:.5f} in phase)")
            else:
                gap = max(min_i, min_j) - min(max_i, max_j)
                print(f"  Gap: {inst_i}:{d_i} and {inst_j}:{d_j} "
                      f"(gap = {gap:.5f} in phase)")

    sorted_ranges = sorted(night_phase_ranges, key=lambda x: x[2])

    gaps = []
    current_end = sorted_ranges[0][3]
    for _, _, pmin, pmax in sorted_ranges[1:]:
        if pmin > current_end + bin_size:
            gaps.append((current_end, pmin))
        current_end = max(current_end, pmax)

    if gaps:
        print(f"  Detected {len(gaps)} phase gap(s):")
        for gs, ge in gaps:
            print(f"    Gap from phase {gs:.5f} to {ge:.5f}")
    else:
        print("  No phase gaps detected (continuous coverage)")
    print("-------------------------------\n")

    return gaps


def fill_phase_gaps(binned_profile, binned_phases, binned_kp, binned_kp_extra, bin_size, gaps):
    """
    Fill phase gaps with zero-signal rows and trim interior gap rows for display.

    For each gap, inserts evenly spaced zero rows from gap_start to gap_end
    so that the binned phase array is continuous. Then, to avoid the gap
    dominating the plot, removes interior gap rows keeping only a few
    (``3 * bin_size`` in phase) on each edge of the gap.

    Args:
        binned_profile: 2D array of binned profiles (n_bins x n_rv)
        binned_phases: 1D array of bin phases
        binned_kp: 1D array of binned Kp values
        binned_kp_extra: 1D array of binned extra Kp values
        bin_size: Phase bin width used for stepping and for determining
            how many zero rows to keep at each gap edge
        gaps: List of (gap_start, gap_end) tuples from analyze_phase_coverage

    Returns:
        tuple: (filled_profiles, filled_phases, filled_kp, filled_kp_extra, n_gap_rows)
    """
    if not gaps:
        return binned_profile, binned_phases, binned_kp, binned_kp_extra, 0

    step = bin_size / 2
    n_rv = binned_profile.shape[1]
    has_extra = len(binned_kp_extra) > 0

    filled_profiles = list(binned_profile)
    filled_phases = list(binned_phases)
    filled_kp = list(binned_kp)
    filled_kp_extra = list(binned_kp_extra) if has_extra else []

    n_gap_rows = 0
    for gap_start, gap_end in gaps:
        fill_phases = np.arange(gap_start + step, gap_end, step)
        for fp in fill_phases:
            filled_profiles.append(np.zeros(n_rv))
            filled_phases.append(fp)
            filled_kp.append(0.0)
            if has_extra:
                filled_kp_extra.append(0.0)
            n_gap_rows += 1

    sort_idx = np.argsort(filled_phases)
    filled_phases = np.array(filled_phases)[sort_idx]
    filled_profiles = np.array(filled_profiles)[sort_idx]
    filled_kp = np.array(filled_kp)[sort_idx]
    filled_kp_extra = np.array(filled_kp_extra)[sort_idx] if has_extra else np.array([])
    index_keep_phases = np.ones_like(filled_phases)
    keep_step = 3 * bin_size
    for gap_start, gap_end in gaps:
        index_keep_phases = np.logical_and(
            index_keep_phases, np.logical_or(filled_phases < gap_start + keep_step, filled_phases > gap_end + keep_step)
        )
    filled_phases = filled_phases[index_keep_phases]
    filled_profiles = filled_profiles[index_keep_phases]
    filled_kp = filled_kp[index_keep_phases]
    filled_kp_extra = filled_kp_extra[index_keep_phases] if has_extra else np.array([])

    print(f"  Filled {n_gap_rows} zero-signal rows across {len(gaps)} gap(s)")

    return filled_profiles, filled_phases, filled_kp, filled_kp_extra, n_gap_rows


def plot_and_save_cor(ccor_new, rv_array, transit_phase, night_obj, base_dir):
    """
    Generate a PDF with cross-correlation plots for each spectral order.

    Args:
        ccor_new: 3D array of cross-correlation data (rv x phases x orders)
        rv_array: Radial velocity array
        transit_phase: Orbital phase array
        night_obj: Night object with order information
        base_dir: Directory to save the output PDF
    """
    pdf_path = str(Path(base_dir) / "corrected_orders.pdf")
    with PdfPages(pdf_path) as pdf:
        for k in range(night_obj.n_good_orders):
            fig, ax = plt.subplots(1, 1, figsize=(10, 8), dpi=300)
            order_data = ccor_new[:, :, k]
            im = ax.imshow(
                order_data.T,
                origin="lower",
                aspect="auto",
                extent=(rv_array[0], rv_array[-1], transit_phase[0], transit_phase[-1]),
                cmap='jet',
                zorder=1
            )
            ax.set_title(f"Order {night_obj.good_order[k]} (index {k})")
            ax.set_xlabel(r"$V_{rad}$ [km/s]")
            ax.set_ylabel("Phase")
            cbar = plt.colorbar(im, ax=ax)
            cbar.set_label('Cross-correlation')
            pdf.savefig(fig, bbox_inches='tight')
            plt.close(fig)
    print(f"Saved corrected orders plot to: {pdf_path}")


def mask_gaps_for_traces(x_values, y_indices, phases, gaps):
    """
    Replace trace coordinates falling inside phase gaps with NaN.

    This forces Matplotlib to break the line at gap boundaries instead
    of drawing a straight segment across the gap.

    Args:
        x_values: 1D array of x-coordinates (e.g. RV values)
        y_indices: 1D array of y-coordinates (row indices)
        phases: 1D array of phase values corresponding to each point
        gaps: Array of (gap_start, gap_end) pairs

    Returns:
        tuple: (x_masked, y_masked) with NaN at gap positions
    """
    x_masked = np.array(x_values, dtype=float)
    y_masked = np.array(y_indices, dtype=float)

    for gap_start, gap_end in gaps:
        in_gap = (phases >= gap_start) & (phases <= gap_end)
        x_masked[in_gap] = np.nan
        y_masked[in_gap] = np.nan

    return x_masked, y_masked


def plot_ccf_grid(sss_new, rv_array, filepdf):
    """
    Generate a text-based CCF values grid plot saved as PDF.

    Each cell displays the numerical CCF value colored by magnitude.

    Args:
        sss_new: 2D cross-correlation map (phases x rv)
        rv_array: Radial velocity array
        filepdf: Base PDF path; grid is saved as ``*_ccf_grid.pdf``
    """
    row_idx, col_idx = np.unravel_index(sss_new.argmax(), sss_new.shape)
    print(f"Max value of cc: {np.max(sss_new)} at spectrum phase = {row_idx} and rv = {col_idx}")

    ccf_grid_pdf_path = filepdf.replace(".pdf", "_ccf_grid.pdf") if filepdf else "ccf_grid.pdf"
    n_rows, n_cols = sss_new.shape
    cmap_grid = plt.get_cmap("gnuplot2")
    norm = Normalize(vmin=sss_new.min(), vmax=sss_new.max())
    cell_width = 0.8
    cell_height = 0.4
    fig_width = max(12, n_cols * cell_width + 2)
    fig_height = max(8, n_rows * cell_height + 2)

    with PdfPages(ccf_grid_pdf_path) as pdf_grid:
        fig_grid, ax_grid = plt.subplots(figsize=(fig_width, fig_height))
        ax_grid.set_xlim(-0.5, n_cols - 0.5)
        ax_grid.set_ylim(-0.5, n_rows - 0.5)
        for i in range(n_rows):
            for j in range(n_cols):
                value = sss_new[i, j]
                color = cmap_grid(norm(value))
                ax_grid.text(j, i, f"{value:.3f}", ha='center', va='center',
                             fontsize=6, color=color, fontweight='bold')
        ax_grid.set_yticks(range(n_rows))
        ax_grid.set_yticklabels(range(n_rows))
        ax_grid.set_xticks(range(n_cols))
        ax_grid.set_xticklabels([f"{rv:.1f}" for rv in rv_array], rotation=90, fontsize=6)
        ax_grid.set_xlabel("RV [km/s]")
        ax_grid.set_ylabel("Phase Index")
        ax_grid.set_title("CCF Values Grid")
        ax_grid.set_xticks(np.arange(-0.5, n_cols, 1), minor=True)
        ax_grid.set_yticks(np.arange(-0.5, n_rows, 1), minor=True)
        ax_grid.grid(which='minor', color='gray', linestyle='-', linewidth=0.5, alpha=0.3)
        ax_grid.tick_params(which='minor', size=0)
        sm = plt.cm.ScalarMappable(cmap=cmap_grid, norm=norm)
        sm.set_array([])
        cbar = plt.colorbar(sm, ax=ax_grid, orientation='vertical', fraction=0.02, pad=0.02)
        cbar.set_label('CCF Value')
        plt.tight_layout()
        pdf_grid.savefig(fig_grid)
        plt.close(fig_grid)
    print(f"CCF grid PDF saved to: {ccf_grid_pdf_path}")


def plot_run_cc_bg(
    rv_array, transit_phase, title, rad_mode, mode, sss_new, rv_planet,
    target_information, selected_cmap_name,
    extra_rv_trace=None, doppler_shadow_step=False, rv_DS=None,
    pdf_total=None, filepdf=None, filepng=None, plot_cc_grid=False,
    phase_gaps=np.array([])
):
    """
    Create cross-correlation phase-velocity plots with optional Doppler shadow traces.

    The y-axis uses a row-index coordinate system (0 to N) so that the image
    has uniform pixel spacing even when phases are non-uniform (e.g. after
    gap trimming). Physical phase labels are mapped onto the index ticks.
    RV traces are plotted in the same index space, with NaN masking inside
    phase gaps to break the lines.

    Args:
        rv_array: Radial velocity array
        transit_phase: 1D array of phase values (one per image row)
        title: Plot title
        rad_mode: Radiation mode ('Transmission' or 'Emission')
        mode: Processing mode (0=retrieval, 1=manual)
        sss_new: 2D cross-correlation map (n_phases x n_rv)
        rv_planet: Planetary RV trace (one value per phase)
        target_information: Target information object
        selected_cmap_name: Name of the colormap to use
        extra_rv_trace: Optional additional RV trace to plot
        doppler_shadow_step: Whether to plot Doppler shadow trace
        rv_DS: Doppler shadow RV values
        pdf_total: PdfPages object for multi-page PDF output
        filepdf: Path for saving individual PDF file
        filepng: Path for saving individual PNG file
        plot_cc_grid: Whether to generate a CCF values grid plot
        phase_gaps: Array of (gap_start, gap_end) pairs for phase gaps.
            Used to break RV trace lines and mask transit-limit
            interpolation inside gaps.
    """
    from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
    selected_cmap = ConstantVariables.CMAPS.get(selected_cmap_name, "inferno")

    if plot_cc_grid:
        plot_ccf_grid(sss_new, rv_array, filepdf)

    fig, ax = plt.subplots(1, 1, figsize=(10, 8))
    im_transit = ax.imshow(
        sss_new,
        origin="lower",
        aspect="auto",
        extent=(rv_array[0], rv_array[-1], 0, sss_new.shape[0]),
        cmap=selected_cmap,
        zorder=1
    )

    # Map physical phase labels onto the row-index y-axis
    n_rows = sss_new.shape[0]
    n_ticks = min(12, n_rows)
    tick_step = max(1, int(n_rows / n_ticks))
    indices_to_show = np.arange(0, n_rows, tick_step)
    labels_to_show = [f"{transit_phase[i]:.2f}" for i in indices_to_show]
    ax.set_yticks(indices_to_show)
    ax.set_yticklabels(labels_to_show)
    ax.set_title(title)
    ax.set_xlabel(r"$V_{rad}$ [km/s]")
    ax.set_ylabel("Phase")

    # Continuous index array for trace plotting
    y_idx = np.arange(len(transit_phase))

    # Interpolate physical transit limits (T14, T23) to index positions
    idx_t14_pos = np.interp(target_information.limit_phase_t14, transit_phase, y_idx)
    idx_t14_neg = np.interp(-target_information.limit_phase_t14, transit_phase, y_idx)
    idx_t23_pos = np.interp(target_information.limit_phase_t23, transit_phase, y_idx)
    idx_t23_neg = np.interp(-target_information.limit_phase_t23, transit_phase, y_idx)

    pre_m = None
    post_m = None

    index_gap = 0
    for g_start, g_end in phase_gaps:
        index_gap += 1

        # Convert phase values to index coordinates
        idx_start = np.interp(g_start, transit_phase, y_idx)
        idx_end = np.interp(g_end, transit_phase, y_idx)

        # Plot horizontal lines using the calculated indices
        ax.axhline(idx_start, linestyle="--", color="gray", alpha=0.7,
                   label=f"Start GAP{index_gap} = {g_start:.2f}", zorder=10)
        ax.axhline(idx_end, linestyle="--", color="gray", alpha=0.7,
                   label=f"End GAP{index_gap} = {g_end:.2f}", zorder=10)

    if rad_mode == "Transmission":
        rv_full = [rv_array[0], rv_array[-1]]

        ax.fill_between(rv_full, idx_t14_pos, y_idx[-1], color="black", alpha=0.4, zorder=2)
        ax.fill_between(rv_full, y_idx[0], idx_t14_neg, color="black", alpha=0.4, zorder=2)

        ax.axhline(idx_t14_pos, linestyle="-", color="black", label="T14", linewidth=2.0, zorder=3)
        ax.axhline(idx_t14_neg, linestyle="-", color="black", linewidth=2.0, zorder=3)
        ax.axhline(idx_t23_pos, linestyle="--", color="black", label="T23", linewidth=2.0, zorder=10)
        ax.axhline(idx_t23_neg, linestyle="--", color="black", linewidth=2.0, zorder=10)

        transit_mask = (transit_phase >= -target_information.limit_phase_t14) & \
                       (transit_phase <= target_information.limit_phase_t14)
        transit_y_idx = y_idx[transit_mask]

        ax.imshow(sss_new[transit_mask, :], origin="lower", aspect="auto",
                  extent=(rv_array[0], rv_array[-1], transit_y_idx[0], transit_y_idx[-1]),
                  cmap=selected_cmap, zorder=5)

        pre_m = transit_phase < -target_information.limit_phase_t14
        post_m = transit_phase > target_information.limit_phase_t14

        rv_p_x, rv_p_y = mask_gaps_for_traces(rv_planet, y_idx, transit_phase, phase_gaps)
        ax.plot(rv_p_x[pre_m], rv_p_y[pre_m], label='RV nominal', linestyle='--',
                color='lightgreen', linewidth=2.0, zorder=10)
        ax.plot(rv_p_x[post_m], rv_p_y[post_m], linestyle='--',
                color='lightgreen', linewidth=2.0, zorder=10)

        if doppler_shadow_step:
            ds_x, ds_y = mask_gaps_for_traces(rv_DS, y_idx, transit_phase, phase_gaps)
            ax.plot(ds_x[pre_m], ds_y[pre_m], label='RV Doppler Shadow', linestyle='--',
                    color='black', linewidth='2', zorder=10)
            ax.plot(ds_x[post_m], ds_y[post_m], linestyle='--',
                    color='black', linewidth='2', zorder=10)
    else:
        rv_p_x, rv_p_y = mask_gaps_for_traces(rv_planet, y_idx, transit_phase, phase_gaps)
        ax.plot(rv_p_x, rv_p_y, linestyle="--", color="lightgreen",
                label='RV nominal', linewidth=2.0, zorder=10)

        if doppler_shadow_step:
            ds_x, ds_y = mask_gaps_for_traces(rv_DS, y_idx, transit_phase, phase_gaps)
            ax.plot(ds_x, ds_y, linestyle='--', color='black', linewidth='2', zorder=10)

    if extra_rv_trace is not None and len(extra_rv_trace) > 0:
        ex_x, ex_y = mask_gaps_for_traces(extra_rv_trace, y_idx, transit_phase, phase_gaps)
        extra_rv_label = "RV retrieval" if mode == 0 else "RV at Kp peak"

        if rad_mode == "Transmission" and pre_m is not None:
            ax.plot(ex_x[pre_m], ex_y[pre_m], label=extra_rv_label, linestyle=(0, (8, 8)),
                    color='cyan', linewidth=2.0, zorder=10)
            ax.plot(ex_x[post_m], ex_y[post_m], linestyle=(0, (8, 8)),
                    color='cyan', linewidth=2.0, zorder=10)
        else:
            ax.plot(ex_x, ex_y, linestyle=(0, (8, 8)), color="cyan",
                    label=extra_rv_label, linewidth=3.0, zorder=11)

    cbar = plt.colorbar(im_transit)
    cbar.set_label('Cross-correlation')
    ax.legend()

    if pdf_total is not None:
        pdf_total.savefig(fig)
    if filepdf is not None:
        fig.savefig(filepdf, bbox_inches="tight", pad_inches=0.1)
        filepkl = filepdf.replace('.pdf', '_data.pkl')
        plot_data = {
            'rv_array': rv_array,
            'transit_phase': transit_phase,
            'title': title,
            'rad_mode': rad_mode,
            'mode': mode,
            'sss_new': sss_new,
            'rv_planet': rv_planet,
            'rv_planet_retrieval': extra_rv_trace,
            'extra_rv_trace': extra_rv_trace,
            'doppler_shadow_step': doppler_shadow_step,
            'rv_DS': rv_DS,
            'selected_cmap_name': selected_cmap_name,
            'phase_gaps': phase_gaps,
            'limit_phase_t14': target_information.limit_phase_t14 if rad_mode == "Transmission" else None,
            'limit_phase_t12': target_information.limit_phase_t12 if rad_mode == "Transmission" else None,
            'limit_phase_t23': target_information.limit_phase_t23 if rad_mode == "Transmission" else None,
        }
        with open(filepkl, 'wb') as f:
            dump(plot_data, f)
    if filepng is not None:
        fig.savefig(filepng, bbox_inches="tight", pad_inches=0.1)
    plt.clf()
    plt.close(fig)


def doppler_shadow_bg(
    doppler_shadow_bool, target_information, night_obj, rv_planet, rv_array, sss_new,
    transit_phase, title_plot, rad_mode, mode, rv_planet_retrieval,
    selected_cmap_name,
    ds_width=20., pdf_total=None, base_dir=None,
    type_transit="Full", maxfev=5000, rv_start=-150, rv_end=150, rv_bin=1
):
    """
    Apply Doppler shadow correction to cross-correlation data.

    Plots the original CC map, then optionally fits and removes the Doppler
    shadow signal using a double-Gaussian model in the stellar rest frame.

    Args:
        doppler_shadow_bool: Whether to apply Doppler shadow correction
        target_information: Target information object
        night_obj: Night object with phase and velocity data
        rv_planet: Planetary RV trace
        rv_array: Radial velocity array
        sss_new: 2D cross-correlation map
        transit_phase: Orbital phase array
        title_plot: Base title for plots
        rad_mode: Radiation mode ('Transmission' or 'Emission')
        mode: Processing mode (0=retrieval, 1=manual)
        rv_planet_retrieval: Retrieval RV trace for overplotting
        selected_cmap_name: Name of the colormap to use
        ds_width: Width for overlap detection between planet and DS traces
        pdf_total: PdfPages object for multi-page output
        base_dir: Output directory path
        type_transit: Transit selection ('Full', 'Up-Mid transit', 'Down-Mid transit')
        maxfev: Maximum function evaluations for curve fitting
        rv_start: Minimum RV for output grid
        rv_end: Maximum RV for output grid
        rv_bin: RV bin size

    Returns:
        np.ndarray: Cross-correlation data after Doppler shadow correction
    """
    from GUIBRUSHR.General_Constants.FunctionsAndConstants.general_functions import rv_DopplerShadow
    filepdf = str(base_dir / "Vrad_Phase.pdf")
    filepng = str(base_dir / "Vrad_Phase.png")
    plot_run_cc_bg(
        rv_array, transit_phase, title_plot, rad_mode, mode, sss_new.T, rv_planet,
        target_information, selected_cmap_name,
        rv_planet_retrieval, doppler_shadow_step=False, rv_DS=None,
        pdf_total=pdf_total, filepdf=filepdf, filepng=filepng, plot_cc_grid=False
    )
    if not doppler_shadow_bool:
        return sss_new.transpose()

    # Doppler shadow correction algorithm
    # Adapted by Di Paola from Borsa, Rainer, Biassoni

    sss_new = sss_new.T

    rv_DS = rv_DopplerShadow(target_information, night_obj.phases) - night_obj.barycentric_velocity

    overlap = np.abs(rv_planet - rv_DS)
    idxs_overlap = np.where(overlap < ds_width)[0]
    try:
        overlap_ph1 = idxs_overlap[0]
        overlap_ph2 = idxs_overlap[-1]
    except IndexError:
        overlap_ph1 = False
        overlap_ph2 = None

    filepdf = str(base_dir / "Vrad_Phase_DS.pdf")
    filepng = str(base_dir / "Vrad_Phase_DS.png")
    plot_run_cc_bg(
        rv_array, transit_phase, title_plot + " - Doppler Shadow", rad_mode, mode, sss_new, rv_planet,
        target_information, selected_cmap_name,
        rv_planet_retrieval, doppler_shadow_step=True, rv_DS=rv_DS,
        pdf_total=pdf_total, filepdf=filepdf, filepng=filepng
    )

    pfn_rvs = []
    for i in range(sss_new.shape[0]):
        pfn_rvs.append(rv_array - rv_DS[i])
    pfn_rvs = np.asarray(pfn_rvs)

    rvmin = np.amin(pfn_rvs[:, 0])
    rvmax = np.amax(pfn_rvs[:, -1])
    rv_shift = np.arange(int(rvmin), int(rvmax) + rv_bin, rv_bin)

    pfn_shiftDS = []
    for i in range(sss_new.shape[0]):
        spl = UnivariateSpline(pfn_rvs[i], sss_new[i], k=3, s=0, ext="zeros")
        pfn_new = spl(rv_shift)
        pfn_shiftDS.append(np.asarray(pfn_new))
    pfn_shiftDS = np.asarray(pfn_shiftDS)

    rv_planet_DS = rv_planet - rv_DS
    rv_DS0 = rv_DS - rv_DS

    filepdf = str(base_dir / "Vrad_Phase_DS_rest_frame.pdf")
    filepng = str(base_dir / "Vrad_Phase_DS_rest_frame.png")
    plot_run_cc_bg(
        rv_shift, transit_phase, title_plot + " - Doppler Shadow Rest Frame", rad_mode, mode,
        pfn_shiftDS, rv_planet_DS,
        target_information, selected_cmap_name,
        rv_planet_retrieval, doppler_shadow_step=True, rv_DS=rv_DS0,
        pdf_total=pdf_total, filepdf=filepdf, filepng=filepng
    )

    if rad_mode == "Transmission":
        select_ph1 = night_obj.intransit_real[0]
        select_ph2 = night_obj.intransit_real[-1]
    else:
        select_ph1 = night_obj.intransit[0]
        select_ph2 = night_obj.intransit[-1]

    if type_transit == "Full":
        idxs_no_overlap = np.concatenate(
            (np.arange(select_ph1, overlap_ph1), np.arange(overlap_ph2, select_ph2 + 1))
        )
        meanDS = np.nanmedian(pfn_shiftDS[idxs_no_overlap[2:-2]], axis=0)
        p0 = (1., max(meanDS) - 1., 0., 10., min(meanDS) - 1, 0., target_information.v_sini / 2.)
        pars, pcov = optimize.curve_fit(bigaussian, rv_shift, meanDS, p0, maxfev=maxfev)
    elif type_transit == "Up-Mid transit":
        idxs_no_overlap_up = np.arange(overlap_ph2, select_ph2 + 1)
        meanDS = np.nanmedian(pfn_shiftDS[idxs_no_overlap_up[:-2]], axis=0)
        p0up = (1., max(meanDS) - 1., 0., 10., min(meanDS) - 1, 0., target_information.v_sini / 2.)
        pars, pcov = optimize.curve_fit(bigaussian, rv_shift, meanDS, p0up, maxfev=maxfev)
    else:
        idxs_no_overlap_down = np.arange(select_ph1, overlap_ph1)
        meanDS = np.nanmedian(pfn_shiftDS[idxs_no_overlap_down[2:]], axis=0)
        p0down = (1., min(meanDS) - 1., 0., 10., max(meanDS) - 1, 0., target_information.v_sini / 2.)
        pars, pcov = optimize.curve_fit(bigaussian, rv_shift, meanDS, p0down, maxfev=maxfev)

    rvDS = pars[2]
    sigmaDS = abs(pars[3])
    heightDS = pars[1] - pars[0]
    errorsDS = np.sqrt(np.abs(np.diag(pcov)))
    pfn_shiftDSremoved = pfn_shiftDS

    textDS = '\n'.join((
        r'$\mathrm{rv_{0}}=%.2f \pm %.2f$' % (rvDS, errorsDS[2],),
        r'$\sigma=%.2f \pm %.2f$' % (sigmaDS, errorsDS[3],),
        r'$\mathrm{height}=%.4f \pm %.4f$' % (heightDS, errorsDS[0] + errorsDS[1],)
    ))

    # Save Doppler shadow fit plot to file (no GUI popup in background mode)
    ds_fit_path = str(base_dir / "doppler_shadow_fit.png")
    fig_ds, ax_ds = plt.subplots(1, 1, figsize=(10, 8))
    ax_ds.set_title("Doppler Shadow Profile")
    ax_ds.set_xlabel("RV [km/s]")
    ax_ds.set_ylabel("Cross-correlation")
    ax_ds.plot(rv_shift, meanDS, label="Median DS profile", color="blue", linewidth=2)
    ax_ds.plot(rv_shift, bigaussian(rv_shift, *pars), label="Double Gaussian fit",
               color="red", linestyle="--", linewidth=2)
    ax_ds.annotate(textDS, xy=(0.02, 0.8), xycoords='axes fraction', fontsize=10,
                   bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
    ax_ds.legend()
    ax_ds.grid(True, alpha=0.3)
    plt.savefig(ds_fit_path, bbox_inches="tight")
    plt.close(fig_ds)

    pfn_shiftDSremovedc = pfn_shiftDS
    pfn_shiftDSremovedc[select_ph1 + 2:select_ph2 - 2] = (
        pfn_shiftDSremoved[select_ph1 + 2:select_ph2 - 2] - bigaussian(rv_shift, *pars)
    )

    pfn_DSremoved, rv_original = shiftRV(pfn_shiftDSremovedc, rv_shift, -rv_DS, rv_bin)

    indexrv = np.where((rv_original >= rv_start) & (rv_original <= rv_end))[0]
    if indexrv.size > rv_array.size:
        indexrv = indexrv[1:]
    elif indexrv.size < rv_array.size:
        indexrv = np.insert(indexrv, 0, indexrv[0] - 1)
    rv_original_cent = rv_original[indexrv]

    pfn_DSremoved_or = pfn_DSremoved[:, indexrv]
    sss_new_doppler_shadow = pfn_DSremoved[:, indexrv]

    filepdf = str(base_dir / "Vrad_Phase_DS_corrected.pdf")
    filepng = str(base_dir / "Vrad_Phase_DS_corrected.png")
    plot_run_cc_bg(
        rv_original_cent, transit_phase, title_plot + " - Doppler Shadow Corrected", rad_mode, mode,
        pfn_DSremoved_or, rv_planet,
        target_information, selected_cmap_name,
        rv_planet_retrieval, doppler_shadow_step=True, rv_DS=rv_DS,
        pdf_total=pdf_total, filepdf=filepdf, filepng=filepng
    )

    return sss_new_doppler_shadow


def run_cc_func_bg(params, path_default):
    """
    Background version of run_cc_func.

    Reads all parameters from the exchange dictionary and performs the
    cross-correlation analysis. Returns the path to the output PDF file.
    """
    from GUIBRUSHR.GUI.DataInterface.DBSQLite3 import DBSQLite3
    from GUIBRUSHR.GUI.DataInterface.DataInterface import get_target_info
    from GUIBRUSHR.General_Constants.Classes.Night import Night
    from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
    from GUIBRUSHR.General_Constants.FunctionsAndConstants.general_functions import (
        create_fits, convolve_resolution, rv_planet_and_star, calculate_rv_planet_and_star
    )
    # Unpack parameters
    path_results = params["path_results"]
    path_instruments = params["path_instruments"]
    order_sel = params["order_sel"]
    dates = params["dates"]
    instruments = params["instruments"]
    tell_rm_method = params["tell_rm_method"]
    wavelength = params["wavelength"]
    transit_depth = params["transit_depth"]
    model_name = params["model_name"]
    target = params["target"]
    ccffilter = params["ccffilter"]
    mode = params["mode"]
    rad_mode = params["rad_mode"]
    speed = params["speed"]
    convolve_instrument = params["convolve_instrument"]
    path_folder_targets = params["path_folder_targets"]

    # GUI widget values
    doppler_shadow_bool = params["doppler_shadow_bool"]
    ds_width = params["ds_width"]
    maxfev = params["maxfev"]
    type_transit = params["type_transit"]
    rv_start = params["rv_start"]
    rv_end = params["rv_end"]
    rv_bin = params["rv_bin"]
    transit_limits = params["transit_limits"]
    eccentricity = params["eccentricity"]
    mask_night_str = params["mask_night_str"]
    mask_rv = params["mask_rv"]
    min_rv_mask = params["min_rv_mask"]
    max_rv_mask = params["max_rv_mask"]
    mask_phase = params["mask_phase"]
    min_phase_mask = params["min_phase_mask"]
    max_phase_mask = params["max_phase_mask"]
    extra_kp = params["extra_kp"]
    extra_rv_val = params["extra_rv"]
    pdf_orders_ccf = params["pdf_orders_ccf"]
    bin_multi = params["bin_multi"]
    type_cc = params["type_cc"]
    selected_cmap_name = params["selected_cmap_name"]
    speeds = params["speeds"]

    # Set up output directories and files
    n_nights = len(dates)
    if mode == 0:
        cross_corr_dir = Path(path_results, "cross_correlation")
        prefix = ""
    else:
        cross_corr_dir = Path(path_instruments[0], dates[0], "cross_correlation",
                              tell_rm_method, order_sel, model_name)
        if n_nights > 1:
            str_nights = "_".join(dates)
            prefix = "multi_" + str_nights
        else:
            prefix = ""
    cross_corr_dir = Path(cross_corr_dir, prefix)

    filename_pdf_total = str(cross_corr_dir / "Vrad_Phase.pdf")
    cross_corr_dir.mkdir(parents=True, exist_ok=True)
    pdf_total = PdfPages(filename_pdf_total)

    # Get target information
    target_information = get_target_info(path_folder_targets, target, rad_mode)

    # Initialize arrays
    rv_extra = None
    kp_phases = np.array([])
    kp_extra_phases = np.array([])
    v_totals_earth_frame = np.array([])
    list_traces_binned = np.array([])
    list_phases_binned = np.array([])
    night_phase_ranges = []

    # Track instrument changes across nights for derivative parameter indexing
    last_instrument = instruments[0]
    index_night_in_current_instrument = -1
    counter_derivative_params = -1

    rv_array = np.arange(rv_start, rv_end + rv_bin, rv_bin)
    nrv = len(rv_array)
    clight = ConstantVariables.CLIGHT
    mask_night_values = [int(x.strip()) for x in mask_night_str.split(',')]

    for ii in range(n_nights):
        print("Instrument:", instruments[ii], "Date:", dates[ii])

        current_instrument = instruments[ii]
        if current_instrument != last_instrument:
            index_night_in_current_instrument = -1

        if convolve_instrument:
            DB = DBSQLite3(path_default)
            instrument_obj = DB.get_instrument_by_key(current_instrument)
            DB.close_DB()
            wl_convolved_resolution, model_convolved_resolution = convolve_resolution(
                wavelength, transit_depth, instrument_obj.hwhm_km_s, 0, 0.1
            )
        else:
            wl_convolved_resolution = wavelength
            model_convolved_resolution = transit_depth

        index_night_in_current_instrument += 1
        if index_night_in_current_instrument != 0:
            counter_derivative_params += 1
        last_instrument = current_instrument

        if mode == 0:
            path_order = Path(path_instruments[ii], dates[ii])
            path_order_tell = path_order
            path_order_test = path_order_tell
        else:
            path_order = Path(path_instruments[ii], dates[ii], "orders_selection", order_sel)
            path_order_tell = path_order / tell_rm_method
            path_order_test = str(Path(path_order_tell, "test"))

        path_good_order = str(path_order / ("good_order_" + order_sel + ".pkl"))

        if transit_limits == "T14":
            limit_phase = target_information.limit_phase_t14
        else:
            limit_phase = target_information.limit_phase_t23

        night_obj = Night(
            limit_phase, path_instruments[ii],
            dates[ii], rad_mode, path_good_order, path_order_tell, path_order_test,
            table_output_file=None, CC=True, simulation=False, synthetic=False,
            retrieval_standardize="From pkl"
        )

        print("Number of spectra in transit: ",
              str(night_obj.count_in_transit_real) + "/" + str(night_obj.n_phases_considered))

        wlensp_spline = night_obj.lambdas
        if np.ndim(wlensp_spline) == 2 and speed == speeds[0]:
            speed = speeds[0]

        if speed == speeds[0]:  # AUTO mode
            if "A" in dates[ii]:
                lambd = np.array(wlensp_spline[:, :, :2])
            else:
                lambd = wlensp_spline[:, :, 0]
        else:  # FULL mode
            lambd = wlensp_spline

        dim_lambd = np.ndim(lambd)
        plus_bad = []

        sigma = np.ma.array(night_obj.sigma_spectra, mask=night_obj.maskinvm)
        spectr = np.ma.array(night_obj.spectra, mask=night_obj.maskinvm)
        mask_for_lambd = np.array(night_obj.maskinvm[:, :, 0])

        if dim_lambd > 2:
            mask_for_lambd = np.expand_dims(mask_for_lambd, axis=2)
            mask_for_lambd = np.tile(mask_for_lambd, (1, 1, 2))

        lambd = np.ma.array(lambd, mask=mask_for_lambd)

        transit_phase = night_obj.phase_new_range

        rv_planet, rv_star, kpph = rv_planet_and_star(
            eccentricity, target_information, 0, 0, 0,
            night_obj, "CC", 0, None, 0
        )

        v_totals_earth_frame = np.append(v_totals_earth_frame, rv_planet - kpph)
        kp_phases = np.append(kp_phases, kpph)

        ccor = np.zeros([nrv, night_obj.n_phases_considered, night_obj.n_good_orders])
        ccor_new = np.zeros([nrv, night_obj.n_phases_considered, night_obj.n_good_orders])

        modelint_c = splrep(wl_convolved_resolution, model_convolved_resolution)

        for k in range(night_obj.n_good_orders):
            print(f"Order: {k+1}/{night_obj.n_good_orders}")

            if np.sum(np.array(plus_bad) == night_obj.good_order[k]) != 0:
                ccor[:, :, k] = 0
                continue

            if dim_lambd == 2:
                lobs = np.array(lambd[:, k])
                lobs_tile = np.tile(lobs, (nrv, 1)).T
            else:
                lobs = np.array(lambd[:, k, :])
                lobs_tile = np.transpose(np.tile(lobs, (nrv, 1, 1)), (1, 2, 0))

            dl = lobs_tile * rv_array / clight
            modelint = splev(lobs_tile - dl, modelint_c, der=0)

            if "A" in dates[ii]:
                modelint = np.repeat(modelint, int(night_obj.n_phases_considered / 2), axis=1)

            s1 = spectr[:, k, :]
            s1_mean = np.mean(s1, axis=0)
            s1_centered = s1 - s1_mean[None, :]

            s2_mean = np.mean(modelint, axis=0)
            s2_centered = modelint - s2_mean[None, :]

            if type_cc in [ConstantVariables.LIST_CORRELATION_METHODS[0],
                           ConstantVariables.LIST_CORRELATION_METHODS[1]]:
                if dim_lambd == 2:
                    numerator = np.tensordot(s1_centered, s2_centered, axes=([0], [0]))
                    norm1 = np.sqrt(np.sum(s1_centered ** 2, axis=0))
                    norm2 = np.sqrt(np.sum(s2_centered ** 2, axis=0))
                    denom = np.outer(norm1, norm2)
                else:
                    numerator = np.zeros([night_obj.n_phases_considered, nrv])
                    denom = np.zeros([night_obj.n_phases_considered, nrv])
                    norm1 = np.sqrt(np.sum(s1_centered ** 2, axis=0))
                    for img in range(night_obj.n_phases_considered):
                        numerator[img, :] = np.dot(s1_centered[:, img], s2_centered[:, img, :])
                        norm22 = np.sqrt(np.sum(s2_centered[:, img, :] ** 2, axis=0))
                        denom[img, :] = np.outer(norm1[img], norm22)
                cc_res = numerator / denom
            else:
                sig = sigma[:, k, :]
                weighted = s1_centered / (sig ** 2)
                cc_res = np.zeros([night_obj.n_phases_considered, nrv])
                if dim_lambd == 2:
                    cc_res = np.tensordot(weighted, s2_centered, axes=([0], [0]))
                else:
                    for img in range(night_obj.n_phases_considered):
                        cc_res[img, :] = np.dot(weighted[:, img], s2_centered[:, img, :])

            cc_res[~np.isfinite(cc_res)] = 0
            ccor[:, :, k] = cc_res.T
            ccor_new[:, :, k] = ccor[:, :, k]

            if ccffilter and rad_mode == "Transmission":
                if transit_limits == "T14":
                    limit_phase = target_information.limit_phase_t14
                else:
                    limit_phase = target_information.limit_phase_t23
                cond1 = np.array(transit_phase) < -limit_phase
                cond2 = np.array(transit_phase) > limit_phase

                if np.sum(cond1) == 0:
                    cond1 = cond2
                elif np.sum(cond2) == 0:
                    cond2 = cond1

                ccfrif1 = np.mean(ccor[:, cond1, k], axis=1)
                ccfrif2 = np.mean(ccor[:, cond2, k], axis=1)

                pc_ccf = np.ones((len(rv_array), 3))
                pc_ccf[:, 1] = ccfrif1
                pc_ccf[:, 2] = ccfrif2

                u, s, vh = np.linalg.svd(pc_ccf, full_matrices=False)
                uT = u.T
                nD = np.diag(1 / s)
                vHcT = vh.conj().T

                for ic in range(len(kpph)):
                    ccfi = ccor[:, ic, k]
                    c = np.dot(uT, ccfi)
                    w = np.dot(nD, c)
                    x = np.dot(vHcT, w)
                    ynew = np.dot(pc_ccf, x)
                    ccor_new[:, ic, k] = ccfi - ynew

        sss_new = np.sum(ccor_new, axis=2)

        apply_mask_to_night = False
        if ii < len(mask_night_values):
            apply_mask_to_night = (mask_night_values[ii] == 1)

        if apply_mask_to_night and mask_rv:
            rv_mask_indices = np.where((rv_array >= min_rv_mask) & (rv_array <= max_rv_mask))[0]
            sss_new[rv_mask_indices, :] = 0
            print(f"Night {ii+1} ({dates[ii]}): Applied RV mask: {min_rv_mask} to {max_rv_mask} km/s"
                  f" ({len(rv_mask_indices)} RV points masked)")

        if apply_mask_to_night and mask_phase:
            phase_mask_indices = np.where(
                (transit_phase >= min_phase_mask) & (transit_phase <= max_phase_mask)
            )[0]
            sss_new[:, phase_mask_indices] = 0
            print(f"Night {ii+1} ({dates[ii]}): Applied phase mask: {min_phase_mask} to {max_phase_mask}"
                  f" ({len(phase_mask_indices)} phase points masked)")

        if not apply_mask_to_night and (mask_rv or mask_phase):
            print(f"Night {ii+1} ({dates[ii]}): Skipped masking "
                  f"(mask_night={mask_night_values[ii] if ii < len(mask_night_values) else 'N/A'})")

        if ii == 0:
            list_traces_binned = np.array(sss_new.T)
            list_phases_binned = np.array(transit_phase)
        else:
            list_traces_binned = np.append(list_traces_binned, sss_new.T, axis=0)
            list_phases_binned = np.append(list_phases_binned, transit_phase)

        night_phase_ranges.append((
            dates[ii], instruments[ii],
            float(np.min(transit_phase)), float(np.max(transit_phase))
        ))

        if mode == 0:
            base_dir = Path(path_results, "cross_correlation", instruments[ii], dates[ii])
            kp_extra = 0
            vsys_extra = 0
        else:
            base_dir = Path(path_instruments[ii], dates[ii], "cross_correlation",
                            tell_rm_method, order_sel, model_name)
            kp_extra = extra_kp
            vsys_extra = extra_rv_val

        base_dir.mkdir(parents=True, exist_ok=True)

        path_sss = str(base_dir / "sss.fits")
        path_cor = str(base_dir / "cor.fits")
        path_pkl = str(base_dir / "range.pkl")

        print("Saving in:", base_dir)

        range_dictionary = {
            "min_rv": rv_start,
            "max_rv": rv_end,
            "step_rv": rv_bin,
            "phase_obs": night_obj.phases,
            "barycentric_velocity": night_obj.barycentric_velocity,
            "hjd_ck": night_obj.hjd_ck,
            "limit_phase": limit_phase,
        }
        with open(path_pkl, "wb") as f:
            dump(range_dictionary, f)

        if mode == 0 or kp_extra > 0:
            phases = night_obj.phases_considered
            if -999 in night_obj.hjd_ck:
                hjd0 = 0
                period = 1
                times_of_observation = phases
            else:
                hjd0 = target_information.hjd0
                period = target_information.period
                times_of_observation = night_obj.hjd_ck_considered

            if mode == 0:
                with open(str(Path(path_results, "output_data_contribution.pkl")), "rb") as fo:
                    res = load(fo)
                stringa_med = res["stringa_med"]
                name_and_values_med = stringa_med.split("\n")[:-1]
                params_ret = {
                    line.split('=')[0].strip(): float(line.split('=')[1].strip())
                    for line in name_and_values_med
                }
                params_ret = {key.split()[-1].strip("'"): value for key, value in params_ret.items()}
                kpret = params_ret["kp"]
                vsysret = params_ret.get("rv", 0)
                ecc = params_ret.get("ecc", 0)
                opi = params_ret.get("opi", 1.57)
                dVsys_arr = [params_ret[key] for key in params_ret.keys() if 'dVsys' in key]
                current_dvsys = (
                    0 if (index_night_in_current_instrument == 0 or len(dVsys_arr) == 0)
                    else dVsys_arr[counter_derivative_params]
                )
                new_kp = kpret
                new_vsys_extra = vsysret
                new_dvsys = current_dvsys
            else:
                new_kp = kp_extra
                new_vsys_extra = vsys_extra
                new_dvsys = 0
                ecc = target_information.eccentricity
                opi = target_information.argument_of_periastron

            rv_extra, _, kpp_extra = calculate_rv_planet_and_star(
                hjd0, times_of_observation, eccentricity, period, ecc, opi, new_kp,
                None, phases, -1, target_information.systemic_velocity,
                night_obj.barycentric_velocity_considered, new_dvsys,
                vsys_extra=new_vsys_extra
            )
            kp_extra_phases = np.append(kp_extra_phases, kpp_extra)

        title_plot = f"Instrument: {instruments[ii]}, Night: {dates[ii]}"

        final_sss_new = doppler_shadow_bg(
            doppler_shadow_bool, target_information, night_obj, rv_planet, rv_array, sss_new,
            transit_phase, title_plot, rad_mode, mode, rv_extra,
            selected_cmap_name,
            ds_width=ds_width, pdf_total=pdf_total, base_dir=base_dir,
            type_transit=type_transit, maxfev=maxfev,
            rv_start=rv_start, rv_end=rv_end, rv_bin=rv_bin
        )

        if pdf_orders_ccf:
            plot_and_save_cor(ccor_new, rv_array, transit_phase, night_obj, base_dir)

        create_fits(ccor_new.transpose(2, 1, 0), path_cor)
        create_fits(final_sss_new, path_sss)

    # Process multi-night data if available
    if n_nights > 1:
        index_sort = list_phases_binned.argsort()
        list_traces_binned = list_traces_binned[index_sort, :]
        list_phases_binned = list_phases_binned[index_sort]
        kp_phases = kp_phases[index_sort]
        v_totals_earth_frame = v_totals_earth_frame[index_sort]

        if len(kp_extra_phases) > 0:
            kp_extra_phases = kp_extra_phases[index_sort]

        shifted_profiles = []
        for j in range(len(list_phases_binned)):
            total_shift = v_totals_earth_frame[j]
            interp_func = interp1d(rv_array, list_traces_binned[j, :],
                                   kind="linear", fill_value="extrapolate")
            shifted_profile = interp_func(rv_array + total_shift)
            shifted_profiles.append(shifted_profile)

        shifted_profiles = np.array(shifted_profiles)

        limit_rv = int(rv_end * 3 / 5)
        rv_indices = np.where((rv_array > -limit_rv) & (rv_array < limit_rv))[0]
        shifted_profiles = shifted_profiles[:, rv_indices]
        new_rv = rv_array[rv_indices]

        binned_profile, binned_phases, binned_kp, binned_kp_extra = bin_averaging(
            shifted_profiles, list_phases_binned,
            kp_phases, kp_extra_phases,
            bin_multi
        )

        # Analyze phase coverage and fill gaps with zeros
        phase_gaps = analyze_phase_coverage(night_phase_ranges, bin_multi)
        binned_profile, binned_phases, binned_kp, binned_kp_extra, n_gap_rows = fill_phase_gaps(
            binned_profile, binned_phases, binned_kp, binned_kp_extra,
            bin_multi, phase_gaps
        )

        # Shift to planet rest frame (on full data including all gap rows)
        step = rv_array[1] - rv_array[0]
        shifts_cols = np.round(binned_kp / step).astype(int)
        shifted_binned = np.zeros_like(binned_profile)

        for i in range(binned_profile.shape[0]):
            shift = shifts_cols[i]
            if shift > 0:
                shifted_binned[i, :-shift] = binned_profile[i, shift:]
            elif shift < 0:
                shifted_binned[i, -shift:] = binned_profile[i, :shift]
            else:
                shifted_binned[i, :] = binned_profile[i, :]

        binned_pdf_path = str(cross_corr_dir / "binned_trace.pdf")
        binned_png_path = str(cross_corr_dir / "binned_trace.png")

        plot_run_cc_bg(
            new_rv, binned_phases,
            "Binned trace", rad_mode, mode,
            binned_profile, binned_kp,
            target_information, selected_cmap_name,
            binned_kp_extra, doppler_shadow_step=False, rv_DS=None,
            pdf_total=pdf_total,
            filepdf=binned_pdf_path, filepng=binned_png_path, phase_gaps=np.array(phase_gaps)
        )
        print(f"Saved standalone binned trace: {binned_pdf_path}")

        temp_extra_kp = binned_kp_extra - binned_kp if len(binned_kp_extra) > 0 else None

        plot_run_cc_bg(
            new_rv, binned_phases,
            "Binned trace in planet rest frame", rad_mode, mode,
            shifted_binned, binned_kp - binned_kp,
            target_information, selected_cmap_name,
            temp_extra_kp, doppler_shadow_step=False, rv_DS=None,
            pdf_total=pdf_total, phase_gaps=np.array(phase_gaps)
        )

        # Save full binning data (with all gap rows) as pickle
        binning_data = {
            "target_information": target_information,
            "binned_profile": binned_profile,
            "binned_phases": binned_phases,
            "binned_kp": binned_kp,
            "binned_kp_extra": binned_kp_extra,
            "shifted_binned": shifted_binned,
            "rv_array": new_rv,
            "bin_size": bin_multi,
            "night_phase_ranges": night_phase_ranges,
            "phase_gaps": phase_gaps,
            "n_gap_rows": n_gap_rows,
            "rad_mode": rad_mode,
            "mode": mode,
            "selected_cmap_name": selected_cmap_name,
        }
        binning_pkl_path = str(cross_corr_dir / "binning_data.pkl")
        with open(binning_pkl_path, "wb") as f:
            dump(binning_data, f)
        print(f"Saved binning data: {binning_pkl_path}")

    pdf_total.close()
    return filename_pdf_total


def main():
    exchange_path = sys.argv[1]
    path_default = sys.argv[2]
    # Add project root to sys.path so GUIBRUSHR modules can be imported
    sys.path.append(path_default)
    os.chdir(path_default)

    with open(str(Path(exchange_path, "exchange_cc.pkl")), "rb") as f:
        params = pickle.load(f)

    try:
        filename_pdf_total = run_cc_func_bg(params, path_default)
        with open(str(Path(exchange_path, "result_cc.pkl")), "wb") as f:
            pickle.dump({"filename_pdf_total": filename_pdf_total}, f)
    except Exception as e:
        with open(str(Path(exchange_path, "error_cc.pkl")), "wb") as f:
            pickle.dump(str(e) + "\n" + traceback.format_exc(), f)


if __name__ == "__main__":
    main()