# Copyright (c) ModelScope Contributors. All rights reserved.
import os
import torch
import torch.distributed as dist
from dataclasses import asdict
from transformers.utils import is_torch_npu_available
from typing import List, Optional, Union

from swift.megatron.arguments import MegatronSftArguments
from swift.megatron.trainers import MegatronEmbeddingTrainer, MegatronRerankerTrainer, MegatronTrainer
from swift.pipelines import SwiftSft
from swift.utils import append_to_jsonl, get_logger, is_last_rank, plot_images

if is_torch_npu_available():
    # Enable Megatron on Ascend NPU
    from mindspeed.megatron_adaptor import repatch

    from swift.model.npu_patcher import patch_mindspeed_te_cp_implementation
else:
    repatch = None
    patch_mindspeed_te_cp_implementation = None

logger = get_logger()


class MegatronSft(SwiftSft):
    args_class = MegatronSftArguments
    args: args_class

    def prepare_trainer(self):
        args = self.args
        if args.task_type == 'embedding':
            return MegatronEmbeddingTrainer(self.args, self.template)
        elif args.task_type in {'reranker', 'generative_reranker'}:
            return MegatronRerankerTrainer(self.args, self.template)
        else:
            return MegatronTrainer(self.args, self.template)

    def _set_seed(self):
        pass

    def __init__(self, args: Optional[Union[List[str], MegatronSftArguments]] = None) -> None:
        self.train_msg = {}
        super(SwiftSft, self).__init__(args)
        args = self.args
        if repatch is not None:
            megatron_args = asdict(self.args)
            if args.attention_backend != 'local':
                # MindSpeed requires passing `use_flash_attn` to Megatron
                # to enable flash attention on Ascend NPU.
                args.use_flash_attn = True
                megatron_args['use_flash_attn'] = True
            patch_mindspeed_te_cp_implementation(megatron_args)
            repatch(megatron_args)
        template_cls = args.template_meta.template_cls
        if args.model_meta.is_multimodal and template_cls and template_cls.use_model:
            kwargs = {'return_dummy_model': True}
        else:
            kwargs = {'load_model': False}
        with torch.device('meta'):
            self.model, self.processor = args.get_model_processor(**kwargs, download_model=args.mcore_model is None)
        self._prepare_template()
        args.save_args(args.output_dir)
        self.template.use_megatron = True

    def run(self):
        args = self.args
        train_dataset, val_dataset = self._prepare_dataset()
        args.init_iters(train_dataset, val_dataset)
        trainer = self.prepare_trainer()
        try:
            trainer.train(train_dataset, val_dataset)
        finally:
            state = trainer.state
            self._handle_trainer_state(trainer, is_last_rank())
            self.train_msg.update({
                'last_model_checkpoint': state.last_model_checkpoint,
                'best_model_checkpoint': state.best_model_checkpoint,
                'best_metric': state.best_metric,
            })
            # Visualization
            if is_last_rank():
                images_dir = os.path.join(args.output_dir, 'images')
                logger.info(f'images_dir: {images_dir}')
                plot_images(images_dir, args.tensorboard_dir)

                jsonl_path = os.path.join(args.output_dir, 'logging.jsonl')
                append_to_jsonl(jsonl_path, self.train_msg, strict=False, write_on_rank='last')
        # Exceptions may cause the process to hang, preventing the exception from being propagated.
        # Therefore, destroy_process_group() should not be placed inside the finally block.
        if dist.is_initialized():
            dist.destroy_process_group()
        return self.train_msg


def megatron_sft_main(args: Optional[Union[List[str], MegatronSftArguments]] = None):
    return MegatronSft(args).main()
