#!/usr/bin/env python

# SPDX-License-Identifier: GPL-3.0-or-later
# FileType: SOURCE
# SPDX-FileCopyrightText: 2025 GFZ Helmholtz Centre for Geosciences
# SPDX-FileCopyrightText: 2025, "Eva Boergens" at GFZ Potsdam
"""Main module."""
import os
import re
from pathlib import Path
from typing import Dict, Union, List

import numpy as np

from geogravl3.io import read_gfc
from geogravl3.io.writers import write_tws_nc, write_obp_nc, write_ice_nc, save_timeseries_to_csv, \
    save_ice_timeseries_to_csv
from geogravl3.processing.grid_processing.l3_processing import (
    tws_processing, im_processing, obp_processing, prepare_land_ocean_masks, leakage_correction
)
from geogravl3.processing.grid_processing.remove_coseismic import get_eq_filenames
from geogravl3.processing.shc_processing.degree_1_terms import estimate_degree1_sh
from geogravl3.processing.shc_processing.replace_low_degrees import replace_low_degrees
from geogravl3.processing.spherical_harmonics.sh_synthesis import sh_synthesis
from geogravl3.processing.shc_processing.gia_correction import reduce_gia_model
from geogravl3.processing.shc_processing.run_filter import run_filter
from geogravl3.processing.shc_processing.s2_aliased_signal import remove_s2_alias
from geogravl3.processing.timeseries import remove_mean_sh
from geogravl3.datamodels.grids import Grid3DObject, Grid3DIceObject
from geogravl3.datamodels.shc import SHObject
from geogravl3.io.readers import read_grid_definition, read_load_love_numbers, read_input_folder, \
    read_region_geojson, read_ice_basins_from_kernel
from geogravl3.utils.utils import create_logger, get_constant
from geogravl3.utils.date_utils import datetime_to_date_float, reference_period_2_datetime
from geogravl3.config import Config
from geogravl3.processing.l4_processing.l4_timeseries import (
    precompute_region_masks, compute_regional_timeseries,
    compute_regional_timeseries_std, compute_regional_ice_timeseries, compute_regional_ice_timeseries_std)
from geogravl3.data_downloads.downloads import download_resources
from geogravl3.io.readers import read_l2b_spherical_harmonic_coefficients


class ResultObjects:
    """
    Define an object to store all intermediate results.

    Attributes
    ----------
    type (str): either shc or grid
    name (str): describing latest processing step
    value: Value of latest processing step, either List[SHObject], Grid3DObject,
    or Dict[str, Union[Grid3DObject (for TWS and OBP), Grid3DIceObject (for ice)]]
    filter_name (str): name of the applied filter for this processing line. None before filtering
    domain (str): domain name after the domain specific processing

    """

    def __init__(self, type: str = None,
                 name: str = None,
                 value: Union[List[SHObject], Grid3DObject, Dict[str, Union[Grid3DObject, Grid3DIceObject]]] = None,
                 filter_name: str = None,
                 domain: str = None
                 ):
        """
        Initialize a ResultObject instance.

        Attributes:
        type: Category in which the results fall, either 'shc' or 'grid'
        name: Specific name related to the processing step, should be self-explanatory
        filter_name: Name of the applied filter, None before filtering
        domain: Name of the domain, None before domain specific stuff
        value: Result to be stored, either list

        """
        self.type = type
        self.name = name
        self.value = value
        self.filter_name = filter_name
        self.domain = domain


