import copy
import json
import re
import time
from typing import Dict, Iterator, List, Optional, Union

import json5

from qwen_agent.agents.assistant import KNOWLEDGE_SNIPPET, Assistant, format_knowledge_to_source_and_content
from qwen_agent.agents.doc_qa.parallel_doc_qa_member import NO_RESPONSE, ParallelDocQAMember
from qwen_agent.agents.doc_qa.parallel_doc_qa_summary import ParallelDocQASummary
from qwen_agent.agents.keygen_strategies import GenKeyword
from qwen_agent.llm.base import BaseChatModel, ModelServiceError
from qwen_agent.llm.schema import DEFAULT_SYSTEM_MESSAGE, USER, Message
from qwen_agent.log import logger
from qwen_agent.tools import BaseTool
from qwen_agent.tools.doc_parser import DocParser
from qwen_agent.tools.simple_doc_parser import PARSER_SUPPORTED_FILE_TYPES
from qwen_agent.utils.parallel_executor import parallel_exec
from qwen_agent.utils.tokenization_qwen import count_tokens
from qwen_agent.utils.utils import (extract_files_from_messages, extract_text_from_message, get_file_type,
                                    print_traceback)

MAX_NO_RESPONSE_RETRY = 4
DEFAULT_NAME = 'Simple Parallel DocQA With RAG Sum Agents'
DEFAULT_DESC = '简易并行后用RAG召回内容，然后回答的Agent'

PARALLEL_CHUNK_SIZE = 1000  # chunk size param for parallel chunk

MAX_RAG_TOKEN_SIZE = 4500
RAG_CHUNK_SIZE = 300


