import torch
import os
import sys
import json
import random
import numpy as np
import argparse
from torch.utils.tensorboard import SummaryWriter
from datetime import datetime
from tqdm import tqdm
from torch.nn import DataParallel
import logging
from transformers.modeling_gpt2 import GPT2Config, GPT2LMHeadModel
from transformers import GPT2Tokenizer
from os.path import join, exists
from itertools import zip_longest, chain
# from chatbot.model import DialogueGPT2Model
import torch.nn.functional as F
from nltk.tokenize import word_tokenize

cur_dir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(cur_dir)
import config


def set_interact_args():
    """
    Sets up the training arguments.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--device', default='0', type=str, required=False, help='生成设备')
    parser.add_argument('--temperature', default=1, type=float, required=False, help='生成的temperature')
    parser.add_argument('--topk', default=8, type=int, required=False, help='最高k选1')
    parser.add_argument('--topp', default=0, type=float, required=False, help='最高积累概率')
    parser.add_argument('--model_config', default='config/model_config_dialogue_small.json', type=str, required=False,
                        help='模型参数')
    parser.add_argument('--log_path', default='data/interacting.log', type=str, required=False, help='interact日志存放位置')
    parser.add_argument('--dialogue_model_path', default='dialogue_model_path/', type=str, required=False,
                        help='对话模型路径')
    parser.add_argument('--save_samples_path', default="sample/", type=str, required=False, help="保存聊天记录的文件路径")
    parser.add_argument('--repetition_penalty', default=1.0, type=float, required=False,
                        help="重复惩罚参数，若生成的对话重复性较高，可适当提高该参数")
    parser.add_argument('--seed', type=int, default=None, help='设置种子用于生成随机数，以使得训练的结果是确定的')
    parser.add_argument('--max_len', type=int, default=25, help='每个utterance的最大长度,超过指定长度则进行截断')
    parser.add_argument('--max_history_len', type=int, default=3, help="dialogue history的最大长度")
    parser.add_argument('--no_cuda', action='store_true', help='不使用GPU进行预测')
    return parser.parse_args()


class Chatbot:
    def __init__(self):
        self.client = 0
        self.model = None
        self.history = []
        self.args = None
        self.tokenizer = None
        self.device = None
        self.samples_file = None
        self.save_samples_path = None

    def create_logger(self):
        """
        将日志输出到日志文件和控制台
        """
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.INFO)

        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(message)s')

        # 创建一个handler，用于写入日志文件
        file_handler = logging.FileHandler(
            filename=self.args.log_path)
        file_handler.setFormatter(formatter)
        file_handler.setLevel(logging.INFO)
        logger.addHandler(file_handler)

        # 创建一个handler，用于将日志输出到控制台
        console = logging.StreamHandler()
        console.setLevel(logging.DEBUG)
        console.setFormatter(formatter)
        logger.addHandler(console)

        return logger


    def top_k_top_p_filtering(self, logits, top_k=0, top_p=0.0, filter_value=-float('Inf')):
        """ Filter a distribution of logits using top-k and/or nucleus (top-p) filtering
            Args:
                logits: logits distribution shape (vocabulary size)
                top_k > 0: keep only top k tokens with highest probability (top-k filtering).
                top_p > 0.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering).
                    Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751)
            From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317
        """
        assert logits.dim() == 1  # batch size 1 for now - could be updated for more but the code would be less clear
        top_k = min(top_k, logits.size(-1))  # Safety check
        if top_k > 0:
            # Remove all tokens with a probability less than the last token of the top-k
            # torch.topk()返回最后一维最大的top_k个元素，返回值为二维(values,indices)
            # ...表示其他维度由计算机自行推断
            indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
            logits[indices_to_remove] = filter_value  # 对于topk之外的其他元素的logits值设为负无穷

        if top_p > 0.0:
            sorted_logits, sorted_indices = torch.sort(logits, descending=True)  # 对logits进行递减排序
            cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)

            # Remove tokens with cumulative probability above the threshold
            sorted_indices_to_remove = cumulative_probs > top_p
            # Shift the indices to the right to keep also the first token above the threshold
            sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
            sorted_indices_to_remove[..., 0] = 0

            indices_to_remove = sorted_indices[sorted_indices_to_remove]
            logits[indices_to_remove] = filter_value
        return logits

    def load_model(self, model_path=None, args=None):
        self.args = args
        if self.args:
            logger = self.create_logger(self.args)
        if not model_path:
            model_path = self.args.dialogue_model_path
        # 当用户使用GPU,并且GPU可用时
        cuda = torch.cuda.is_available()
        self.device = 'cuda' if cuda else 'cpu'
        # logger.info('using device:{}'.format(self.device))
        os.environ["CUDA_VISIBLE_DEVICES"] = self.device
        self.tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
        special_tokens_dict = {'cls_token': '[CLS]', 'sep_token': '[SEP]', 'pad_token': '[PAD]'}
        num_added_toks = self.tokenizer.add_special_tokens(special_tokens_dict)
        self.model = GPT2LMHeadModel.from_pretrained(model_path)
        self.model.to(self.device)
        self.model.eval()
        if self.args:
            if self.args.save_samples_path:
                if not os.path.exists(self.args.save_samples_path):
                    os.makedirs(self.args.save_samples_path)
                self.samples_file = open(self.args.save_samples_path + '/samples.txt', 'a', encoding='utf8')
                self.samples_file.write("聊天记录{}:\n".format(datetime.now()))
            # 存储聊天记录，每个utterance以token的id的形式进行存储
        self.history = []
        print('load model')

    def predict(self, text, client=0):
        # 判断是否还是同一个用户，如果不是，清空聊天记录
        if client != self.client:
            self.history = []
            self.client = client
        try:
            if self.args:
                if self.args.save_samples_path:
                    self.samples_file.write("user:{}\n".format(text))
            # revise needed 更精细的处理
            self.history.append(self.tokenizer.convert_tokens_to_ids(word_tokenize(text.lower())))
            input_ids = [self.tokenizer.convert_tokens_to_ids('[CLS]')]  # 每个input以[CLS]为开头

            max_history_len = config.max_history_len
            if self.args:
                if self.args.max_history_len:
                    max_history_len = self.args.max_history_len
            for history_id, history_utr in enumerate(self.history[-max_history_len:]):
                input_ids.extend(history_utr)
                input_ids.append(self.tokenizer.convert_tokens_to_ids('[SEP]'))
            curr_input_tensor = torch.tensor(input_ids).long().to(self.device)
            generated = []
            # 最多生成max_len个token
            max_len = config.max_len
            repetition_penalty = config.repetition_penalty
            temperature = config.temperature
            topk = config.topk
            topp = config.topp
            if self.args:
                repetition_penalty = self.args.repetition_penalty
                temperature = self.args.temperature
                topk = self.args.topk
                topp = self.args.topp
            for _ in range(max_len):
                outputs = self.model(input_ids=curr_input_tensor)
                next_token_logits = outputs[0][-1, :]
                # 对于已生成的结果generated中的每个token添加一个重复惩罚项，降低其生成概率
                for id in set(generated):
                    next_token_logits[id] /= repetition_penalty
                next_token_logits = next_token_logits / temperature
                # 对于[UNK]的概率设为无穷小，也就是说模型的预测结果不可能是[UNK]这个token
                next_token_logits[self.tokenizer.convert_tokens_to_ids('[UNK]')] = -float('Inf')
                filtered_logits = self.top_k_top_p_filtering(next_token_logits, top_k=topk, top_p=topp)
                # torch.multinomial表示从候选集合中无放回地进行抽取num_samples个元素，权重越高，抽到的几率越高，返回元素的下标
                next_token = torch.multinomial(F.softmax(filtered_logits, dim=-1), num_samples=1)
                if next_token == self.tokenizer.convert_tokens_to_ids('[SEP]'):  # 遇到[SEP]则表明response生成结束
                    break
                generated.append(next_token.item())
                curr_input_tensor = torch.cat((curr_input_tensor, next_token), dim=0)
                # his_text = tokenizer.convert_ids_to_tokens(curr_input_tensor.tolist())
                # print("his_text:{}".format(his_text))
            self.history.append(generated)
            # print(generated)
            text = self.tokenizer.convert_ids_to_tokens(generated)
            print("chatbot:" + " ".join(text))
            if self.save_samples_path:
                self.samples_file.write("chatbot:{}\n".format("".join(text)))
            return " ".join(text)
        except KeyboardInterrupt:
            #if self.args.save_samples_path:
                #self.samples_file.close()
            return "something wrong, refresh the page and try again!"

if __name__ == '__main__':
    bot = Chatbot()
    bot.load_model()
