from django.utils import timezone
from rdkit import Chem
from rest_framework import serializers
from rest_framework.exceptions import NotAuthenticated

from askcos_site.main.models import BlacklistedReactions, BlacklistedChemicals, SavedResults
from askcos_site.askcos_celery.treebuilder.tb_coordinator_mcts import get_buyable_paths as get_buyable_paths_v1
from askcos_site.askcos_celery.treebuilder.tb_coordinator_mcts_v2 import get_buyable_paths as get_buyable_paths_v2
from askcos_site.main.utils import is_banned
from .celery import CeleryTaskAPIView

class TreeBuilderSerializer(serializers.Serializer):
    """Serializer for tree builder task parameters."""
    smiles = serializers.CharField()
    version = serializers.IntegerField(default=1)
    max_depth = serializers.IntegerField(default=4)
    max_branching = serializers.IntegerField(default=25)
    expansion_time = serializers.IntegerField(default=60)
    template_count = serializers.IntegerField(default=100)
    max_cum_prob = serializers.FloatField(default=0.995)
    max_chemicals = serializers.IntegerField(required=False, allow_null=True)
    max_reactions = serializers.IntegerField(required=False, allow_null=True)
    max_iterations = serializers.IntegerField(required=False, allow_null=True)

    buyable_logic = serializers.CharField(default='and')
    max_ppg_logic = serializers.CharField(default='none')
    max_ppg = serializers.IntegerField(required=False)

    max_scscore_logic = serializers.CharField(default='none')
    max_scscore = serializers.FloatField(required=False)

    chemical_property_logic = serializers.CharField(default='none')
    max_chemprop_c = serializers.IntegerField(required=False)
    max_chemprop_n = serializers.IntegerField(required=False)
    max_chemprop_o = serializers.IntegerField(required=False)
    max_chemprop_h = serializers.IntegerField(required=False)

    chemical_popularity_logic = serializers.CharField(default='none')
    min_chempop_reactants = serializers.IntegerField(required=False)
    min_chempop_products = serializers.IntegerField(required=False)

    filter_threshold = serializers.FloatField(default=0.75)
    template_set = serializers.CharField(default='reaxys')
    template_prioritizer_version = serializers.IntegerField(default=0)
    buyables_source = serializers.ListField(child=serializers.CharField(allow_blank=True), required=False, allow_empty=True)
    return_first = serializers.BooleanField(default=True)
    max_trees = serializers.IntegerField(default=500)

    score_trees = serializers.BooleanField(default=True)
    cluster_trees = serializers.BooleanField(default=True)
    cluster_method = serializers.CharField(default='hdbscan')
    cluster_min_samples = serializers.IntegerField(default=5)
    cluster_min_size = serializers.IntegerField(default=5)

    json_format = serializers.CharField(default='treedata')

    store_results = serializers.BooleanField(default=False)
    description = serializers.CharField(default='')

    banned_reactions = serializers.ListField(child=serializers.CharField(), required=False)
    banned_chemicals = serializers.ListField(child=serializers.CharField(), required=False)

    priority = serializers.IntegerField(default=1)

    def validate_smiles(self, value):
        """Verify that the requested smiles is valid. Returns canonicalized SMILES."""
        mol = Chem.MolFromSmiles(value)
        if not mol:
            raise serializers.ValidationError('Cannot parse smiles with rdkit.')
        if is_banned(self.context['request'], value):
            raise serializers.ValidationError('ASKCOS does not provide results for compounds on restricted lists such as the CWC and DEA schedules.')
        return Chem.MolToSmiles(mol)

    def validate_buyable_logic(self, value):
        """Verify that the the specified buyable_logic is valid."""
        if value not in ['none', 'and', 'or']:
            raise serializers.ValidationError("Logic should be one of ['none', 'and', 'or'].")
        return value

    def validate_max_ppg_logic(self, value):
        """Verify that the the specified max_ppg_logic is valid."""
        if value not in ['none', 'and', 'or']:
            raise serializers.ValidationError("Logic should be one of ['none', 'and', 'or'].")
        return value

    def validate_max_scscore_logic(self, value):
        """Verify that the the specified max_scscore_logic is valid."""
        if value not in ['none', 'and', 'or']:
            raise serializers.ValidationError("Logic should be one of ['none', 'and', 'or'].")
        return value

    def validate_chemical_property_logic(self, value):
        """Verify that the the specified chemical_property_logic is valid."""
        if value not in ['none', 'and', 'or']:
            raise serializers.ValidationError("Logic should be one of ['none', 'and', 'or'].")
        return value

    def validate_chemical_popularity_logic(self, value):
        """Verify that the the specified chemical_popularity_logic is valid."""
        if value not in ['none', 'and', 'or']:
            raise serializers.ValidationError("Logic should be one of ['none', 'and', 'or'].")
        return value

    def validate_banned_chemicals(self, value):
        """
        Verify that the provided SMILES is valid. Returns canonicalized SMILES.
        """
        new_value = []
        for v in value:
            mol = Chem.MolFromSmiles(v)
            if not mol:
                raise serializers.ValidationError('Cannot parse smiles with rdkit.')
            new_value.append(Chem.MolToSmiles(mol, isomericSmiles=True))
        return new_value

    def validate_banned_reactions(self, value):
        """
        Verify that the provided SMILES is valid. Returns canonicalized SMILES.
        """
        new_value = []
        for v in value:
            try:
                reactants, agents, products = v.split('>')
            except ValueError:
                raise serializers.ValidationError('Cannot parse reaction smiles.')
            try:
                reactants = standardize(reactants, isomericSmiles=True)
            except ValueError:
                raise serializers.ValidationError('Cannot parse reaction reactants.')
            try:
                agents = standardize(agents, isomericSmiles=True)
            except ValueError:
                raise serializers.ValidationError('Cannot parse reaction agents.')
            try:
                products = standardize(products, isomericSmiles=True)
            except ValueError:
                raise serializers.ValidationError('Cannot parse reaction products.')
            new_value.append(reactants + '>' + agents + '>' + products)
        return new_value