class ParallelDocQA(Assistant):

    def __init__(self,
                 function_list: Optional[List[Union[str, Dict, BaseTool]]] = None,
                 llm: Optional[Union[Dict, BaseChatModel]] = None,
                 system_message: Optional[str] = DEFAULT_SYSTEM_MESSAGE,
                 name: Optional[str] = DEFAULT_NAME,
                 description: Optional[str] = DEFAULT_DESC,
                 files: Optional[List[str]] = None):

        function_list = function_list or []
        super().__init__(
            function_list=[{
                'name': 'retrieval',
                'max_ref_token': MAX_RAG_TOKEN_SIZE,
                'parser_page_size': RAG_CHUNK_SIZE,
                'rag_searchers': ['keyword_search']
            }] + function_list,
            llm=llm,
            system_message=system_message,
            name=name,
            description=description,
            files=files,
        )

        self.doc_parse = DocParser()
        self.summary_agent = ParallelDocQASummary(llm=self.llm)

    def _get_files(self, messages: List[Message]):
        session_files = extract_files_from_messages(messages, include_images=False)
        valid_files = []
        for file in session_files:
            f_type = get_file_type(file)
            if f_type in PARSER_SUPPORTED_FILE_TYPES and file not in valid_files:
                valid_files.append(file)
        return valid_files

    def _parse_and_chunk_files(self, messages: List[Message]):
        valid_files = self._get_files(messages)
        records = []
        for file in valid_files:
            # here to split docs, we should decide chunc size by input doc token length
            # if a document's tokens are below this max_ref_token, it will remain unchunked.
            _record = self.doc_parse.call(params={'url': file},
                                          parser_page_size=PARALLEL_CHUNK_SIZE,
                                          max_ref_token=PARALLEL_CHUNK_SIZE)
            records.append(_record)
        return records

    def _retrieve_according_to_member_responses(
        self,
        messages: List[Message],
        lang: str = 'en',
        user_question: str = '',
        member_res: str = '',
    ):
        messages = copy.deepcopy(messages)
        valid_files = self._get_files(messages)

        keygen = GenKeyword(llm=self.llm)
        member_res_token_num = count_tokens(member_res)

        # Limit the token length of keygen input to avoid wasting tokens due to excessively long docqa member output.
        unuse_member_res = member_res_token_num > MAX_RAG_TOKEN_SIZE
        query = user_question if unuse_member_res else f'{user_question}\n\n{member_res}'

        try:
            *_, last = keygen.run([Message(USER, query)])
        except ModelServiceError:
            print_traceback()

        keyword = last[-1].content
        keyword = keyword.strip()
        if keyword.startswith('```json'):
            keyword = keyword[len('```json'):]
        if keyword.endswith('```'):
            keyword = keyword[:-3]

        try:
            logger.info(keyword)
            keyword_dict = json5.loads(keyword)
            keyword_dict['text'] = query
            if unuse_member_res:
                keyword_dict['text'] += '\n\n' + member_res
            rag_query = json.dumps(keyword_dict, ensure_ascii=False)
        except Exception:
            rag_query = query
            if unuse_member_res:
                rag_query += '\n\n' + member_res

        # max_ref_token is the retrieve doc token size
        # parser_page_size is the chunk size in retrieve

        retrieve_content = self.function_map['retrieval'].call(
            {
                'query': rag_query,
                'files': valid_files
            },
            max_ref_token=MAX_RAG_TOKEN_SIZE,
            parser_page_size=RAG_CHUNK_SIZE,
        )
        if not isinstance(retrieve_content, str):
            retrieve_content = json.dumps(retrieve_content, ensure_ascii=False, indent=4)

        retrieve_content = format_knowledge_to_source_and_content(retrieve_content)

        snippets = []

        for k in retrieve_content:
            snippets.append(KNOWLEDGE_SNIPPET[lang].format(source=k['source'], content=k['content']))

        assert len(snippets) > 0, retrieve_content
        retrieve_res = '\n\n'.join(snippets)
        return retrieve_res

    def _is_none_response(self, text: str) -> bool:
        none_response_list = ['很抱歉', NO_RESPONSE, "\"res\": \"none\""]
        for none_response in none_response_list:
            if none_response in text.lower():
                return True
        return False

    def _extract_text_from_output(self, output):
        # Remove symbols and keywords from the JSON structure using regular expressions
        cleaned_output = re.sub(r'[{}"]|("res":\s*"ans"|"res":\s*"none"|"\s*content":\s*)', '', output)

        # cleaned_output = re.sub(r'\s*,\s*|\{no_response\}', '', cleaned_output).strip()
        return cleaned_output

    def _parser_json(self, content):
        content = content.strip()
        if content.startswith('```json'):
            content = content[len('```json'):]
        if content.endswith('```'):
            content = content[:-3]
        try:
            content_dict = json5.loads(content)
            return True, content_dict
        except Exception:
            return False, content

    def _run(self, messages: List[Message], lang: str = 'en', **kwargs) -> Iterator[List[Message]]:

        messages = copy.deepcopy(messages)
        # Extract User Question
        user_question = extract_text_from_message(messages[-1], add_upload_info=False)
        logger.info('user_question: ' + user_question)

        # Implement chunk strategy for parallel agent
        records = self._parse_and_chunk_files(messages=messages)
        assert len(records) > 0, 'records is empty, all url parsing failed.'

        data = []
        idx = 0
        for record in records:
            assert len(record['raw']) > 0, 'Document content cannot be empty or null.'
            for chunk in record['raw']:
                chunk_text = chunk['content']
                data.append({
                    'index': idx,
                    'messages': messages,
                    'lang': lang,
                    'knowledge': chunk_text,
                    'instruction': user_question,
                })
                idx += 1
        logger.info('Parallel Member Num: ' + str(len(data)))

        # Retry for None in 7b model
        retry_cnt = MAX_NO_RESPONSE_RETRY
        member_res = ''
        while retry_cnt > 0:
            time1 = time.time()
            results = parallel_exec(self._ask_member_agent, data, jitter=0.5)
            # results = serial_exec(self._qa, data)
            time2 = time.time()
            logger.info(f'Finished parallel_exec. Time spent: {time2 - time1} seconds.')
            ordered_results = sorted(results, key=lambda x: x[0])
            filtered_results = []

            for index, text in ordered_results:
                parser_success, parser_json_content = self._parser_json(text)
                if parser_success and ('res' in parser_json_content) and ('content' in parser_json_content):
                    pa_res, pa_cotent = parser_json_content['res'], parser_json_content['content']
                    if (pa_res in ['ans', 'none']) and (isinstance(pa_cotent, str)):
                        if pa_res == 'ans':
                            filtered_results.append((index, pa_cotent.strip()))
                            continue
                        elif pa_res == 'none':
                            continue
                if self._is_none_response(text):
                    continue
                clean_output = self._extract_text_from_output(text)
                filtered_results.append((index, clean_output.strip()))

            if filtered_results:
                member_res = '\n\n'.join(text for index, text in filtered_results)
                break
            retry_cnt -= 1

        retrieve_content = self._retrieve_according_to_member_responses(messages=messages,
                                                                        lang=lang,
                                                                        user_question=user_question,
                                                                        member_res=member_res)
        return self.summary_agent.run(messages=messages, lang=lang, knowledge=retrieve_content)

    def _ask_member_agent(self,
                          index: int,
                          messages: List[Message],
                          lang: str = 'en',
                          knowledge: str = '',
                          instruction: str = '') -> tuple:
        doc_qa = ParallelDocQAMember(llm=self.llm)
        *_, last = doc_qa.run(messages=messages, knowledge=knowledge, lang=lang, instruction=instruction)
        return index, last[-1].content
