import copy
import datetime
import decimal
import functools
import json
import warnings
import xml.etree.ElementTree as ET
from functools import reduce
from io import BytesIO
from sys import getsizeof

str_default_date = "%Y-%m-%dT%H:%M:%S"


class ReachDateUtils:
    @staticmethod
    def chunk_date_range(start_date, end_date, days_per_chunk):
        """
        Divides the interval between start_date and end_date into blocks of days_to_request days.
        Returns a list of tuples, each representing a date range.
        Each new range starts the day after the end of the previous range.
        """
        ranges = []
        current_start_date = start_date

        while current_start_date < end_date:
            current_end_date = current_start_date + datetime.timedelta(
                days=days_per_chunk - 1
            )

            # Ensure that the final interval does not exceed end_date
            if current_end_date > end_date:
                current_end_date = end_date
            ranges.append((current_start_date, current_end_date))

            # Updating current_start_date to the day after current_end_date
            current_start_date = current_end_date + datetime.timedelta(days=1)

        return ranges

    @staticmethod
    def date_to_str(date: datetime, strf_format: str = None) -> str:
        return date.strftime(strf_format or str_default_date)

    @staticmethod
    def str_to_date(date: str, strf_format: str = None) -> datetime:
        return datetime.datetime.strptime(date, strf_format or str_default_date)


class ReachChunkObjectsUtils:
    MAX_FIREHOSE_SIZE = 900000

    @staticmethod
    def set_dict_value(obj, path, value):
        """

        :param obj: root object
        :param path:
        :param value:
        :return:
        """
        *path, last = path.split(".")
        for bit in path:
            obj = obj.setdefault(bit, {})
        obj[last] = value

    @staticmethod
    def chunk_list_items(
        obj_to_split: dict, path_of_array: str, max_size=MAX_FIREHOSE_SIZE
    ) -> list:
        """
        Function created to split a dict into smaller dict's, keeping the properties default.
        :param obj_to_split: root object to split.
        :param path_of_array: array path, separated by dots.
        :param max_size: maximum size in bytes to each
        :return: list
        """
        if path_of_array:
            map_list = path_of_array.split(".")
            edges = reduce(
                lambda d, key: d.get(key) if isinstance(d, dict) else None,
                map_list,
                obj_to_split,
            )
            if not edges:
                edges = [obj_to_split]
                path_of_array = None
        elif isinstance(obj_to_split, list):
            edges = obj_to_split
        else:
            raise Exception("path_of_array cannot be None for dict")
        list_of_items = []
        current_items = []
        for elem in edges:
            current_items.append(elem)

            if len(json.dumps(current_items).encode("utf-8")) >= max_size:
                current_edge = copy.deepcopy(obj_to_split)
                if path_of_array:
                    ReachChunkObjectsUtils.set_dict_value(
                        current_edge, path_of_array, current_items
                    )
                list_of_items.append(current_edge)
                current_items = []
        if current_items:
            current_edge = copy.deepcopy(obj_to_split)
            if path_of_array:
                ReachChunkObjectsUtils.set_dict_value(
                    current_edge, path_of_array, current_items
                )
            list_of_items.append(current_edge)
        return list_of_items

    @staticmethod
    def chunk_xml_by_tag(content: str, header: str, footer: str, tag_name):
        context = ET.iterparse(BytesIO(content.encode("utf-8")))
        list_xml = []

        for event, elem in context:
            if elem.tag in tag_name:
                list_xml.append(
                    f"{header}{ET.tostring(elem, encoding='unicode')}{footer}"
                )
        return list_xml

    @staticmethod
    def chunk_xml(content: str, header: str, footer: str, tag_name, max_size):
        context = ET.iterparse(BytesIO(content.encode("utf-8")))
        list_xml = []

        current_xml: str = ""
        for event, elem in context:
            if elem.tag in tag_name:
                current_xml += ET.tostring(elem, encoding="unicode")
            if getsizeof(current_xml) >= max_size:
                list_xml.append(f"{header}{current_xml}{footer}")
                current_xml = ""
        if not list_xml:
            list_xml.append(f"{header}{current_xml}{footer}")
            current_xml = ""
        if current_xml:
            list_xml.append(f"{header}{current_xml}{footer}")
        return list_xml

    @staticmethod
    def split_list_into_chunks(items, chunk_size):
        list_of_chunks = []
        for i in range(0, len(items), chunk_size):
            list_of_chunks.append(items[i : i + chunk_size])
        return list_of_chunks


def deprecated(reason):
    def decorator(func):
        @functools.wraps(func)
        def new_func(*args, **kwargs):
            warnings.simplefilter("always", DeprecationWarning)
            warnings.warn(
                f"{func.__name__} está obsoleta: {reason}",
                category=DeprecationWarning,
                stacklevel=2,
            )
            warnings.simplefilter("default", DeprecationWarning)
            return func(*args, **kwargs)

        return new_func

    return decorator


def remove_nulls(data):
    if isinstance(data, dict):
        return {k: remove_nulls(v) for k, v in data.items() if v is not None}
    elif isinstance(data, list):
        return [remove_nulls(item) for item in data if item is not None]
    else:
        return data


def build_update_params_for_attributes(attributes_to_update):
    """
    Constructs the parameters needed for an update operation in DynamoDB, specifically
    for the provided attributes. It handles reserved words by using attribute name aliases.

    :param attributes_to_update: A dictionary containing the attribute names and their new values.
    :return: A tuple containing the UpdateExpression, ExpressionAttributeValues, and
             ExpressionAttributeNames for the update operation.
    """
    update_expression = "SET " + ", ".join(
        f"#{k} = :val{k}" for k in attributes_to_update.keys()
    )

    expression_attribute_values = {
        f":val{k}": v for k, v in attributes_to_update.items()
    }

    expression_attribute_names = {f"#{k}": k for k in attributes_to_update.keys()}

    return update_expression, expression_attribute_values, expression_attribute_names


class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return str(o)
        return super().default(o)


def is_valid_email(v):
    import re

    regex = r"^\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
    return re.match(regex, v)