def standardize(smiles, isomericSmiles=True):
    """
    Split input SMILES into individual molecules, canonicalizes each, then
    sorts and re-combines canonicalized SMILES into single SMILES.
    """
    parts = smiles.split('.')
    canonicalized_parts = []
    for part in parts:
        mol = Chem.MolFromSmiles(part)
        if not mol:
            raise ValueError()
        canonicalized_parts.append(Chem.MolToSmiles(mol, isomericSmiles=isomericSmiles))
    canonicalized_parts.sort()
    return '.'.join(canonicalized_parts)

class TreeBuilderAPIView(CeleryTaskAPIView):
    """
    API endpoint for tree builder prediction task.

    Method: POST

    Parameters:

    - `smiles` (str): SMILES string of target
    - `version` (int, optional): tree builder version to use
    - `max_depth` (int, optional): maximum depth of returned pathways
    - `max_branching` (int, optional): maximum branching during pathway exploration
    - `expansion_time` (int, optional): time limit for tree expansion
    - `template_count` (int, optional): number of templates to consider
    - `max_cum_prob` (float, optional): maximum cumulative probability of templates
    - `max_chemicals` (int, optional): maximum number of chemicals explored (v2 only)
    - `max_reactions` (int, optional): maximum number of reactions explored (v2 only)
    - `max_iterations` (int, optional): maximum iterations to run MCTS (v2 only)
    - `buyable_logic` (str, optional): logic type for buyable termination (none/and/or)
    - `max_ppg_logic` (str, optional): logic type for price based termination (none/and/or)
    - `max_ppg` (int, optional): maximum price for price based termination
    - `max_scscore_logic` (str, optional): logic type for synthetic complexity termination (none/and/or)
    - `max_scscore` (int, optional): maximum scscore for synthetic complexity termination
    - `chemical_property_logic` (str, optional): logic type for chemical property termination (none/and/or)
    - `max_chemprop_c` (int, optional): maximum carbon count for termination
    - `max_chemprop_n` (int, optional): maximum nitrogen count for termination
    - `max_chemprop_o` (int, optional): maximum oxygen count for termination
    - `max_chemprop_h` (int, optional): maximum hydrogen count for termination
    - `chemical_popularity_logic` (str, optional): logic type for chemical popularity termination (none/and/or)
    - `min_chempop_reactants` (int, optional): minimum reactant precedents for termination
    - `min_chempop_products` (int, optional): minimum product precedents for termination
    - `filter_threshold` (float, optional): fast filter threshold
    - `template_set` (str, optional): template set to use
    - `template_prioritizer_version` (int, optional): version number of template relevance model to use
    - `buyables_source` (list[str], optional): list of source(s) to consider when looking up buyables
    - `return_first` (bool, optional): whether to return upon finding the first pathway
    - `max_trees` (int, optional): maximum number of pathways to return
    - `score_trees` (bool, optional): whether to score trees using pathway ranking model
    - `cluster_trees` (bool, optional): whether to cluster trees
    - `cluster_method` (bool, optional): method to use for clustering, supports 'hdbscan' or 'kmeans'
    - `cluster_min_samples` (bool, optional): minimum number of samples when using 'hdbscan'
    - `cluster_min_size` (bool, optional): minimum cluster size when using 'hdbscan'
    - `json_format` (str, optional): return format for trees, either 'treedata' or 'nodelink'
    - `store_results` (bool, optional): whether to permanently save this result
    - `description` (str, optional): description to associate with stored result
    - `banned_reactions` (list[str], optional): list of reactions to not consider
    - `banned_chemicals` (list[str], optional): list of molecules to not consider
    - `priority` (int, optional): set priority for celery task (0 = low, 1 = normal (default), 2 = high)

    Returns:

    - `task_id`: celery task ID
    """

    serializer_class = TreeBuilderSerializer

    def execute(self, request, data):
        """
        Execute tree builder task and return celery result object.
        """
        if data['store_results'] and not request.user.is_authenticated:
            raise NotAuthenticated('You must be authenticated to store tree builder results.')

        termination_logic = {'and': [], 'or': []}

        buyable_logic = data['buyable_logic']
        if buyable_logic != 'none':
            termination_logic[buyable_logic].append('buyable')

        max_ppg_logic = data['max_ppg_logic']
        if max_ppg_logic != 'none':
            max_ppg = data.get('max_ppg')
            termination_logic[max_ppg_logic].append('max_ppg')
        else:
            max_ppg = None

        max_scscore_logic = data['max_scscore_logic']
        if max_scscore_logic != 'none':
            max_scscore = data.get('max_scscore')
            termination_logic[max_scscore_logic].append('max_scscore')
        else:
            max_scscore = None

        chemical_property_logic = data['chemical_property_logic']
        if chemical_property_logic != 'none':
            param_dict = {
                'C': 'max_chemprop_c',
                'N': 'max_chemprop_n',
                'O': 'max_chemprop_o',
                'H': 'max_chemprop_h',
            }
            max_elements = {k: data[v] for k, v in param_dict.items() if v in data}
            termination_logic[chemical_property_logic].append('max_elements')
        else:
            max_elements = None

        chemical_popularity_logic = data['chemical_popularity_logic']
        if chemical_popularity_logic != 'none':
            min_history = {
                'as_reactant': data.get('min_chempop_reactants', 5),
                'as_product': data.get('min_chempop_products', 5),
            }
            termination_logic[chemical_popularity_logic].append('min_history')
        else:
            min_history = None

        # Clean buyables source
        buyables_source = data.get('buyables_source')
        if buyables_source is not None and 'none' in buyables_source:
            # Include both null and empty string source in query
            buyables_source.remove('none')
            buyables_source.extend([None, ''])

        # Retrieve user specific banlists
        banned_reactions = data.get('banned_reactions', [])
        banned_chemicals = data.get('banned_chemicals', [])
        if request.user.is_authenticated:
            banned_reactions += list(set(
                [x.smiles for x in BlacklistedReactions.objects.filter(user=request.user, active=True)]))
            banned_chemicals += list(set(
                [x.smiles for x in BlacklistedChemicals.objects.filter(user=request.user, active=True)]))

        args = (data['smiles'],)
        kwargs = {
            'max_depth': data['max_depth'],
            'max_branching': data['max_branching'],
            'expansion_time': data['expansion_time'],
            'template_count': data['template_count'],
            'max_chemicals': data.get('max_chemicals'),
            'max_reactions': data.get('max_reactions'),
            'max_iterations': data.get('max_iterations'),
            'max_cum_template_prob': data['max_cum_prob'],
            'max_ppg': max_ppg,
            'max_scscore': max_scscore,
            'max_elements': max_elements,
            'min_history': min_history,
            'termination_logic': termination_logic,
            'filter_threshold': data['filter_threshold'],
            'template_set': data['template_set'],
            'template_prioritizer_version': data['template_prioritizer_version'],
            'buyables_source': buyables_source,
            'known_bad_reactions': banned_reactions,
            'forbidden_molecules': banned_chemicals,
            'return_first': data['return_first'],
            'max_trees': data['max_trees'],
            'run_async': data['store_results'],
            'score_trees': data['score_trees'],
            'cluster_trees': data['cluster_trees'],
            'cluster_method': data['cluster_method'],
            'cluster_min_samples': data['cluster_min_samples'],
            'cluster_min_size': data['cluster_min_size'],
            'json_format': data['json_format'],
            'paths_only': True,
        }

        if data['version'] == 1:
            result = get_buyable_paths_v1.apply_async(args, kwargs, priority=data['priority'])
        elif data['version'] == 2:
            result = get_buyable_paths_v2.apply_async(args, kwargs, priority=data['priority'])

        if data['store_results']:
            now = timezone.now()
            saved_result = SavedResults.objects.create(
                user=request.user,
                created=now,
                dt=now.strftime('%B %d, %Y %H:%M:%S %p'),
                result_id=result.id,
                result_state='pending',
                result_type='tree_builder',
                description=data['description']
            )

        return result