def run_pipeline(*, config_dict: dict):
    """
    Execute the pipeline with a provided configuration dictionary and set up logging.

    This function initializes and configures the pipeline by accepting a configuration
    dictionary, validating, and transforming it into the required format. It also sets up
    logging for both file output and console output using the provided settings in the
    configuration.

    Parameters:
        config_dict (dict): Dictionary containing pipeline configurations. This must
        adhere to the structure and types expected by the `Config` class.

    Raises:
        Exception: If there is an issue with validating, setting up the configuration,
        or initializing the logging pipeline, an exception is raised that includes
        details of the underlying issue.
    """
    # get a logger
    logger = create_logger(config_dict["mandatory_settings"]["output_folder"],
                           config_dict["optional_settings"]["logging_level"])

    # Download required resources if not already downloaded or running within a local repository
    download_resources(logger=logger)

    try:
        config_dict = Config(**config_dict).model_dump(by_alias=True)
        logger.info(f"Starting geogravL3 with the following configuration: {config_dict}")
    except Exception as e:
        message = f"Failed to start geogravL3 pipeline => {e}"
        logger.error(message)
        raise Exception(message) from e

    # read input files into a list of SH coefficient Objects
    logger.info("Reading input data")
    input_shc, dict_date_to_filename = read_input_folder(
        file_list=config_dict["mandatory_settings"]["input_folder"],
        max_degree=config_dict["optional_settings"]["max_degree"],
        logger=logger
    )
    # set up a dictionary to store the results
    # We will call this 'sh_results' to clarify it holds Spherical Harmonics
    sh_results = [ResultObjects('shc', 'input_data', input_shc)]

    # ---------------------------------------------------------
    # SHC PROCESSING OR READING
    # ---------------------------------------------------------
    read_l2b_mode = (
        config_dict["optional_settings"].get("read_l2b", False)
        or config_dict["optional_settings"].get("readL2BOutput", False)
    )
    time_mean = Path(config_dict["optional_settings"]["time_mean"])
    if time_mean.is_file():
        ref_period = None
    else:  # is string of format `YYYY-MM` or `YYYY/MM-YYYY/MM`
        ref_period = reference_period_2_datetime(config_dict["optional_settings"]["time_mean"])

    if not read_l2b_mode:
        # -----------------------------------------------------
        # PREPROCESSING (Standard Mode)
        # -----------------------------------------------------
        logger.info("Starting standard SH preprocessing...")

        # 1. Remove Mean Field
        logger.info("Removing mean field")
        sh_list = sh_results[0].value
        times = datetime_to_date_float(np.array([s.date for s in sh_results[0].value]))

        if time_mean.is_file():
            sh_mean_field = read_gfc(logger=logger, file_name=config_dict["optional_settings"]["time_mean"])
            if sh_mean_field.get_max_degree() < sh_list[0].get_max_degree():
                messages = (f"Max degree of the mean field {config_dict['optional_settings']['time_mean']} with "
                            f"{sh_mean_field.get_max_degree()} needs to be equal or larger than the max degree of the "
                            f"input data with {sh_list[0].get_max_degree()} ")
                logger.error(messages)
                raise ValueError(messages)
            n_max = sh_list[0].get_max_degree()
            sh_demeaned = [SHObject(logger=logger,
                                    date=s.date,
                                    cnm=s.cnm - sh_mean_field.cnm[0:n_max + 1, 0:n_max + 1, ],
                                    snm=s.snm - sh_mean_field.snm[0:n_max + 1, 0:n_max + 1, ])
                           for s in sh_list]
            sh_results = [ResultObjects('shc', 'reduced_mean', sh_demeaned)]
        else:
            if len(sh_list) == 1:
                logger.info("Only one time step in input data, mean removal skipped.")
            else:
                if len(ref_period) == 2:
                    mean_dict = remove_mean_sh(
                        logger=logger, times=times, sh_list=sh_list,
                        start_idx=ref_period[0], end_idx=ref_period[1]
                    )
                else:
                    mean_dict = remove_mean_sh(logger=logger, times=times, sh_list=sh_list)
                sh_results = [ResultObjects('shc', 'reduced_mean', mean_dict['sh_demeaned'])]

        # 2. Filtering
        flag_nofilt = any(
            domain.lower() == ice
            for domain in config_dict["optional_settings"]["domain"]
            for ice in ["ice", "greenland", "antarctica"]
        )
        filters = config_dict["optional_settings"]["filter"]

        if filters is not None:
            logger.info(f"Filtering with: {filters}")
            temp_results = []
            temp_results.extend([
                ResultObjects('shc', 'filtered',
                              run_filter(filter_name=f, input_shc=sh_results[0].value, lmax=None,
                                         dict_date_to_filename=dict_date_to_filename, logger=logger),
                              filter_name=f)
                for f in filters
            ])
            if config_dict["optional_settings"]["leakage"]:
                new = set()
                pattern = re.compile(r"([VD]DK)([2-5])")
                for filt in filters:
                    m = pattern.fullmatch(filt)
                    if m:
                        prefix, num = m.groups()
                        n = int(num)

                        new.add(f"{prefix}{n - 1}")
                        new.add(f"{prefix}{n + 1}")
                filters_leakage = list(new - set(filters))
                temp_results.extend([
                    ResultObjects('shc', 'filtered',
                                  run_filter(filter_name=f, input_shc=sh_results[0].value, lmax=None,
                                             dict_date_to_filename=dict_date_to_filename, logger=logger),
                                  filter_name=f"{f}_leakage")
                    for f in filters_leakage
                ])
            if flag_nofilt:
                temp_results.append(ResultObjects('shc', 'filtered', sh_results[0].value, filter_name='nofilt'))
            sh_results = temp_results
        elif flag_nofilt:
            logger.info("Filtering skipped (nofilt only)")
            sh_results = [ResultObjects('shc', 'filtered', sh_results[0].value, filter_name='nofilt')]
        else:
            logger.info("Filtering skipped")

        # 3. Low degree replacement
        if config_dict["optional_settings"]['lowdegree_coefficients'] is not None:
            logger.info("Replace low degree harmonics")
            temp_results = []
            for r in sh_results:
                output_shc = replace_low_degrees(
                    logger=logger, shc=r.value,
                    filename_replacements=config_dict["optional_settings"]["lowdegree_coefficients"]
                )
                temp_results.append(ResultObjects('shc', 'lowdegree_replaced', output_shc, filter_name=r.filter_name))
            sh_results = temp_results

        # 4. GIA model
        if config_dict["optional_settings"]["gia_model"] is not None:
            logger.info("Subtracting GIA model")
            temp_results = []
            for r in sh_results:
                output_shc = reduce_gia_model(
                    logger=logger,
                    filename_gia_model=config_dict["optional_settings"]["gia_model"],
                    shc=r.value
                )
                temp_results.append(ResultObjects('shc', 'gia_reduced', output_shc, filter_name=r.filter_name))
            sh_results = temp_results

        # 5. Geocenter motion
        if config_dict["optional_settings"]['insert_geocenter_motion']:
            logger.info("Insert geocentre motion to degree 1 coefficients.")
            load_love_numbers = read_load_love_numbers(logger=logger,
                                                       file_name=config_dict["optional_settings"]["love_numbers"])
            lo_mask = prepare_land_ocean_masks(logger=logger,
                                               file_land_ocean_mask=config_dict['optional_settings']['land_ocean_mask'])
            temp_results = []
            for r in sh_results:
                output_shc, cs1 = estimate_degree1_sh(
                    logger=logger, sh_objects=r.value, lomask=lo_mask, lmax=45, love_numbers=load_love_numbers
                )
                temp_results.append(ResultObjects('shc', 'deg_1_replaces', output_shc, filter_name=r.filter_name))
            sh_results = temp_results

        # 6. S2 aliasing
        if config_dict["optional_settings"]["remove_s2_alias"]:
            logger.info("Remove s2 aliased signal (161-day period)")
            temp_results = []
            for r in sh_results:
                if ref_period is None or len(ref_period) == 1:
                    output_shc = remove_s2_alias(logger=logger, shc=r.value)
                elif len(ref_period) == 2:
                    output_shc = remove_s2_alias(logger=logger, shc=r.value, start_idx=ref_period[0],
                                                 end_idx=ref_period[1])

                temp_results.append(ResultObjects('shc', 's2_removed', output_shc, filter_name=r.filter_name))
            sh_results = temp_results

        # 7. Write L2B Output (Only if NOT reading L2B)
        write_l2b_mode = config_dict["optional_settings"].get("write_l2b", False)
        if write_l2b_mode:
            from geogravl3.io.writers import write_spherical_harmonic_coefficients_to_netcdf
            l2b_out = config_dict["optional_settings"].get("l2b_output_file")
            if l2b_out is None:
                l2b_out = os.path.join(config_dict["mandatory_settings"]["output_folder"], "l2b_output.nc")
            logger.info(f"write_l2b=True -> writing SH coefficients to {l2b_out}")

            # Write the first result object (usually the filtered/processed one)
            # If multiple filters exist, this blindly takes the first one in the list.
            write_spherical_harmonic_coefficients_to_netcdf(logger=logger, filename=l2b_out,
                                                            spherical_harmonic_objects=sh_results[0].value)

    else:
        # -----------------------------------------------------
        # READ L2B MODE (Skips preprocessing)
        # -----------------------------------------------------
        logger.info("read_l2b=True -> skipping preprocessing and loading L2B SH file.")
        l2b_file = config_dict["optional_settings"].get("l2b_file", None)
        if l2b_file is None:
            raise ValueError("read_l2b=True but no 'l2b_file' was provided in config.")

        sh_list = read_l2b_spherical_harmonic_coefficients(
            logger=logger,
            filename=l2b_file
        )
        # Use 'l2b' instead of 'nofilt' so that TWS/OBP processing
        # (which skips 'nofilt') will actually process this data.
        sh_results = [ResultObjects('shc', 'input_data', sh_list, filter_name='l2b')]

        logger.info("L2B spherical harmonic coefficients loaded successfully.")
        logger.info("Skipping: mean removal, filtering, low-degree correction, GIA, S2 aliasing, degree-1.")

    # ---------------------------------------------------------
    # SH SYNTHESIS
    # ---------------------------------------------------------
    logger.info("SH synthesis")
    grid_info = read_grid_definition(file_name=config_dict["optional_settings"]["grid"])
    love_numbers = read_load_love_numbers(logger=logger, file_name=config_dict["optional_settings"]["love_numbers"])

    # Separate list for grids. sh_results remains available for Ice processing.
    grid_results = []

    for r in sh_results:
        sh_list = r.value
        maxdeg = sh_list[0].get_max_degree()
        output_grid = sh_synthesis(
            logger=logger, shc=sh_list, gridinfo=grid_info,
            love_numbers=love_numbers, n_max=maxdeg,
            ref_surface=config_dict["optional_settings"]["reference_surface"]
        )
        grid_results.append(ResultObjects(
            type='grid', name='shs', value=output_grid, filter_name=r.filter_name, domain=None
        ))

    # Initialize a clean list for final domain results (TWS/OBP/Ice grids)
    l3_results = []

    # Prepare Domain variables
    land_domains = ["all", "land"]
    ice_domains = ["ice", "greenland", "antarctica"]

    # -----------------------
    # TWS (Land) Processing -> Uses grid_results
    # -----------------------
    if any(domain in land_domains for domain in config_dict["optional_settings"]["domain"]):
        list_earthquakes_filenames = get_eq_filenames(config_dict["optional_settings"]["earthquake"])

        # Domain: Land
        if 'land' in config_dict["optional_settings"]["domain"]:
            logger.info("TWS calculation (Land)")
            for r in grid_results:
                name = r.filter_name or ''
                if name == 'nofilt' or '_leakage' in name:
                    continue
                tws, tws_std = tws_processing(
                    logger=logger, input_ewh_grid=r.value,
                    file_land_ocean_mask=config_dict["optional_settings"]["land_ocean_mask"],
                    domain='land',
                    file_ice_mask=config_dict["optional_settings"]["land_ice_mask"],
                    list_earthquakes=list_earthquakes_filenames,
                    file_climatology=config_dict["optional_settings"]["climatology"]
                )
                l3_results.append(ResultObjects(type='grid', name='tws',
                                                value={'tws': tws, 'tws_std': tws_std},
                                                filter_name=r.filter_name, domain='land'))
                pattern = re.compile(r"([VD]DK)([2-5])")
                if config_dict["optional_settings"]["leakage"] and pattern.fullmatch(r.filter_name):
                    m = pattern.fullmatch(r.filter_name)
                    prefix, num = m.groups()
                    filter_lower = f"{prefix}{int(num) - 1}"
                    grid_filter_lower = next((g.value for g in grid_results if filter_lower in g.filter_name))
                    filter_upper = f"{prefix}{int(num) + 1}"
                    grid_filter_upper = next((g.value for g in grid_results if filter_upper in g.filter_name))
                    leakage_grid = leakage_correction(logger=logger,
                                                      grid_filter_lower=grid_filter_lower,
                                                      grid_filter_upper=grid_filter_upper,
                                                      filter_id=int(num))
                    leakage_grid.grid = leakage_grid.grid * 1000  # unit mm
                    write_tws_nc(logger=logger,
                                 filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                       f'{r.filter_name}_tws.nc'),
                                 tws=tws,
                                 std_tws=tws_std,
                                 leakage_correction=leakage_grid,
                                 reference_epoch=ref_period)
                else:
                    write_tws_nc(logger=logger,
                                 filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                       f'{r.filter_name}_tws.nc'),
                                 tws=tws,
                                 std_tws=tws_std,
                                 reference_epoch=ref_period)

        # Domain: All
        if 'all' in config_dict["optional_settings"]["domain"]:
            logger.info("TWS global calculation")
            for r in grid_results:
                name = r.filter_name or ''
                if name == 'nofilt' or '_leakage' in name:
                    continue
                tws, tws_std = tws_processing(
                    logger=logger, input_ewh_grid=r.value,
                    file_land_ocean_mask=config_dict["optional_settings"]["land_ocean_mask"],
                    domain='all', list_earthquakes=list_earthquakes_filenames,
                    file_climatology=config_dict["optional_settings"]["climatology"]
                )
                l3_results.append(ResultObjects(type='grid', name='tws_global',
                                                value={'tws': tws, 'tws_std': tws_std},
                                                filter_name=r.filter_name, domain='land'))
                pattern = re.compile(r"([VD]DK)([2-5])")
                if config_dict["optional_settings"]["leakage"] and pattern.fullmatch(r.filter_name):
                    m = pattern.fullmatch(r.filter_name)
                    prefix, num = m.groups()
                    filter_lower = f"{prefix}{int(num) - 1}"
                    grid_filter_lower = next((g.value for g in grid_results if filter_lower in g.filter_name))
                    filter_upper = f"{prefix}{int(num) + 1}"
                    grid_filter_upper = next((g.value for g in grid_results if filter_upper in g.filter_name))
                    leakage_grid = leakage_correction(logger=logger,
                                                      grid_filter_lower=grid_filter_lower,
                                                      grid_filter_upper=grid_filter_upper,
                                                      filter_id=int(num))
                    leakage_grid.grid = leakage_grid.grid * 1000  # unit mm
                    write_tws_nc(logger=logger,
                                 filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                       f'tws_global_{r.filter_name}.nc'),
                                 tws=tws,
                                 std_tws=tws_std,
                                 leakage_correction=leakage_grid,
                                 reference_epoch=ref_period)
                else:
                    write_tws_nc(logger=logger,
                                 filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                       f'tws_global_{r.filter_name}.nc'),
                                 tws=tws,
                                 std_tws=tws_std,
                                 reference_epoch=ref_period)

    # -----------------------
    # OBP (Ocean) Processing -> Uses grid_results
    # -----------------------
    if 'ocean' in config_dict["optional_settings"]["domain"]:
        logger.info("OBP calculation")
        list_earthquakes_filenames = get_eq_filenames(config_dict["optional_settings"]["earthquake"])

        for r in grid_results:
            name = r.filter_name or ''
            if name == 'nofilt' or '_leakage' in name:
                continue
            sle, sle_std, residual_obp, residual_obp_std = obp_processing(
                logger=logger, input_ewh_grid=r.value,
                file_land_ocean_mask=config_dict["optional_settings"]["land_ocean_mask"],
                love_numbers=love_numbers, list_earthquakes=list_earthquakes_filenames,
                file_climatology=config_dict["optional_settings"]["climatology"]
            )
            l3_results.append(ResultObjects(type='grid', name='obp',
                                            value={'sle': sle, 'sle_std': sle_std,
                                                   'residual_obp': residual_obp, 'residual_obp_std': residual_obp_std},
                                            filter_name=r.filter_name, domain='ocean'))
            pattern = re.compile(r"([VD]DK)([2-5])")
            if config_dict["optional_settings"]["leakage"] and pattern.fullmatch(r.filter_name):
                m = pattern.fullmatch(r.filter_name)
                prefix, num = m.groups()
                filter_lower = f"{prefix}{int(num) - 1}"
                grid_filter_lower = next((g.value for g in grid_results if filter_lower in g.filter_name))
                filter_upper = f"{prefix}{int(num) + 1}"
                grid_filter_upper = next((g.value for g in grid_results if filter_upper in g.filter_name))
                leakage_grid = leakage_correction(logger=logger,
                                                  grid_filter_lower=grid_filter_lower,
                                                  grid_filter_upper=grid_filter_upper,
                                                  filter_id=int(num))
                # factor m -> hPa
                water_density = get_constant('water_density')
                standard_gravity = get_constant('standard_gravity_acceleration_pole')
                factor = water_density * standard_gravity / 100
                leakage_grid.grid = leakage_grid.grid * factor  # unit hPa
                write_obp_nc(logger=logger,
                             filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                   f'{r.filter_name}_obp.nc'),
                             resobp=residual_obp,
                             std_resobp=residual_obp_std,
                             barslv=sle,
                             std_barslv=sle_std,
                             leakage_correction=leakage_grid,
                             reference_epoch=ref_period)

            else:

                write_obp_nc(logger=logger,
                             filename=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                                   f'{r.filter_name}_obp.nc'),
                             resobp=residual_obp,
                             std_resobp=residual_obp_std,
                             barslv=sle,
                             std_barslv=sle_std,
                             reference_epoch=ref_period)

    # -----------------------
    # Ice Processing -> Uses sh_results (SHC)
    # -----------------------
    if any(domain in ice_domains for domain in config_dict["optional_settings"]["domain"]):
        logger.info("IM calculation")
        try:
            # Must use sh_results, not grid_results, because im_processing needs SHC
            result_nofilt = [r for r in sh_results if r.filter_name == "nofilt"][0]
        except IndexError:
            logger.warning("No 'nofilt' result found. Skipping Ice processing.")
            result_nofilt = None

        if result_nofilt:
            # Greenland
            if any(domain in ["greenland", "ice"] for domain in config_dict["optional_settings"]["domain"]):
                im, im_std = im_processing(
                    shc=result_nofilt.value,
                    file_name_sensitivity_kernel=config_dict["optional_settings"]["sensitivity_kernel_greenland"],
                    love_numbers=love_numbers,
                    logger=logger,
                    file_climatology=config_dict["optional_settings"]["climatology"]
                )
                l3_results.append(ResultObjects(type='grid', name='im_greenland',
                                                value={'im': im, 'im_std': im_std},
                                                filter_name='nofilt', domain='greenland'))
                write_ice_nc(os.path.join(config_dict['mandatory_settings']['output_folder'], 'im_greenland.nc'),
                             dm=im, std_dm=im_std, reference_epoch=ref_period)

            # Antarctica
            if any(domain in ["antarctica", "ice"] for domain in config_dict["optional_settings"]["domain"]):
                im, im_std = im_processing(
                    shc=result_nofilt.value,
                    file_name_sensitivity_kernel=config_dict["optional_settings"]["sensitivity_kernel_antarctica"],
                    love_numbers=love_numbers,
                    logger=logger,
                    file_climatology=config_dict["optional_settings"]["climatology"]
                )
                l3_results.append(ResultObjects(type='grid', name='im_antarctica',
                                                value={'im': im, 'im_std': im_std},
                                                filter_name='nofilt', domain='antarctica'))
                write_ice_nc(os.path.join(config_dict['mandatory_settings']['output_folder'], 'im_antarctica.nc'),
                             dm=im, std_dm=im_std, reference_epoch=ref_period)

    # -----------------------
    # Time Series Computation -> Uses l3_results (Grids from all domains)
    # -----------------------

    # TWS
    region_file = config_dict["optional_settings"]["region_land_geojson"]
    if region_file is not None and any(domain in land_domains for domain in config_dict["optional_settings"]["domain"]):
        logger.info("Compute mean time series for tws.")
        # Find all TWS results (potentially multiple filters)
        tws_objs = [obj for obj in l3_results if "tws" in obj.name]

        if tws_objs:
            # Precompute masks once using the first grid as reference
            regions = read_region_geojson(logger=logger, file_path=region_file)
            ref_tws = tws_objs[0].value["tws"]
            region_masks_tws = precompute_region_masks(logger=logger, grid=ref_tws, regions=regions)

            for tws_res in tws_objs:
                tws = tws_res.value['tws']
                tws_std = tws_res.value['tws_std']
                filter_name = getattr(tws_res, "filter_name", "unknown_filter")

                logger.info(f"Compute regional time series for tws (filter: {filter_name}).")

                mean_ts = compute_regional_timeseries(
                    logger=logger, grid=tws, regions=regions, precomputed_masks=region_masks_tws
                )
                std_ts = compute_regional_timeseries_std(
                    logger=logger, grid=tws_std, regions=regions, precomputed_masks=region_masks_tws
                )

                save_timeseries_to_csv(
                    file_path=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                           f'tws_timeseries_{filter_name}.csv'),
                    means_dict=mean_ts, stds_dict=std_ts, dates=tws.dates
                )

    # OBP
    region_file = config_dict["optional_settings"]["region_ocean_geojson"]
    if region_file is not None and 'ocean' in config_dict["optional_settings"]["domain"]:
        logger.info("Compute mean time series for ocean.")
        obp_objs = [obj for obj in l3_results if "obp" in obj.name]

        if obp_objs:
            # Precompute masks once
            regions = read_region_geojson(logger=logger, file_path=region_file)
            ref_sle = obp_objs[0].value['sle']
            region_masks_ocean = precompute_region_masks(logger=logger, grid=ref_sle, regions=regions)

            for obp_res in obp_objs:
                sle = obp_res.value['sle']
                sle_std = obp_res.value['sle_std']
                residual_obp = obp_res.value['residual_obp']
                residual_obp_std = obp_res.value['residual_obp_std']
                filter_name = getattr(obp_res, "filter_name", "unknown_filter")

                logger.info(f"Compute regional time series for ocean (filter: {filter_name}).")

                mean_ts_sle = compute_regional_timeseries(
                    logger=logger, grid=sle, regions=regions, precomputed_masks=region_masks_ocean
                )
                std_ts_sle = compute_regional_timeseries_std(
                    logger=logger, grid=sle_std, regions=regions, precomputed_masks=region_masks_ocean
                )
                save_timeseries_to_csv(
                    file_path=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                           f'barslv_timeseries_{filter_name}.csv'),
                    means_dict=mean_ts_sle, stds_dict=std_ts_sle, dates=sle.dates
                )

                mean_ts_res = compute_regional_timeseries(
                    logger=logger, grid=residual_obp, regions=regions, precomputed_masks=region_masks_ocean
                )
                std_ts_res = compute_regional_timeseries_std(
                    logger=logger, grid=residual_obp_std, regions=regions, precomputed_masks=region_masks_ocean
                )

                # Use sh_results[0] for fallback dates if needed, or from the grid
                dates_source = sh_results[0].value.dates if hasattr(sh_results[0].value, 'dates') else sle.dates

                save_timeseries_to_csv(
                    file_path=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                           f'resobp_timeseries_{filter_name}.csv'),
                    means_dict=mean_ts_res, stds_dict=std_ts_res, dates=dates_source
                )

    # IM (Greenland/Antarctica)
    if any(domain in ice_domains for domain in config_dict["optional_settings"]["domain"]):

        # Greenland
        if any(domain in ["greenland", "ice"] for domain in config_dict["optional_settings"]["domain"]):
            gis_objs = [obj for obj in l3_results if "greenland" in obj.name]
            if gis_objs:
                logger.info("Compute mean time series for Greenland.")
                gis_results = gis_objs[0]
                basin_dict = read_ice_basins_from_kernel(logger=logger,
                                                         file_path=config_dict["optional_settings"]
                                                         ["sensitivity_kernel_greenland"])
                mean_ts = compute_regional_ice_timeseries(grid=gis_results.value['im'], basin_dict=basin_dict)
                mean_ts_std = compute_regional_ice_timeseries_std(logger=logger, grid=gis_results.value['im_std'],
                                                                  basin_dict=basin_dict)
                save_ice_timeseries_to_csv(
                    file_path=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                           'greenland_timeseries.csv'),
                    mean_dict=mean_ts, std_dict=mean_ts_std, dates=gis_results.value['im'].dates
                )

        # Antarctica
        if any(domain in ["antarctica", "ice"] for domain in config_dict["optional_settings"]["domain"]):
            ais_objs = [obj for obj in l3_results if "antarctica" in obj.name]
            if ais_objs:
                logger.info("Compute mean time series for Antarctica.")
                ais_results = ais_objs[0]
                basin_dict = read_ice_basins_from_kernel(logger=logger,
                                                         file_path=config_dict["optional_settings"]
                                                         ["sensitivity_kernel_antarctica"])
                mean_ts = compute_regional_ice_timeseries(grid=ais_results.value['im'], basin_dict=basin_dict)
                mean_ts_std = compute_regional_ice_timeseries_std(logger=logger, grid=ais_results.value['im_std'],
                                                                  basin_dict=basin_dict)
                save_ice_timeseries_to_csv(
                    file_path=os.path.join(config_dict['mandatory_settings']['output_folder'],
                                           'antarctica_timeseries.csv'),
                    mean_dict=mean_ts, std_dict=mean_ts_std, dates=ais_results.value['im'].dates
                )
