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

from swift.megatron.arguments import MegatronExportArguments
from swift.megatron.convert import test_convert_precision
from swift.megatron.model import get_mcore_model
from swift.megatron.utils import load_mcore_checkpoint, prepare_mcore_model, save_mcore_checkpoint
from swift.pipelines import SwiftPipeline, prepare_model_template
from swift.utils import disable_safe_ddp_context_use_barrier, get_logger, is_master

logger = get_logger()


class MegatronExport(SwiftPipeline):
    args_class = MegatronExportArguments
    args: args_class

    def run(self):
        os.environ['DISABLE_MP_DDP'] = 'true'
        args = self.args
        if args.to_hf:
            self.convert_mcore2hf()
        elif args.to_mcore:
            self.convert_hf2mcore()

    def convert_mcore2hf(self) -> None:
        args = self.args
        args.experts_impl = 'eager'
        download_model = args.model is not None
        _, template = prepare_model_template(args, load_model=False, download_model=download_model)
        self.processor = template.processor
        hf_config = self.processor.model_info.config
        mg_model = get_mcore_model(args, hf_config)[0]
        logger.info('Megatron model created successfully.')
        bridge = mg_model.config.bridge
        if args.mcore_model is not None:
            load_mcore_checkpoint(args, [mg_model], load_arg='mcore_model')
        elif args.model is not None:
            bridge.load_weights([mg_model], args.model_info.model_dir)
        else:
            raise ValueError('Please specify `--mcore_model` or `--model`.')
        if args.adapters or args.mcore_adapter is not None:
            peft_model = prepare_mcore_model(args, mg_model)
            if args.mcore_adapter is not None:
                load_mcore_checkpoint(args, [mg_model], load_arg='mcore_adapter')
            elif args.adapters:
                assert len(args.adapters) == 1, 'Currently only support one adapter'
                bridge.load_weights([mg_model], args.adapters[0], peft_format=True)
            if args.merge_lora:
                logger.info('Merge LoRA...')
                mg_model = peft_model.merge_and_unload()
        logger.info('Converting weights and saving the model...')
        save_peft_format = args.tuner_type == 'lora' and not args.merge_lora
        bridge.save_weights(
            [mg_model],
            args.output_dir,
            peft_format=save_peft_format,
            args=args,
            processor=self.processor,
        )
        if is_master():
            if args.ckpt_dir:
                args_path = os.path.join(args.ckpt_dir, 'args.json')
                shutil.copy(args_path, os.path.join(args.output_dir, 'args.json'))
            else:
                args.save_args(args.output_dir)
        if args.test_convert_precision:
            with disable_safe_ddp_context_use_barrier():
                if save_peft_format:
                    kwargs = {'adapters': [args.output_dir]}
                else:
                    kwargs = {'model': args.output_dir, 'torch_dtype': None, 'adapters': []}
                device_map = args.device_map or 'auto'
                hf_model, template = prepare_model_template(
                    args, device_map=device_map, **kwargs) if is_master() else (None, template)
            test_convert_precision(args, hf_model, mg_model, template, test_convert_dtype=args.test_convert_dtype)
            dist.barrier()

    def convert_hf2mcore(self) -> None:
        args = self.args
        args.experts_impl = 'eager'
        download_model = args.model is not None
        _, template = prepare_model_template(args, load_model=False, download_model=download_model)
        self.processor = template.processor
        hf_config = self.processor.model_info.config
        mg_model = get_mcore_model(args, hf_config)[0]
        logger.info('Megatron model created successfully.')
        bridge = mg_model.config.bridge
        if args.model is not None:
            bridge.load_weights([mg_model], args.model_info.model_dir)
        elif args.mcore_model is not None:
            load_mcore_checkpoint(args, [mg_model], load_arg='mcore_model')
        else:
            raise ValueError('Please specify `--mcore_model` or `--model`.')
        dist.barrier()
        if args.adapters or args.mcore_adapter is not None:
            peft_model = prepare_mcore_model(args, mg_model)
            if args.adapters:
                assert len(args.adapters) == 1, 'Currently only support one adapter'
                bridge.load_weights([mg_model], args.adapters[0], peft_format=True)
            elif args.mcore_adapter is not None:
                load_mcore_checkpoint(args, [mg_model], load_arg='mcore_adapter')
            if args.merge_lora:
                logger.info('Merge LoRA...')
                mg_model = peft_model.merge_and_unload()
        logger.info('Successfully transferred HF model weights to MG model.')
        _test_convert_precision = strtobool(os.getenv('SWIFT_TEST_CONVERT_PRECISION', '0'))
        if not _test_convert_precision:
            args.save_args(args.output_dir)
            logger.info('Saving the model...')
            save_peft_format = args.tuner_type == 'lora' and not args.merge_lora
            save_mcore_checkpoint(args, [mg_model], peft_format=save_peft_format)
        # hf_model does not support loading args.mcore_adapter, so test_convert_precision cannot be performed
        support_convert_precision = args.mcore_adapter is None
        if args.test_convert_precision:
            if support_convert_precision:
                with disable_safe_ddp_context_use_barrier():
                    device_map = args.device_map or 'auto'
                    hf_model, template = prepare_model_template(
                        args, device_map=device_map) if is_master() else (None, template)
                test_convert_precision(args, hf_model, mg_model, template, test_convert_dtype=args.test_convert_dtype)
                dist.barrier()
            else:
                logger.warning('Skip test_convert_precision because `--mcore_adapter` is specified.')


def megatron_export_main(args: Optional[Union[List[str], MegatronExportArguments]] = None):
    return MegatronExport(args).main()