tree_builder = TreeBuilderAPIView.as_view()


'''
This file contains all the implementations and data relevant to channel functions
'''
#pylint: disable=trailing-whitespace

from state import get_store, get_tokens
from auth import verify_token
from error import InputError, AccessError

MESSAGE_BLOCK = 50
SLACKR_OWNER = 1
SLACKR_MEMBER = 2

def channel_invite(token, channel_id, u_id):
    '''
    Invites a user (with user id u_id) to join a channel with ID channel_id.
    Once invited the user is added to the channel immediately.
    Args: token (str), channel_id (int), u_id (int)
    InputError: channel ID does not correspond to a valid channel;
    user ID does not refer to a valid user.
    AccessError: authorised user is not already a member of channel with channel ID.
    '''
    # verify the validity of the authorised user's token
    if verify_token(token) is False:
        raise AccessError(description="Invalid token")

    # check that channel_id corresponds to a valid channel
    data = get_store()
    if not data.channels.channel_exists(channel_id):
        raise InputError(
            description="Channel with this channel ID does not exist")

    # check that the authorised user belongs to this valid channel
    auth_u_id = get_tokens()[token]
    if not data.user_channel.is_member(auth_u_id, channel_id):
        raise AccessError(
            description="The authorised user is not a member of this channel")

    # check that u_id corresponds to a valid user
    if not data.users.user_exists(u_id):
        raise InputError(description="User ID is not valid")

    # add the user with u_id into the channel
    # update the database by adding a link between the user and the channel
    # checks if user is already apart of the channel
    data.user_channel.join_channel(u_id, channel_id)

