# 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.

from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union

import numpy as np
import paddle
import paddle.distributed as dist
import paddle.nn as nn
import tqdm
from paddle.distributed.fleet.utils import recompute

from ...utils.log import logger
from .. import AutoTokenizer, MistralModel, PretrainedConfig, PretrainedModel
from ..model_outputs import BaseModelOutputWithPast, ModelOutput

__all__ = ["NVEncodeModel"]


@dataclass
class EncoderOutput(ModelOutput):
    q_reps: Optional[paddle.Tensor] = None
    p_reps: Optional[paddle.Tensor] = None
    loss: Optional[paddle.Tensor] = None
    scores: Optional[paddle.Tensor] = None


def scaled_dot_product_attention(q, k, v):  # [bs, len, num_heads, dim]
    matmul_qk = paddle.matmul(q.transpose([0, 2, 1, 3]), k.transpose([0, 2, 3, 1]))
    dk = paddle.to_tensor(k.shape[-1], dtype=paddle.float32)
    scaled_attention_logits = matmul_qk / paddle.sqrt(dk)
    attention_weights = paddle.nn.functional.softmax(scaled_attention_logits, axis=-1)  # [bs, num_heads, q_len, k_len]
    output = paddle.matmul(attention_weights, v.transpose([0, 2, 1, 3]))  # [bs, num_heads, q_len, dim]
    output = output.transpose([0, 2, 1, 3])  # [bs, q_len, num_heads, dim]
    return output


def _make_bidirection_mask(
    input_ids_shape: paddle.shape,
    dtype: paddle.dtype,
    past_key_values_length: int = 0,
):
    """
    Make bidirection mask used for sliding window attention
    """
    bsz, tgt_len = input_ids_shape

    tensor = paddle.full(
        (tgt_len, tgt_len),
        fill_value=1,
    )
    mask = paddle.tril(tensor, diagonal=0)
    mask = paddle.ones_like(mask)  # here is for bidirection attention
    mask = paddle.log(mask).astype(dtype)

    if past_key_values_length > 0:
        mask = paddle.concat([paddle.zeros([tgt_len, past_key_values_length], dtype=dtype), mask], axis=-1)
    return mask[None, None, :, :].expand([bsz, 1, tgt_len, tgt_len + past_key_values_length])


def _expand_mask(mask: paddle.Tensor, dtype: paddle.dtype, tgt_len):
    expanded_mask = mask
    if len(mask.shape) == 2:
        """
        Expands attention_mask from `[bsz, seq_len]` to `[bsz, 1, tgt_seq_len, src_seq_len]`.
        """
        bsz, src_len = mask.shape
        tgt_len = tgt_len if tgt_len is not None else src_len

        expanded_mask = mask[:, None, None, :].expand([bsz, 1, tgt_len, src_len]).astype(dtype)
    elif len(mask.shape) == 3:
        """
        Expands attention_mask from `[bsz, tgt_seq_len, seq_len]` to `[bsz, 1, tgt_seq_len, src_seq_len]`.
        """
        expanded_mask = mask.unsqueeze(1).astype(dtype)

    inverted_mask = 1.0 - expanded_mask

    return paddle.where(inverted_mask > 0.5, paddle.full_like(inverted_mask, paddle.finfo(dtype).min), inverted_mask)


