# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import atexit
import copy
import multiprocessing
import os
import time

import paddle

from paddlenlp.utils.log import logger


def _save_optimizer(obj, name_mapping, path, saved_signal_path, protocol):
    start_time = time.time()
    for k, v in obj.items():
        if k == "master_weights" and isinstance(v, dict):
            for kk, vv in v.items():
                if isinstance(vv, paddle.Tensor):
                    vv.name = name_mapping["master_weights"][kk]
        else:
            if k in name_mapping and isinstance(v, paddle.Tensor):
                v.name = name_mapping[k]
    paddle.save(obj, path, protocol)
    # dump saved_signal
    with open(saved_signal_path, mode="w+") as f:
        f.write("1")
        f.flush()
        os.fsync(f.fileno())
    end_time = time.time()
    elapsed_time = end_time - start_time
    logger.info(f"Async save optimizer took {elapsed_time:.6f} seconds to execute.")


class AsyncSaver:
    def __init__(self):
        self.context = multiprocessing.get_context("spawn")
        self.cpu_optimizer_state_dict = {}
        self.pool = self.context.Pool(1)
        self.result = None
        self.name_mapping = None

        atexit.register(self.shutdown)

    def run(self, optimizer_state_dict, path, saved_signal_path, protocol=4):
        logger.info(f"Started saving optimizer_state_dict to {os.path.abspath(path)}.")
        self._wait_for_previous_result()

        self._reset_state(path, saved_signal_path, protocol)
        self._process_optimizer_state_dict(optimizer_state_dict)

        self.result = self.pool.apply_async(
            _save_optimizer,
            args=(self.cpu_optimizer_state_dict, self.name_mapping, self.path, self.saved_signal_path, self.protocol),
        )

        logger.info("Finished launching saving optimizer_state_dict process")

    def _wait_for_previous_result(self):
        if self.result is not None:
            max_retries = 5
            for retries in range(max_retries):
                try:
                    self.result.get()
                    break
                except Exception as e:
                    if retries == max_retries - 1:
                        raise RuntimeError(f"Failed after {max_retries} retries during async save.")

                    time.sleep(1 + retries * 2)
                    logger.warning(f"An error occurred during async save: {e}. Retrying...")
                    self.result = self.pool.apply_async(
                        _save_optimizer,
                        args=(
                            self.cpu_optimizer_state_dict,
                            self.name_mapping,
                            self.path,
                            self.saved_signal_path,
                            self.protocol,
                        ),
                    )

            if self.result.ready() and not self.result.successful():
                raise RuntimeError("The previous async save task failed.")
        else:
            pass

    def _reset_state(self, path, saved_signal_path, protocol):
        self.cpu_optimizer_state_dict.clear()
        self.name_mapping = {"master_weights": {}}
        self.path = path
        self.saved_signal_path = saved_signal_path
        self.protocol = protocol

    def _process_optimizer_state_dict(self, optimizer_state_dict):
        for k, v in optimizer_state_dict.items():
            if k == "master_weights":
                self.cpu_optimizer_state_dict[k] = {}
                for kk, vv in v.items():
                    self.cpu_optimizer_state_dict[k][kk] = vv.pin_memory()
                    self.name_mapping[k][kk] = vv.name
            elif k == "LR_Scheduler":
                self.cpu_optimizer_state_dict[k] = copy.deepcopy(v)
            else:
                self.cpu_optimizer_state_dict[k] = v.pin_memory()
                self.name_mapping[k] = v.name
            paddle.device.synchronize()

    def shutdown(self):
        self.pool.close()
        self.pool.join()

    def __del__(self):
        self.shutdown()