def channel_details(token, channel_id):
    '''
    Given a Channel with ID channel_id that the authorised user is part of,
    provide basic details about the channel.
    Args: token (str), channel_id (int)
    InputError: channel_id is not a valid channel
    AccessError: authorised user is not part of the channel with channel_id
    Output: (channel) name, owner_members, all_members
    '''

    data = get_store()

    # verify the token is valid
    if verify_token(token) is False:
        raise AccessError(description="Invalid token")

    if not data.channels.channel_exists(channel_id):
        raise InputError(description='Channel does not exist')

    # check that the authorised user is member of said channel
    auth_u_id = get_tokens()[token]
    if not data.user_channel.is_member(auth_u_id, channel_id):
        raise AccessError(
            description="The authorised user is not a member of channel with this channel ID")

    # Create two lists and append details about owner members of the channel and all its members
    # return the dictionary containing details of the channel
    return {
        "name": data.channels.channel_details(channel_id)['name'],
        "owner_members": data.channel_owners(channel_id),
        "all_members": data.channel_members(channel_id)
    }

def channel_messages(token, channel_id, start):
    '''
    Lists up to 50 messages within 'channel_id', beginning from the message indexed 'start'.
    Args:
        token (str): of the user authorising this action
        channel_id (int): of the channel whose messages require displaying
        start (int): index of the first message to display
    Raises:
        AccessError:
            if token invalid
            if authorised user does not hav permission to view the channel's messages
        InputError:
            if channel_id does not correspond to a valid channel
    Return: List of 50 messages from channel with channel_id
        starting from index 'start', if we reached the end of the list we set the 'end' index to -1
    '''
    # verify the user
    if verify_token(token) is False:
        raise AccessError(description='Invalid token')
    # get database information
    data = get_store()
    # getting id of the user
    u_id = get_tokens()[token]
    # verify the channel exists
    if not data.channels.channel_exists(channel_id):
        raise InputError(description='Channel does not exist')

    # verify the user is a member of the channel
    if not data.user_channel.is_member(u_id, channel_id):
        raise AccessError(
            description="You do not have permission to view this channel's messages")
    # getting the messages of the channel
    details = channel_id, start
    messages, more = data.channel_messages(u_id, details)
    return {"messages": messages,
            "start": start,
            "end": -1 if not more else start + MESSAGE_BLOCK
           }

def channel_leave(token, channel_id):
    '''
    Allows a user with 'token' to leave the channel with 'channel_id'

    Args: token (str), channel_id (int)
    Raises:
        AccessError:
            if token invalid
            if user with u_id was not a member of the channel in the first place
        InputError:
            if channel_id does not correspond to a valid channel
    Return: an empty dictionary
    '''
    # verify the user
    if verify_token(token) is False:
        raise AccessError(description='Invalid token')

    # get database information
    data = get_store()
    # getting id of the user
    u_id = get_tokens()[token]

    # verify the channel exists
    if not data.channels.channel_exists(channel_id):
        raise InputError(description="Invalid channel id")

    # verify the user is a member of the channel
    if not data.user_channel.is_member(u_id, channel_id):
        raise AccessError(description="Cannot leave channel: not a member")

    # deleting the user from the channel list
    data.user_channel.leave_channel(u_id, channel_id)
    return {}

def channel_join(token, channel_id):
    '''
    Allows a user with a valid 'token' to join a public channel with 'channel_id'

    Args: token (str), channel_id (int)
    Raises:
        AccessError:
            if token invalid
            if the channel is private
        InputError:
            if channel_id does not correspond to an existing channel
            if user with u_id is already a member of the channel
    Return: an empty dictionary
    '''
    # verify the user
    if verify_token(token) is False:
        raise AccessError(description='Invalid token')

    # get database information
    data = get_store()
    # getting id of the user
    u_id = get_tokens()[token]

    # verify the channel exists
    if not data.channels.channel_exists(channel_id):
        raise InputError(description="Invalid channel id")

    # verify user is not already a member
    if data.user_channel.is_member(u_id, channel_id):
        raise InputError(description="Already a member")

    # verify the channel is public unless user is a slackr owner
    if not data.admins.is_admin(u_id) and data.channels.is_private(channel_id):
        raise AccessError(
            description="Cannot join channel: channel is private")

    # ... joining channel: if admin
    if data.admins.is_admin(u_id):
        data.user_channel.add_owner(u_id, channel_id)
    else:
        data.user_channel.join_channel(u_id, channel_id)

def channel_addowner(token, channel_id, u_id):
    '''
    Promotes user with 'u_id' to an owner of channel with 'channel_id'

    Args:
        token (str): of the user authorising this action
        channel_id (int): of the channel to which to promote the user to owner
        u_id (int): of the user to be promoted
    Raises:
        AccessError:
            if token invalid
            if token does not belong to a user with permission to promote
        InputError:
            if channel_id does not correspond to a valid channel
    '''
    # verify the user
    if verify_token(token) is False:
        raise AccessError(description='Invalid token')

    # get database information
    data = get_store()
    # getting id of the user
    u_id_invoker = get_tokens()[token]

    # verify the channel exists
    if not data.channels.channel_exists(channel_id):
        raise InputError(description="Invalid channel id")

    # verify the invoker is either an owner of the channel or an admin
    if not data.admins.is_admin(u_id_invoker) and \
       not data.user_channel.is_owner(u_id_invoker, channel_id):
        raise AccessError(
            description="You do not have privileges to add owners")

    data.user_channel.add_owner(u_id, channel_id)