class LatentModel(PretrainedModel):
    config_class = PretrainedConfig

    def __init__(self, config):
        super().__init__(config)

        self.cross_attend_blocks_0_fn_to_kv = paddle.nn.Linear(
            in_features=config.hidden_size, out_features=2 * config.max_position_embeddings, bias_attr=False
        )
        self.cross_attend_blocks_0_fn_to_out = paddle.nn.Linear(
            in_features=config.max_position_embeddings, out_features=config.hidden_size, bias_attr=False
        )
        self.cross_attend_blocks_0_fn_to_q = paddle.nn.Linear(
            in_features=config.hidden_size, out_features=config.max_position_embeddings, bias_attr=False
        )
        self.cross_attend_blocks_0_norm = paddle.nn.LayerNorm(config.hidden_size)
        self.cross_attend_blocks_0_norm_context = paddle.nn.LayerNorm(config.hidden_size)

        self.cross_attend_blocks_1_fn_net_0 = paddle.nn.Linear(
            in_features=config.hidden_size, out_features=config.max_position_embeddings
        )
        self.cross_attend_blocks_1_fn_net_2 = paddle.nn.Linear(
            in_features=config.max_position_embeddings // 2, out_features=config.hidden_size
        )
        self.cross_attend_blocks_1_norm = paddle.nn.LayerNorm(config.hidden_size)

        self.latents = paddle.nn.Linear(in_features=config.hidden_size, out_features=512, bias_attr=False)

    def forward(self, last_hidden_states, pool_mask):
        one = paddle.eye(
            num_rows=self.config.hidden_size,
            num_columns=self.config.hidden_size,
            dtype=self.latents.weight.dtype,
        )
        self_latents_weight_T = self.latents(one).T
        # latents = repeat(self_latents_weight_T, "d h -> b d h", b=last_hidden_states.shape[0]) # from einops import repeat
        latents = paddle.tile(self_latents_weight_T, repeat_times=last_hidden_states.shape[0]).reshape(
            [self_latents_weight_T.shape[0], last_hidden_states.shape[0], self_latents_weight_T.shape[1]]
        )
        latents = latents.transpose([1, 0, 2])

        normed_x = self.cross_attend_blocks_0_norm(last_hidden_states)
        normed_context = self.cross_attend_blocks_0_norm_context(latents)

        q = self.cross_attend_blocks_0_fn_to_q(normed_x)
        kv = self.cross_attend_blocks_0_fn_to_kv(normed_context)
        k = kv[:, :, : self.config.max_position_embeddings]
        v = kv[:, :, self.config.max_position_embeddings :]

        # q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b n h d", h=self.config.num_key_value_heads), (q, k, v)) # from einops import rearrange
        q = q.reshape(
            [q.shape[0], q.shape[1], self.config.num_key_value_heads, q.shape[2] // self.config.num_key_value_heads]
        )
        k = k.reshape(
            [k.shape[0], k.shape[1], self.config.num_key_value_heads, k.shape[2] // self.config.num_key_value_heads]
        )
        v = v.reshape(
            [v.shape[0], v.shape[1], self.config.num_key_value_heads, v.shape[2] // self.config.num_key_value_heads]
        )

        # k.stop_gradient = False
        # v.stop_gradient = False
        # out = paddle.nn.functional.scaled_dot_product_attention(q, k, v) # if use this, must set k and v stop_gradient to False
        out = scaled_dot_product_attention(q, k, v)  # if use this, no need to manually set k and v
        # out = rearrange(out, "b n h d -> b n (h d)", h=self.config.num_key_value_heads) # from einops import rearrange
        out = out.reshape([out.shape[0], out.shape[1], out.shape[2] * out.shape[3]])

        out_of_layer1 = self.cross_attend_blocks_0_fn_to_out(out) + last_hidden_states

        normed_x = self.cross_attend_blocks_1_norm(out_of_layer1)

        before_geglu = self.cross_attend_blocks_1_fn_net_0(normed_x)

        x_in_gegle = before_geglu[:, :, : self.config.max_position_embeddings // 2]
        gate_in_geglu = before_geglu[:, :, self.config.max_position_embeddings // 2 :]
        x_after_geglu = x_in_gegle * paddle.nn.functional.gelu(gate_in_geglu)

        after_geglu = self.cross_attend_blocks_1_fn_net_2(x_after_geglu)

        out_of_layer2 = after_geglu + out_of_layer1

        pool_mask = pool_mask.astype(out_of_layer2.dtype)
        s = paddle.sum(
            out_of_layer2 * pool_mask.unsqueeze(-1),
            axis=1,
            dtype=str(self.cross_attend_blocks_1_fn_net_2.weight.dtype).split(".")[-1],
        )
        d = paddle.sum(
            pool_mask, axis=1, keepdim=True, dtype=str(self.cross_attend_blocks_1_fn_net_2.weight.dtype).split(".")[-1]
        )
        hiddens = s / d
        hiddens = paddle.nn.functional.normalize(hiddens, p=2, axis=-1)

        return hiddens


class NVEncodeModel(MistralModel):
    def __init__(
        self,
        config,
        tokenizer_path,
        query_instruction,
        document_instruction,
        eval_batch_size=999,
        normalized=True,
        negatives_cross_device=False,
        temperature_=1,
        margin=0.01,
        use_inbatch_neg=True,
        matryoshka_dims=None,
        matryoshka_loss_weights=None,
        max_seq_length=4096,
    ):
        super().__init__(config)  # get mistral model structure

        self.latent_model = LatentModel(config=config)  # get latent model structure

        self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, padding_side="right")
        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token

        self.query_instruction = query_instruction
        self.document_instruction = document_instruction

        self.eval_batch_size = eval_batch_size

        self.normalized = normalized
        self.negatives_cross_device = negatives_cross_device
        if self.negatives_cross_device:
            if not dist.is_initialized():
                raise ValueError("Distributed training has not been initialized for representation all gather.")
            self.process_rank = dist.get_rank()
            self.world_size = dist.get_world_size()
        self.temperature = temperature_
        self.margin = margin
        self.use_inbatch_neg = use_inbatch_neg
        self.matryoshka_dims = matryoshka_dims
        self.matryoshka_loss_weights = matryoshka_loss_weights

        self.max_seq_length = max_seq_length

        self.cross_entropy = nn.CrossEntropyLoss(reduction="mean")

    def _prepare_decoder_attention_mask(self, attention_mask, input_shape, inputs_embeds, past_key_values_length):

        combined_attention_mask = _make_bidirection_mask(
            input_shape,
            inputs_embeds.dtype,
            past_key_values_length=past_key_values_length,
        )

        if attention_mask is not None:
            # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len]
            expanded_attn_mask = _expand_mask(attention_mask, inputs_embeds.dtype, tgt_len=input_shape[-1])
            combined_attention_mask = (
                expanded_attn_mask if combined_attention_mask is None else expanded_attn_mask + combined_attention_mask
            )

        return combined_attention_mask

    def get_model_config(
        self,
    ):
        return self.model_config.to_dict()

    def encode(self, features, instruction_len):
        last_hidden_states = self.m_forward(**features)[0]  # get bs*len*4096
        pool_mask = features["attention_mask"]
        pool_mask[:, :instruction_len] = 0
        embeddings = self.latent_model.forward(last_hidden_states, pool_mask)
        embeddings = paddle.nn.functional.normalize(embeddings, p=2, axis=1)
        return embeddings

    def compute_similarity(self, q_reps, p_reps):
        # q_reps [batch_size, embedding_dim]
        # p_reps [batch_size, embedding_dim]
        return paddle.matmul(q_reps, p_reps.transpose([1, 0]))

    def hard_negative_loss(self, q_reps, p_reps):
        scores = self.compute_similarity(q_reps, p_reps)
        scores = scores / self.temperature
        scores = scores.reshape([q_reps.shape[0], -1])

        target = paddle.arange(scores.shape[0], dtype="int64")
        target = target * (p_reps.shape[0] // q_reps.shape[0])
        loss = self.compute_loss(scores, target)
        return scores, loss

    def in_batch_negative_loss(self, q_reps, p_reps):
        # In batch negatives
        scores = self.compute_similarity(q_reps, p_reps)
        # Subtract margin from all positive samples cosine_sim()
        margin_diag = paddle.full(shape=[q_reps.shape[0]], fill_value=self.margin, dtype=q_reps.dtype)
        scores = scores - paddle.diag(margin_diag)
        # Scale cosine to ease training converge
        scores = scores / self.temperature
        target = paddle.arange(0, q_reps.shape[0], dtype="int64")
        loss = self.compute_loss(scores, target)
        return scores, loss

    def forward(
        self,
        query: Dict[str, paddle.Tensor] = None,
        passage: Dict[str, paddle.Tensor] = None,
        teacher_score: paddle.Tensor = None,
    ):
        instruction_len = len(self.tokenizer.encode(self.query_instruction, add_special_tokens=False)["input_ids"])
        q_reps = self.encode(query, instruction_len)
        instruction_len = len(self.tokenizer.encode(self.document_instruction, add_special_tokens=False)["input_ids"])
        p_reps = self.encode(passage, instruction_len)

        # For non-matryoshka loss, we normalize the representations
        if not self.matryoshka_dims:
            if self.normalized:
                q_reps = paddle.nn.functional.normalize(q_reps, axis=-1)
                p_reps = paddle.nn.functional.normalize(p_reps, axis=-1)

        if self.training:
            # Cross device negatives
            if self.negatives_cross_device:
                q_reps = self._dist_gather_tensor(q_reps)
                p_reps = self._dist_gather_tensor(p_reps)

            if self.matryoshka_dims:
                loss = 0.0
                scores = 0.0
                for loss_weight, dim in zip(self.matryoshka_loss_weights, self.matryoshka_dims):
                    reduced_q = q_reps[:, :dim]
                    reduced_d = p_reps[:, :dim]
                    if self.normalized:
                        reduced_q = paddle.nn.functional.normalize(reduced_q, axis=-1)
                        reduced_d = paddle.nn.functional.normalize(reduced_d, axis=-1)

                    if self.use_inbatch_neg:
                        dim_score, dim_loss = self.in_batch_negative_loss(reduced_q, reduced_d)
                    else:
                        dim_score, dim_loss = self.hard_negative_loss(reduced_q, reduced_d)
                    scores += dim_score
                    loss += loss_weight * dim_loss

            elif self.use_inbatch_neg:
                scores, loss = self.in_batch_negative_loss(q_reps, p_reps)
            else:
                scores, loss = self.hard_negative_loss(q_reps, p_reps)

        else:
            scores = self.compute_similarity(q_reps, p_reps)
            loss = None
        return EncoderOutput(
            loss=loss,
            scores=scores,
            q_reps=q_reps,
            p_reps=p_reps,
        )

    def compute_loss(self, scores, target):
        return self.cross_entropy(scores, target)

    def _dist_gather_tensor(self, t: Optional[paddle.Tensor]):
        if t is None:
            return None

        all_tensors = [paddle.empty_like(t) for _ in range(self.world_size)]
        dist.all_gather(all_tensors, t)

        all_tensors[self.process_rank] = t
        all_tensors = paddle.concat(all_tensors, axis=0)

        return all_tensors

    def save_pretrained(self, output_dir: str, **kwargs):
        state_dict = self.model.state_dict()
        state_dict = type(state_dict)({k: v.clone().cpu() for k, v in state_dict.items()})
        self.model.save_pretrained(output_dir, state_dict=state_dict)

    def m_forward(
        self,
        input_ids: paddle.Tensor = None,
        attention_mask: Optional[paddle.Tensor] = None,
        position_ids: Optional[paddle.Tensor] = None,
        past_key_values: Optional[List[paddle.Tensor]] = None,
        inputs_embeds: Optional[paddle.Tensor] = None,
        use_cache: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ) -> Union[Tuple, BaseModelOutputWithPast]:
        output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
        output_hidden_states = (
            output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
        )
        use_cache = use_cache if use_cache is not None else self.config.use_cache

        return_dict = return_dict if return_dict is not None else self.config.use_return_dict

        # retrieve input_ids and inputs_embeds
        if input_ids is not None and inputs_embeds is not None:
            raise ValueError("You cannot specify both decoder_input_ids and decoder_inputs_embeds at the same time")
        elif input_ids is not None:
            batch_size, seq_length = input_ids.shape
        elif inputs_embeds is not None:
            batch_size, seq_length, _ = inputs_embeds.shape
        else:
            raise ValueError("You have to specify either decoder_input_ids or decoder_inputs_embeds")

        seq_length_with_past = seq_length
        past_key_values_length = 0

        if past_key_values is not None:
            past_key_values_length = past_key_values[0][0].shape[2]
            seq_length_with_past = seq_length_with_past + past_key_values_length

        if position_ids is None:
            position_ids = paddle.arange(
                past_key_values_length, seq_length + past_key_values_length, dtype=paddle.int64
            )
            position_ids = position_ids.unsqueeze(0).expand((batch_size, seq_length))
        else:
            position_ids = position_ids.reshape([-1, seq_length]).astype("int64")

        if inputs_embeds is None:
            inputs_embeds = self.embed_tokens(input_ids)

        attention_mask = self._prepare_decoder_attention_mask(
            attention_mask,
            (batch_size, seq_length),
            inputs_embeds,
            past_key_values_length,
        )

        hidden_states = inputs_embeds

        if self.enable_recompute and self.training:
            if use_cache:
                logger.warning_once(
                    "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`..."
                )
                use_cache = False

        # decoder layers
        all_hidden_states = () if output_hidden_states else None
        all_self_attns = () if output_attentions else None
        next_decoder_cache = () if use_cache else None

        for idx, decoder_layer in enumerate(self.layers):
            if output_hidden_states:
                all_hidden_states += (hidden_states,)

            past_key_value = past_key_values[idx] if past_key_values is not None else None

            has_gradient = not hidden_states.stop_gradient
            if self.enable_recompute and has_gradient:

                def create_custom_forward(module):
                    def custom_forward(*inputs):
                        # None for past_key_value
                        return module(*inputs, past_key_value, output_attentions)

                    return custom_forward

                layer_outputs = recompute(
                    create_custom_forward(decoder_layer),
                    hidden_states,
                    attention_mask,
                    position_ids,
                )
            else:
                layer_outputs = decoder_layer(
                    hidden_states,
                    attention_mask=attention_mask,
                    position_ids=position_ids,
                    past_key_value=past_key_value,
                    output_attentions=output_attentions,
                    use_cache=use_cache,
                )

            hidden_states = layer_outputs[0]

            if use_cache:
                next_decoder_cache += (layer_outputs[2 if output_attentions else 1],)

            if output_attentions:
                all_self_attns += (layer_outputs[1],)

        hidden_states = self.norm(hidden_states)

        # add hidden states from the last decoder layer
        if output_hidden_states:
            all_hidden_states += (hidden_states,)

        next_cache = next_decoder_cache if use_cache else None
        if not return_dict:
            return tuple(v for v in [hidden_states, next_cache, all_hidden_states, all_self_attns] if v is not None)
        return BaseModelOutputWithPast(
            last_hidden_state=hidden_states,
            past_key_values=next_cache,
            hidden_states=all_hidden_states,
            attentions=all_self_attns,
        )

    @paddle.no_grad()
    def encode_sentences(self, sentences: List[str], instruction_len, **kwargs) -> np.ndarray:
        all_embeddings = []
        for start_index in tqdm.tqdm(list(range(0, len(sentences), self.eval_batch_size)), desc="Batches"):

            sentences_batch = sentences[start_index : start_index + self.eval_batch_size]
            inputs = self.tokenizer(
                sentences_batch,
                max_length=self.max_seq_length,
                padding=True,
                return_attention_mask=True,
                return_token_type_ids=False,
                return_tensors="pd",
                truncation=True,
            )
            last_hidden_states = self.m_forward(**inputs)[0]  # get bs*len*4096
            pool_mask = inputs["attention_mask"]
            pool_mask[:, :instruction_len] = 0

            embeddings = self.latent_model.forward(last_hidden_states, pool_mask)
            embeddings = paddle.nn.functional.normalize(embeddings, p=2, axis=1)

            all_embeddings.append(embeddings.cpu().numpy().astype("float32"))

        return np.concatenate(all_embeddings, axis=0)

    def encode_queries(self, queries: List[str], **kwargs) -> np.ndarray:
        input_texts = [self.query_instruction + q + self.tokenizer.eos_token for q in queries]
        instruction_len = len(self.tokenizer.encode(self.query_instruction, add_special_tokens=False)["input_ids"])
        return self.encode_sentences(input_texts, instruction_len)

    def encode_corpus(self, corpus: List[Union[Dict[str, str], str]], **kwargs) -> np.ndarray:
        if isinstance(corpus[0], dict):
            input_texts = ["{} {}".format(doc.get("title", ""), doc["text"]).strip() for doc in corpus]
        else:
            input_texts = corpus

        input_texts = [self.document_instruction + doc + self.tokenizer.eos_token for doc in input_texts]
        instruction_len = len(self.tokenizer.encode(self.document_instruction, add_special_tokens=False)["input_ids"])
        return self.encode_sentences(input_texts, instruction_len)
