# Copyright (c) ModelScope Contributors. All rights reserved.
import json
import torch
from dataclasses import dataclass
from torch import nn
from types import MethodType
from typing import List, Literal, Optional

from swift.utils import get_logger, patch_getattr
from .utils import SwiftAdapter, SwiftConfig, SwiftOutput

logger = get_logger()


@dataclass
class ReftConfig(SwiftConfig):
    """
    Train a model with Reft.
    Paper: https://arxiv.org/pdf/2404.03592

    Args:
        model_type(`Optional[str]`): The model_type to find down_proj/layers.
        layer_key(`Optional[str]`): Manually specify the layer key, for example `language_model.layers`.
        layers (`Optional[List[int]]`): The layer number to inject.
        r(`int`): The rank of Reft.
        intervention_type (`Literal['NoreftIntervention', 'LoreftIntervention',
                        'ConsreftIntervention', 'LobireftIntervention',
                        'DireftIntervention', 'NodireftIntervention']`): The intervention type,
                        default LoreftIntervention
        args (`Optional[str]`): Other reft_args in json-string format
    """

    model_type: Optional[str] = None
    layer_key: Optional[str] = None
    layers: Optional[List[int]] = None
    r: int = 4
    intervention_type: Literal['NoreftIntervention', 'LoreftIntervention', 'ConsreftIntervention',
                               'LobireftIntervention', 'DireftIntervention',
                               'NodireftIntervention'] = 'LoreftIntervention'
    args: Optional[str] = None

    def __post_init__(self):
        from .mapping import SwiftTuners
        self.swift_type = SwiftTuners.REFT
        if self.args:
            self.args = json.loads(self.args)
        else:
            self.args = {}