def channel_removeowner(token, channel_id, u_id):
    '''
    Demote user with 'u_id' from an owner to normal member of channel with 'channel_id'

    Args:
        token (str): of the user authorising this action
        channel_id (int): of the channel to which to demote the user to normal member
        u_id (int): of the user to be demoted
    Raises:
        AccessError:
            if token invalid
            if token does not belong to a user with permission to demote
            if attempting to demote an admin of the slackr
        InputError:
            if channel_id does not correspond to a valid channel
    '''
    # verify the user
    if verify_token(token) is False:
        raise AccessError(description='Invalid token')

    # get database information
    data = get_store()
    # getting id of the user
    u_id_invoker = get_tokens()[token]

    # verify the channel exists
    if not data.channels.channel_exists(channel_id):
        raise InputError(description="Invalid channel id")

    # verify the user to remove is not a Slackr owner
    # and the remover has valid privileges
    if not data.admins.is_admin(u_id_invoker) and data.admins.is_admin(u_id):
        raise AccessError(
            description="You do not have permission to remove the current user")

    # verify the invoker is either an owner of the channel or slackr
    if not data.user_channel.is_owner(u_id_invoker, channel_id):
        raise AccessError(
            description="You do not have premission to remove the current user")

    # remove the ownership
    data.user_channel.remove_owner(u_id, channel_id)


import datetime
import json

import pandas as pd
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import IntegrityError
from django.db.models import Count, F, Q, QuerySet
from django.db.models.functions import TruncMonth
from django.http import request
from django.shortcuts import HttpResponse, redirect, render, reverse
from django.utils import timezone

from datasite.commons import get_dt_page

from .models import *

try:
    from io import BytesIO as IO  # for modern python
except ImportError:
    from io import StringIO as IO  # for legacy python

DISPLAY_LENGTH = 8

def records_by_auth(user: User):
    if user.is_staff:
        return Record.objects.all()
    else:
        return Record.objects.filter(pub_user=user)

def kols_by_auth(user: User):
    if user.groups.filter(name="KOL信息贡献者").exists():
        if user.is_staff:
            return Kol.objects.all().order_by("name")
        else:
            return Kol.objects.filter(pub_user=user)
    elif user.groups.filter(name="医学顾问").exists():
        return Kol.objects.all().order_by("name")
    else:
        return Kol.objects.none()

def get_filters(qs: QuerySet, field: str):
    all_records = (
        qs.values(field)
        .order_by(field)
        .annotate(count=Count(field))
        .order_by(F("count").desc())
    )
    return all_records

def get_param(params):
    kw_param = params.get("kw")  # 根据空格拆分搜索关键字
    kol_param = params.get("kol")
    prov_param = params.getlist("province")  # 省份可能多选，需要额外处理
    city_param = params.getlist("city")  # 省份可能多选，需要额外处理
    month_param = params.getlist("month")

    # nation_param = params.getlist("nation")  # 国家参数
    # nation_id_list = get_id_list(nation_param)

    # prog_param = params.get("program")  # 栏目参数

    # 下面部分准备所有高亮关键字
    highlights = {}
    try:
        kw_list = kw_param.split(" ")
    except:
        kw_list = []

    for kw in kw_list:
        highlights[kw] = '<b class="highlight_kw">{}</b>'.format(kw)

    context = {
        "kw": kw_param,
        "kol": kol_param,
        "provinces": prov_param,
        "cities": city_param,
        "months": month_param,
        # "nations": nation_id_list,
        # "program": prog_param,
        "highlights": highlights,
    }

    return context

@login_required
def records(request: request) -> HttpResponse:
    print(request.GET)
    param_dict = get_param(request.GET)

    # 如果是管理员，显示全部记录，否则仅显示当前用户上传的记录
    records = records_by_auth(request.user)

    search_condition = Q()

    # 根据搜索筛选KOL
    kw = param_dict["kw"]
    if kw is not None:
        kw_list = kw.split(" ")
        for k in kw_list:
            search_condition.add(
                Q(kol__name__icontains=k)  # 搜索KOL姓名
                | Q(kol__hospital__name__icontains=k)  # 搜索供职医院名称
                | Q(purpose__icontains=k)  # 搜索拜访目标
                | Q(feedback__icontains=k),  # 搜索主要反馈
                Q.AND,
            )

    # 根据省份筛选Record
    provinces = param_dict["provinces"]
    if provinces:
        search_condition.add(Q(kol__hospital__province__in=provinces), Q.AND)

    # 根据城市筛选Record
    cities = param_dict["cities"]
    if cities:
        search_condition.add(Q(kol__hospital__city__in=cities), Q.AND)

    # 根据月份筛选Record
    months = param_dict["months"]
    if months:
        search_condition_month = Q()
        for k in months:
            visit_date = datetime.datetime.strptime(k, "%Y-%m-%d").date()
            visit_year = visit_date.year
            visit_month = visit_date.month
            search_condition_month.add(
                Q(visit_date__year=visit_year, visit_date__month=visit_month), Q.OR
            )
        search_condition.add(search_condition_month, Q.AND)

    # 根据Kol筛选Record
    kol = param_dict["kol"]
    if kol:
        search_condition.add(Q(kol__pk=kol), Q.AND)

    # 筛选并删除重复项
    search_result = records.filter(search_condition).distinct()

    #  下方两行代码为了克服MSSQL数据库和Django pagination在distinct(),order_by()等queryset时出现重复对象的bug
    sr_ids = [record.id for record in search_result]
    records = Record.objects.filter(id__in=sr_ids).order_by("-visit_date")

    paginator = Paginator(records, DISPLAY_LENGTH)
    page = request.GET.get("page")

    try:
        rows = paginator.page(page)
    except PageNotAnInteger:
        rows = paginator.page(1)
    except EmptyPage:
        rows = paginator.page(paginator.num_pages)

    # 根据不同维度汇总记录数
    filtered_provinces = get_filters(
        qs=records_by_auth(request.user), field="kol__hospital__province"
    )  # 按省份汇总
    filtered_cities = get_filters(
        qs=records_by_auth(request.user), field="kol__hospital__city"
    )  # 按城市汇总
    filtered_months = (
        records_by_auth(request.user)
        .annotate(month=TruncMonth("visit_date"))
        .values("month")
        .annotate(count=Count("id"))
        .order_by(F("month").desc())
    )  # 按拜访月份汇总

    context = {
        "records": rows,
        "num_pages": paginator.num_pages,
        "record_n": paginator.count,
        "display_length": DISPLAY_LENGTH,
        "kw": param_dict["kw"],
        "kol": Kol.objects.get(pk=int(param_dict["kol"])) if kol else None,
        "highlights": param_dict["highlights"],
        "filtered_provinces": filtered_provinces,
        "selected_provinces": param_dict["provinces"],
        "filtered_cities": filtered_cities,
        "selected_cities": param_dict["cities"],
        "filtered_months": filtered_months,
        "selected_months": param_dict["months"],
    }

    return render(request, "kol/records.html", context)

