# The MIT License (MIT)
# Copyright (c) 2023 ESA Climate Change Initiative
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from datetime import datetime
from typing import Any, List, Mapping, Tuple, Union

import pandas as pd
from dateutil.relativedelta import relativedelta

from xcube_cci.cciodp import CciOdp
from xcube_cci.constants import MONTHS

from .constants import TIMESTAMP_FORMAT


def extract_time_range_as_strings(
        time_range: Union[Tuple, List]
) -> (str, str):
    if isinstance(time_range, tuple):
        time_start, time_end = time_range
    else:
        time_start = time_range[0]
        time_end = time_range[1]
    return \
        extract_time_as_string(time_start), \
        extract_time_as_string(time_end)


def extract_time_as_string(
        time_value: Union[pd.Timestamp, str]
) -> str:
    if isinstance(time_value, str):
        time_value = pd.to_datetime(time_value, utc=True)
    return time_value.tz_localize(None).isoformat()


class TimeRangeGetter:

    def __init__(self, cci_cdc: CciOdp, metadata: dict):
        self._cci_cdc = cci_cdc
        self._metadata = metadata

    def get_time_ranges(self, dataset_id: str,
                        params: Mapping[str, Any]) -> List[Tuple]:
        start_time, end_time, iso_start_time, iso_end_time = \
            self._extract_time_range_as_datetime(
                params.get('time_range',
                           self.get_default_time_range(dataset_id)))
        ecv = dataset_id.split('.')[1]
        time_period = dataset_id.split('.')[2]
        if time_period == 'day':
            start_time = datetime(year=start_time.year, month=start_time.month,
                                  day=start_time.day)
            end_time = datetime(year=end_time.year, month=end_time.month,
                                day=end_time.day,
                                hour=23, minute=59, second=59)
            delta = relativedelta(days=1)
        elif time_period == 'month' or time_period == 'mon':
            start_time = datetime(year=start_time.year, month=start_time.month,
                                  day=1)
            end_time = datetime(year=end_time.year, month=end_time.month, day=1)
            delta = relativedelta(months=1)
            end_time += delta
        elif time_period == 'year' or time_period == 'yr' and ecv != "BIOMASS":
            start_time = datetime(year=start_time.year, month=1, day=1)
            end_time = datetime(year=end_time.year, month=12, day=31)
            delta = relativedelta(years=1)
            if dataset_id.endswith("yr"):
                num_years = int(dataset_id[-3:-2])
                delta_years = relativedelta(years=num_years) - relativedelta(days=1)
                request_time_ranges = []
                this = start_time
                after = this + delta_years
                while after <= end_time:
                    pd_this = pd.Timestamp(datetime.strftime(this, TIMESTAMP_FORMAT))
                    pd_next = pd.Timestamp(datetime.strftime(after, TIMESTAMP_FORMAT))
                    request_time_ranges.append((pd_this, pd_next))
                    this = this + delta
                    after = after + delta
                return request_time_ranges
        elif time_period == 'climatology':
            return [(i + 1, i + 1) for i, month in enumerate(MONTHS)]
        elif time_period == "5-days" and ecv == "VEGETATION":
            return self.extract_5days_vegetation_time_range(dataset_id, iso_start_time, iso_end_time)
        else:
            end_time = end_time.replace(hour=23, minute=59, second=59)
            end_time_str = datetime.strftime(end_time, TIMESTAMP_FORMAT)
            iso_end_time = extract_time_as_string(end_time_str)
            request_time_ranges = self._cci_cdc.get_time_ranges_from_data(
                dataset_id, iso_start_time, iso_end_time
            )
            return request_time_ranges
        drs_end = dataset_id.split('.')[9]
        if drs_end == "greenland_gmb_mass_trends":
            return self.extract_greenland_gmb_time_series(
                dataset_id, iso_start_time, end_time, delta
            )
        request_time_ranges = []
        this = start_time
        while this < end_time:
            after = this + delta
            pd_this = pd.Timestamp(datetime.strftime(this, TIMESTAMP_FORMAT))
            pd_next = pd.Timestamp(datetime.strftime(after, TIMESTAMP_FORMAT))
            request_time_ranges.append((pd_this, pd_next))
            this = after
        return request_time_ranges

    def extract_5days_vegetation_time_range(self, dataset_id: str, start_time: datetime, end_time: datetime):
        file_time_ranges = self._cci_cdc.get_time_ranges_from_data(dataset_id, start_time, end_time)
        five_days = pd.Timedelta(days=5)
        one_day = pd.Timedelta(days=1, seconds=-1)
        actual_time_ranges = []
        for file_time_range in file_time_ranges:
            start_time = file_time_range[0]
            time = pd.Timestamp(year=start_time.year, month=1, day=1)
            end_time = pd.Timestamp(year=start_time.year, month=12, day=30)
            while time < end_time:
                actual_time_ranges.append((time, time + one_day))
                time += five_days
        return actual_time_ranges

    def extract_greenland_gmb_time_series(
            self, dataset_id: str, iso_start_time: str,
            end_time: datetime, delta: pd.Timedelta
    )  -> List[Tuple]:
        end_time = end_time.replace(hour=23, minute=59, second=59)
        end_time_str = datetime.strftime(end_time, TIMESTAMP_FORMAT)
        iso_end_time = extract_time_as_string(end_time_str)
        request_time_ranges = self._cci_cdc.get_time_ranges_from_data(
            dataset_id, iso_start_time, iso_end_time
        )
        start_time = request_time_ranges[0][0]
        end_time = request_time_ranges[-1][1]
        num_years = end_time.year - start_time.year + 1
        span_delta = relativedelta(
            years=num_years - self._metadata["time_chunking"]
        )
        adjusted_time_ranges = []
        span_start_time = start_time
        span_end_time = start_time + span_delta
        span_end_time = span_end_time.replace(hour=23, minute=59, second=59)
        while span_end_time <= end_time:
            pd_this = pd.Timestamp(datetime.strftime(
                span_start_time, TIMESTAMP_FORMAT)
            )
            pd_next = pd.Timestamp(datetime.strftime(
                span_end_time, TIMESTAMP_FORMAT)
            )
            adjusted_time_ranges.append((pd_this, pd_next))
            span_start_time += delta
            span_end_time += delta
        return adjusted_time_ranges

    @staticmethod
    def _extract_time_range_as_datetime(
            time_range: Union[Tuple, List]
    ) -> (datetime, datetime, str, str):
        iso_start_time, iso_end_time = extract_time_range_as_strings(
            time_range)
        start_time = datetime.strptime(iso_start_time, TIMESTAMP_FORMAT)
        end_time = datetime.strptime(iso_end_time, TIMESTAMP_FORMAT)
        return start_time, end_time, iso_start_time, iso_end_time

    def get_default_time_range(self, ds_id: str):
        temporal_start = self._metadata.get('temporal_coverage_start', None)
        temporal_end = self._metadata.get('temporal_coverage_end', None)
        if not temporal_start or not temporal_end:
            time_ranges = self._cci_cdc.get_time_ranges_from_data(ds_id)
            if not temporal_start:
                if len(time_ranges) == 0:
                    raise ValueError(
                        "Could not determine temporal start of dataset. "
                        "Please use 'time_range' parameter."
                    )
                temporal_start = time_ranges[0][0]
            if not temporal_end:
                if len(time_ranges) == 0:
                    raise ValueError(
                        "Could not determine temporal end of dataset. "
                        "Please use 'time_range' parameter."
                    )
                temporal_end = time_ranges[-1][1]
        return temporal_start, temporal_end