class Reft(SwiftAdapter):

    @staticmethod
    def prepare_model(model: nn.Module, config: ReftConfig, adapter_name: str):
        from swift.utils.import_utils import is_pyreft_available
        if not is_pyreft_available():
            raise ImportError('Please install pyreft before using ReFT: '
                              '`pip install pyreft`')

        import pyreft
        from pyreft import (ConsreftIntervention, DireftIntervention, LobireftIntervention, LoreftIntervention,
                            NodireftIntervention, NoreftIntervention, ReftModel)
        from pyreft.interventions import LowRankRotateLayer

        intervention_mapping = {
            'NoreftIntervention': NoreftIntervention,
            'LoreftIntervention': LoreftIntervention,
            'ConsreftIntervention': ConsreftIntervention,
            'LobireftIntervention': LobireftIntervention,
            'DireftIntervention': DireftIntervention,
            'NodireftIntervention': NodireftIntervention,
        }

        patch_getattr(ReftModel, 'model')

        def forward(self, x):
            self.to(x.device)
            return self.forward_origin(x)

        def forward2(self, base, source=None, subspaces=None):
            self.to(base.device)
            return self.forward_origin(base, source, subspaces)

        if not hasattr(LowRankRotateLayer, 'forward_origin'):
            LowRankRotateLayer.forward_origin = LowRankRotateLayer.forward
            LowRankRotateLayer.forward = forward
            NoreftIntervention.forward_origin = NoreftIntervention.forward
            NoreftIntervention.forward = forward2
            LoreftIntervention.forward_origin = LoreftIntervention.forward
            LoreftIntervention.forward = forward2
            ConsreftIntervention.forward_origin = ConsreftIntervention.forward
            ConsreftIntervention.forward = forward2
            LobireftIntervention.forward_origin = LobireftIntervention.forward
            LobireftIntervention.forward = forward2
            DireftIntervention.forward_origin = DireftIntervention.forward
            DireftIntervention.forward = forward2
            NodireftIntervention.forward_origin = NodireftIntervention.forward
            NodireftIntervention.forward = forward2

        module_list_key = config.layer_key
        if module_list_key is None:
            model_key_mapping = Reft.get_model_key_mapping(config.model_type, config)
            module_list_key = model_key_mapping.module_list
        logger.info(f'Applying Reft to module: {module_list_key}')
        module_list: nn.ModuleList = model.get_submodule(module_list_key)
        representations = []
        for idx, layer in enumerate(module_list):
            if config.layers and idx not in config.layers:
                continue
            intervention_config = {
                'layer':
                idx,
                'component':
                module_list_key + f'[{idx}].output',
                'low_rank_dimension':
                config.r,
                'intervention':
                intervention_mapping[config.intervention_type](
                    embed_dim=model.config.hidden_size, low_rank_dimension=config.r, **config.args)
            }
            representations.append(intervention_config)

        reft_config = pyreft.ReftConfig(representations=representations)
        reft_model = pyreft.get_reft_model(model, reft_config, set_device=False)
        reft_model.reft_config = reft_model.config
        reft_model.config = reft_model.model.config

        def _pre_forward_hook(module, args, kwargs):
            if 'base' in kwargs:
                return args, kwargs

            if 'input_ids' not in kwargs:
                raise ValueError('Input does not contain `input_ids`, maybe the model does not support ReFT.')
            # run intervened forward pass
            unit_locations = None
            if 'intervention_locations' in kwargs:
                if kwargs['intervention_locations'].dim() == 3:
                    unit_locations = {
                        'sources->base': (None, kwargs['intervention_locations'].permute(1, 0, 2).tolist())
                    }
                else:
                    # this is dummy for lora only baseline
                    unit_locations = {'sources->base': (None, 0)}
            kwargs = {
                'base': {
                    'input_ids': kwargs['input_ids'],
                    'attention_mask': kwargs['attention_mask']
                },
                'unit_locations': unit_locations,
                'labels': kwargs['labels'],
                'subspaces': kwargs['subspaces'].permute(1, 0, 2).tolist() if 'subspaces' in kwargs else None
            }
            return args, kwargs

        def _post_forward_hook(module, args, kwargs, outputs):
            return outputs[1]

        def _generate(self, **kwargs):
            # run intervened forward pass
            unit_locations = None
            if 'intervention_locations' in kwargs:
                if kwargs['intervention_locations'].dim() == 3:
                    unit_locations = {
                        'sources->base': (None, kwargs['intervention_locations'].permute(1, 0, 2).tolist())
                    }
                else:
                    # this is dummy for lora only baseline
                    unit_locations = {'sources->base': (None, 0)}

            _kwargs = {
                'base': {
                    'input_ids': kwargs.pop('input_ids'),
                    'attention_mask': kwargs.pop('attention_mask')
                },
                'unit_locations': unit_locations,
                'subspaces': kwargs.pop('subspaces').permute(1, 0, 2).tolist() if 'subspaces' in kwargs else None
            }
            _kwargs = {**_kwargs, **kwargs}
            return self.generate_origin(**_kwargs)[1]

        reft_model.generate_origin = reft_model.generate
        reft_model.generate = MethodType(_generate, reft_model)
        reft_model.register_forward_pre_hook(_pre_forward_hook, with_kwargs=True)
        reft_model.register_forward_hook(_post_forward_hook, with_kwargs=True)

        def save_callback(swift_model, model_dir, adapter_name):
            reft_model.save_intervention(save_directory=model_dir, include_model=False)

        def mark_trainable_callback(model):
            return

        def load_callback(swift_model, model_dir, adapter_name):
            reft_model.load_intervention(model_dir, include_model=False)

        return SwiftOutput(
            model=reft_model,
            config=config,
            mark_trainable_callback=mark_trainable_callback,
            save_callback=save_callback,
            load_callback=load_callback)

    @staticmethod
    def has_additional_modules():
        return True

    @staticmethod
    def activate_adapter(module: torch.nn.Module, adapter_name: str, activate: bool, offload: str = None):
        assert activate, 'ReFT does not support deactivate'