@login_required
def create_record(request):
    print(request.POST)
    if request.method == "POST":
        obj = Record(
            kol=Kol.objects.get(pk=int(request.POST.get("select_kol"))),
            visit_date=datetime.datetime.strptime(
                request.POST.get("visit_date"), "%Y-%m-%d"
            ).date(),
            purpose=request.POST.get("text_purpose"),
            attitude_1=int(request.POST.get("attitude_1")),
            attitude_2=int(request.POST.get("attitude_2")),
            attitude_3=int(request.POST.get("attitude_3")),
            attitude_4=int(request.POST.get("attitude_4")),
            attitude_5=int(request.POST.get("attitude_5")),
            feedback=request.POST.get("text_feedback"),
            pub_user=request.user,
        )
        obj.save()

        return redirect(reverse("kol:records"))
    else:
        kols = kols_by_auth(request.user)
        # 根据不同维度汇总记录数
        filtered_provinces = get_filters(
            qs=records_by_auth(request.user), field="kol__hospital__province"
        )  # 按省份汇总
        filtered_months = (
            records_by_auth(request.user)
            .annotate(month=TruncMonth("visit_date"))
            .values("month")
            .annotate(count=Count("id"))
            .order_by(F("month").desc())
        )  # 按拜访月份汇总

        # # 准备治疗观念选项
        # attitudes = {}
        # for i in range(4):
        #     attitudes[Record._meta.get_field(f"attitude_{i}").verbose_name] = eval(
        #         f"ATTITUDE_{i}"
        #     )
        context = {
            "attitude_1_choices": Record.ATTITUDE_1_CHOICES,
            "attitude_2_choices": Record.ATTITUDE_2_CHOICES,
            "attitude_3_choices": Record.ATTITUDE_3_CHOICES,
            "attitude_4_choices": Record.ATTITUDE_4_CHOICES,
            "attitude_5_choices": Record.ATTITUDE_5_CHOICES,
            "kols": kols,
            "filtered_provinces": filtered_provinces,
            "filtered_months": filtered_months,
        }
        return render(request, "kol/create_record.html", context)

@login_required
def update_record(request, pk: int):
    print(request.POST, request.FILES)
    if request.method == "POST":
        obj = Record.objects.get(pk=pk)
        obj.kol = Kol.objects.get(pk=int(request.POST.get("select_kol")))
        obj.visit_date = datetime.datetime.strptime(
            request.POST.get("visit_date"), "%Y-%m-%d"
        ).date()
        obj.purpose = request.POST.get("text_purpose")
        obj.attitude_1 = int(request.POST.get("attitude_1"))
        obj.attitude_2 = int(request.POST.get("attitude_2"))
        obj.attitude_3 = int(request.POST.get("attitude_3"))
        obj.attitude_4 = int(request.POST.get("attitude_4"))
        obj.attitude_5 = int(request.POST.get("attitude_5"))
        obj.feedback = request.POST.get("text_feedback")
        obj.upload_date = timezone.now()
        # obj.pub_user = request.user
        obj.save()

        # for file in request.FILES:
        #     obj_attachment = Attachment(record=obj, file=file)
        #     obj_attachment.save()

        return redirect(reverse("kol:records"))
    else:
        kols = kols_by_auth(request.user)
        # 根据不同维度汇总记录数
        filtered_provinces = get_filters(
            qs=records_by_auth(request.user), field="kol__hospital__province"
        )  # 按省份汇总
        filtered_months = (
            records_by_auth(request.user)
            .annotate(month=TruncMonth("visit_date"))
            .values("month")
            .annotate(count=Count("id"))
            .order_by(F("month").desc())
        )  # 按拜访月份汇总
        context = {
            "attitude_1_choices": Record.ATTITUDE_1_CHOICES,
            "attitude_2_choices": Record.ATTITUDE_2_CHOICES,
            "attitude_3_choices": Record.ATTITUDE_3_CHOICES,
            "attitude_4_choices": Record.ATTITUDE_4_CHOICES,
            "attitude_5_choices": Record.ATTITUDE_5_CHOICES,
            "record": Record.objects.get(pk=pk),
            # "attachments": Attachment.objects.filter(record=Record.objects.get(pk=pk)),
            "kols": kols,
            "filtered_provinces": filtered_provinces,
            "filtered_months": filtered_months,
        }
        return render(request, "kol/create_record.html", context)

