#!/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
"""Functions for replacing the low degree harmonics (c20, c30) in the geogravL3 package."""
from bisect import bisect_left
from logging import Logger
from typing import List
import datetime as dt

import numpy as np

from ...datamodels.shc import SHObject
import geogravl3.io.readers as rd


def replace_low_degrees(logger: Logger,
                        shc: List[SHObject],
                        filename_replacements: str) -> List[SHObject]:
    """
    Replace the low degree harmonics.

    C20 and C30 (only between 2016/11 and 2017/6) are taken from auxiliary file

    Parameters
    ----------
    logger (Logger): logger object
    shc List[SHObject]: List of spherical harmonic objects.
    filename_replacements (str): path and filename to the file containing the replacements for the low degree harmonics

    Returns
    -------
    List[SHObject]
    """
    dates_replacement, c20, c21, s21, c30 = rd.read_lowdegree_coefficients(file_name=filename_replacements)
    dates_replacement_dt = [_to_datetime(d) for d in dates_replacement]
    dates = np.array([s.date for s in shc])
    dates_dt = [_to_datetime(d) for d in dates]

    shc_replaced = shc.copy()

    for i in range(len(dates)):
        index_dates = _get_nearest_date(logger, dates_dt[i], dates_replacement_dt)
        shc_replaced[i].cnm[2, 0] = c20[index_dates]

        if dt.datetime(2016, 11, 1) <= dates_dt[i] <= dt.datetime(2017, 6, 30):
            shc_replaced[i].cnm[3, 0] = c30[index_dates]

    return shc_replaced


def _get_nearest_date(logger: Logger,
                      d_target: dt.date | dt.datetime,
                      dates: np.ndarray[dt.date] | np.ndarray[dt.datetime]) -> int:
    """
    Return the index of the date closest to `d_target`.

    Raises a warning if multiple dates are equally close.
    """
    target_dt = _to_datetime(d_target)
    dates_dt = [_to_datetime(d) for d in dates]

    idx = bisect_left(dates_dt, target_dt) - 1

    if (dates_dt[idx] - target_dt) > dt.timedelta(days=28):
        message = (f"Closest date {dates_dt[idx]}, given as beginning of observation epoch, is more than 28 days "
                   f"away from target ({target_dt}). Considered date missmatch")
        logger.error(message)
        raise ValueError(message)

    return idx


def _to_datetime(d):
    """Convert date to datetime if necessary."""
    return dt.datetime.combine(d, dt.datetime.min.time()) if (isinstance(d, dt.date)
                                                              and not isinstance(d, dt.datetime)) else d