@login_required
def delete_record(request):
    print(request.POST)
    if request.method == "POST":
        id = request.POST.get("id")
        qs_to_delete = Record.objects.get(id=id)  # 执行删除操作
        qs_to_delete.delete()
        return redirect(reverse("kol:records"))

@login_required()
def export_record(request):
    records = records_by_auth(request.user)
    df = pd.DataFrame(
        list(
            records.values(
                "visit_date",
                "kol__name",
                "kol__hospital__xltid",
                "kol__hospital__province",
                "kol__hospital__city",
                "kol__hospital__decile",
                "kol__hospital__name",
                "kol__rating_infl",
                "kol__rating_prof",
                "kol__rating_fav",
                "kol__supervisor",
                "kol__titles",
                "kol__classification",
                "purpose",
                "attitude_1",
                "attitude_2",
                "attitude_3",
                "attitude_4",
                "attitude_5",
                "feedback",
                "upload_date",
                "pub_user__username",
            )
        )
    )
    df["upload_date"] = df["upload_date"].dt.tz_localize(
        None
    )  # Excel不支持带有时区的时间格式，导出会报错
    df.columns = [
        "拜访日期",
        "KOL姓名",
        "医院编码",
        "省份",
        "城市",
        "医院Decile",
        "医院名称",
        "影响力",
        "专业度",
        "支持度",
        "博导/硕导",
        "头衔&荣誉",
        "客户分型",
        "拜访目的",
        "观念_EPO浓度\n3_HIF-PHI在疗效保证前提下，刺激产生内源性EPO越接近生理浓度越好\n2_HIF-PHI在疗效保证前提下，EPO浓度无所谓\n1_HIF-PHI治疗，内源性EPO越高，疗效会越好\n0_本次拜访未涉及",
        "观念_升速稳定性\n3_Hb升速需适中，1-2g/dl最佳\n2_Hb升速慢点无所谓\n1_Hb需尽快达标，每月＞2g/dl危害不大\n0_本次拜访未涉及",
        "观念_PHD选择性\n3_选择性抑制PHD1及PHD3更高的HIF-PHI，更有利于HIF2α稳定，改善铁代谢更好\n2_不了解铁代谢与PHD不同亚型作用的相关性\n1_铁代谢与PHD不同亚型作用无相关性\n0_本次拜访未涉及",
        "观念_升速与血栓\n3_Hb升速波动过大会增加血栓事件风险\n2_不清楚Hb升速波动与血栓事件相关性\n1_Hb升速波动过大与血栓事件无相关性\n0_本次拜访未涉及",
        "观念_脱靶效应\n3_HIF-PHI对脂代谢的影响是“脱靶效应”的表现，HIF应更聚焦于红细胞生成\n2_不清楚HIF-PHI对脂代谢的改变对患者的远期影响\n1_HIF-PHI对脂代谢的影响是对患者的获益\n0_本次拜访未涉及",
        "主要反馈",
        "上传日期",
        "上传用户",
    ]
    excel_file = IO()

    xlwriter = pd.ExcelWriter(excel_file, engine="xlsxwriter")

    df.to_excel(xlwriter, "data", index=False)

    xlwriter.save()
    xlwriter.close()

    excel_file.seek(0)

    # 设置浏览器mime类型
    response = HttpResponse(
        excel_file.read(),
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

    # 设置文件名
    now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    response["Content-Disposition"] = "attachment; filename=" + now + ".xlsx"
    return response

@login_required
def kols(request: request) -> HttpResponse:
    print(request.GET)
    param_dict = get_param(request.GET)

    kols = kols_by_auth(request.user)

    search_condition = Q()

    # 根据搜索筛选KOL
    kw = param_dict["kw"]
    if kw is not None:
        kw_list = kw.split(" ")

        for k in kw_list:
            search_condition.add(
                Q(name__icontains=k)  # 搜索KOL姓名
                | Q(hospital__name__icontains=k)  # 搜索供职医院名称
                | Q(titles__icontains=k),  # 搜索头衔和荣誉
                Q.AND,
            )

    # 根据省份筛选Kol
    provinces = param_dict["provinces"]
    if provinces:
        search_condition = search_condition.add(
            Q(hospital__province__in=provinces), Q.AND
        )

    # 根据城市筛选Kol
    cities = param_dict["cities"]
    if cities:
        search_condition = search_condition.add(Q(hospital__city__in=cities), Q.AND)

    search_result = kols.filter(search_condition).distinct()

    #  下方两行代码为了克服MSSQL数据库和Django pagination在distinct(),order_by()等queryset时出现重复对象的bug
    sr_ids = [kol.id for kol in search_result]
    kols = Kol.objects.filter(id__in=sr_ids).order_by("name")

    paginator = Paginator(kols, DISPLAY_LENGTH)
    page = request.GET.get("page")

    try:
        rows = paginator.page(page)
    except PageNotAnInteger:
        rows = paginator.page(1)
    except EmptyPage:
        rows = paginator.page(paginator.num_pages)

    context = {
        "kols": rows,
        "num_pages": paginator.num_pages,
        "record_n": paginator.count,
        "display_length": DISPLAY_LENGTH,
        "kw": param_dict["kw"],
        "filtered_provinces": get_filters(
            qs=kols_by_auth(request.user), field="hospital__province"
        ),
        "selected_provinces": param_dict["provinces"],
        "filtered_cities": get_filters(
            qs=kols_by_auth(request.user), field="hospital__city"
        ),
        "selected_cities": param_dict["cities"],
    }

    return render(request, "kol/kols.html", context)

@login_required
def create_kol(request):
    print(request.POST)
    if request.method == "POST":
        obj = Kol(
            name=request.POST.get("name"),
            hospital=Hospital.objects.get(pk=int(request.POST.get("select_hp"))),
            # dept=request.POST.get("dept"),
            rating_infl=int(request.POST.get("rating_infl")),
            rating_prof=int(request.POST.get("rating_prof")),
            rating_fav=int(request.POST.get("rating_fav")),
            supervisor=request.POST.get("supervisor"),
            titles=request.POST.get("text_title"),
            classification=request.POST.get("classification"),
            pub_user=request.user,
        )
        try:
            obj.save()
        except IntegrityError:
            context = {"kol": obj}
            return render(request, "kol/kol_duplicated.html", context)

        return redirect(reverse("kol:kols"))
    else:
        hospitals = Hospital.objects.all()
        context = {
            "hospitals": hospitals,
            "filtered_provinces": get_filters(
                qs=kols_by_auth(request.user), field="hospital__province"
            ),
        }
        return render(request, "kol/create_kol.html", context)

@login_required
def update_kol(request, pk: int):
    print(request.POST)
    if request.method == "POST":
        obj = Kol.objects.get(pk=pk)
        obj.name = request.POST.get("name")
        obj.hospital = Hospital.objects.get(pk=int(request.POST.get("select_hp")))
        # obj.dept = request.POST.get("dept")
        obj.rating_infl = int(request.POST.get("rating_infl"))
        obj.rating_prof = int(request.POST.get("rating_prof"))
        obj.rating_fav = int(request.POST.get("rating_fav"))
        obj.supervisor = request.POST.get("supervisor")
        obj.titles = request.POST.get("text_title")
        obj.classification = request.POST.get("classification")
        obj.upload_date = timezone.now()
        # obj.pub_user = request.user

        try:
            obj.save()
        except IntegrityError:
            context = {"kol": obj}
            return render(request, "kol/kol_duplicated.html", context)

        return redirect(reverse("kol:kols"))
    else:
        hospitals = Hospital.objects.all()
        context = {
            "kol": kols_by_auth(request.user).get(pk=pk),
            "hospitals": hospitals,
            "filtered_provinces": get_filters(
                qs=kols_by_auth(request.user), field="hospital__province"
            ),
            "filtered_cities": get_filters(
                qs=kols_by_auth(request.user), field="hospital__city"
            ),
        }
        return render(request, "kol/create_kol.html", context)

@login_required
def delete_kol(request):
    print(request.POST)
    if request.method == "POST":
        id = request.POST.get("id")
        qs_to_delete = Kol.objects.get(id=id)  # 执行删除操作
        qs_to_delete.delete()
        return redirect(reverse("kol:kols"))

@login_required()
def export_kol(request):
    kols = kols_by_auth(request.user)
    df = pd.DataFrame(
        list(
            kols.values(
                "name",
                "hospital__xltid",
                "hospital__province",
                "hospital__city",
                "hospital__decile",
                "hospital__name",
                "rating_infl",
                "rating_prof",
                "rating_fav",
                "supervisor",
                "titles",
                "classification",
                "upload_date",
                "pub_user__username",
            )
        )
    )
    df["upload_date"] = df["upload_date"].dt.tz_localize(
        None
    )  # Excel不支持带有时区的时间格式，导出会报错
    df.columns = [
        "KOL姓名",
        "医院编码",
        "省份",
        "城市",
        "医院Decile",
        "医院名称",
        "影响力",
        "专业度",
        "支持度",
        "博导/硕导",
        "头衔&荣誉",
        "客户分型",
        "上传日期",
        "上传用户",
    ]
    excel_file = IO()

    xlwriter = pd.ExcelWriter(excel_file, engine="xlsxwriter")

    df.to_excel(xlwriter, "data", index=False)

    xlwriter.save()
    xlwriter.close()

    excel_file.seek(0)

    # 设置浏览器mime类型
    response = HttpResponse(
        excel_file.read(),
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

    # 设置文件名
    now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    response["Content-Disposition"] = "attachment; filename=" + now + ".xlsx"
    return response

def bad_request(message):
    response = HttpResponse(
        json.dumps({"message": message}), content_type="application/json"
    )
    response.status_code = 400
    return response

def search_hps(request, kw):
    hospitals = Hospital.objects.filter(name__icontains=kw).order_by("-decile")[:10]
    try:
        results_list = []
        for hp in hospitals:
            option_dict = {
                "name": str(hp),
                "value": hp.pk,
            }
            results_list.append(option_dict)
        res = {
            "success": True,
            "results": results_list,
            "code": 200,
        }
    except Exception as e:
        res = {
            "success": False,
            "errMsg": e,
            "code": 0,
        }
    return HttpResponse(
        json.dumps(res, ensure_ascii=False),
        content_type="application/json charset=utf-8",
    )  # 返回结果必须是json格式