import os
import re
import hashlib
import json
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Optional, Union, Any
import aiohttp
from fastapi import HTTPException
import logging
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, convert_to_openai_messages
from langchain.chat_models import init_chat_model
from utils.settings import MASTERKEY, BACKEND_HOST
from agent.agent_config import AgentConfig

USER = "user"
ASSISTANT = "assistant"
TOOL = "tool"

logger = logging.getLogger('app')

# Create a global thread pool executor for synchronous HTTP calls
thread_pool = ThreadPoolExecutor(max_workers=10)

# Create a concurrency semaphore to limit simultaneous API calls
api_semaphore = asyncio.Semaphore(8)  # Maximum 8 concurrent API calls

def detect_provider(model_name,model_server):
    """Detect provider type based on model name."""
    model_name_lower = model_name.lower()
    if any(claude_model in model_name_lower for claude_model in ["claude", "anthropic"]):
        return "anthropic",model_server.replace("/v1","")
    elif any(openai_model in model_name_lower for openai_model in ["gpt", "openai", "o1"]):
        return "openai",model_server
    else:
        # Default to openai-compatible format
        return "openai",model_server


def is_anthropic_opus_model(model_name: Optional[str]) -> bool:
    """Check if the model is an Anthropic Opus model."""
    return bool(model_name and "opus" in model_name.lower())


def sanitize_model_kwargs(
    model_name: str,
    model_provider: str,
    base_url: Optional[str],
    api_key: Optional[str],
    generate_cfg: Optional[Dict[str, Any]] = None,
    source: str = "agent"
) -> tuple[Dict[str, Any], List[str], bool]:
    """Sanitize model parameters, filtering incompatible params and returning logging info."""
    model_kwargs = {
        "model": model_name,
        "model_provider": model_provider,
        "base_url": base_url,
        "api_key": api_key
    }

    internal_params = {
        'tool_output_max_length',
        'tool_output_truncation_strategy',
        'tool_output_filters',
        'tool_output_exclude',
        'preserve_code_blocks',
        'preserve_json',
    }

    openai_only_params = {
        'n',
        'presence_penalty',
        'frequency_penalty',
        'logprobs',
        'top_logprobs',
        'logit_bias',
        'seed',
        'suffix',
        'best_of',
        'echo',
        'user',
    }

    params_to_filter = set(internal_params)
    is_opus_model = model_provider == 'anthropic' and is_anthropic_opus_model(model_name)

    if model_provider == 'anthropic':
        params_to_filter.update(openai_only_params)
    if is_opus_model:
        params_to_filter.add('temperature')

    original_keys = list((generate_cfg or {}).keys())
    filtered_cfg = {k: v for k, v in (generate_cfg or {}).items() if k not in params_to_filter}
    dropped_params = [k for k in original_keys if k in params_to_filter]

    default_temperature_applied = False
    if not is_opus_model:
        model_kwargs["temperature"] = 0.8
        default_temperature_applied = True

    model_kwargs.update(filtered_cfg)

    logger.info(
        "sanitize_model_kwargs source=%s provider=%s model=%s original_keys=%s dropped_keys=%s default_temperature_applied=%s",
        source,
        model_provider,
        model_name,
        original_keys,
        dropped_params,
        default_temperature_applied
    )

    return model_kwargs, dropped_params, default_temperature_applied


def get_versioned_filename(upload_dir: str, name_without_ext: str, file_extension: str) -> tuple[str, int]:
    """
    Get a versioned filename, automatically handling file deletion and version incrementing.

    Args:
        upload_dir: Upload directory path
        name_without_ext: Filename without extension
        file_extension: File extension (including dot)

    Returns:
        tuple[str, int]: (Final filename, version number)
    """
    # Check if the original file exists
    original_file = os.path.join(upload_dir, name_without_ext + file_extension)
    original_exists = os.path.exists(original_file)
    
    # Find all related versioned files
    pattern = re.compile(re.escape(name_without_ext) + r'_(\d+)' + re.escape(file_extension) + r'$')
    existing_versions = []
    files_to_delete = []
    
    for filename in os.listdir(upload_dir):
        # Check if it is the original file
        if filename == name_without_ext + file_extension:
            files_to_delete.append(filename)
            continue

        # Check if it is a versioned file
        match = pattern.match(filename)
        if match:
            version_num = int(match.group(1))
            existing_versions.append(version_num)
            files_to_delete.append(filename)
    
    # If no related files exist, use the original filename (version 1)
    if not original_exists and not existing_versions:
        return name_without_ext + file_extension, 1
    
    # Delete all existing files (original and versioned)
    for filename in files_to_delete:
        file_to_delete = os.path.join(upload_dir, filename)
        try:
            os.remove(file_to_delete)
            logger.info(f"Deleted file: {file_to_delete}")
        except OSError as e:
            logger.error(f"Failed to delete file {file_to_delete}: {e}")

    # Determine the next version number
    if existing_versions:
        next_version = max(existing_versions) + 1
    else:
        next_version = 2

    # Generate the versioned filename
    versioned_filename = f"{name_without_ext}_{next_version}{file_extension}"

    return versioned_filename, next_version

def create_stream_chunk(chunk_id: str, model_name: str, content: str = None, finish_reason: str = None) -> dict:
    """Create a standardized streaming response chunk"""
    chunk_data = {
        "id": chunk_id,
        "object": "chat.completion.chunk",
        "created": int(__import__('time').time()),
        "model": model_name,
        "choices": [{
            "index": 0,
            "delta": {"content": content} if content is not None else {},
            "finish_reason": finish_reason
        }]
    }
    return chunk_data

# def get_content_from_messages(messages: List[dict], tool_response: bool = True) -> str:
#     """Extract content from qwen-agent messages with special formatting"""
#     full_text = ''
#     content = []
#     TOOL_CALL_S = '[TOOL_CALL]'
#     TOOL_RESULT_S = '[TOOL_RESPONSE]'
#     THOUGHT_S = '[THINK]'
#     ANSWER_S = '[ANSWER]'
#     PREAMBLE_S = '[PREAMBLE]'
    
#     for msg in messages:
#         if msg['role'] == ASSISTANT:
#             if msg.get('reasoning_content'):
#                 assert isinstance(msg['reasoning_content'], str), 'Now only supports text messages'
#                 content.append(f'{THOUGHT_S}\n{msg["reasoning_content"]}')
#             if msg.get('content'):
#                 assert isinstance(msg['content'], str), 'Now only supports text messages'
#                 # Filter out incomplete tool_call text from streaming output
#                 content_text = msg["content"]

#                 # Use regex to replace incomplete tool_call patterns with empty string

#                 # Match and replace incomplete tool_call patterns
#                 content_text = re.sub(r'<t?o?o?l?_?c?a?l?l?$', '', content_text)
#                 # Only add if content is not empty after processing
#                 if content_text.strip():
#                     content.append(f'{ANSWER_S}\n{content_text}')
#             if msg.get('function_call'):
#                 content_text = msg["function_call"]["arguments"]
#                 content_text = re.sub(r'}\n<\/?t?o?o?l?_?c?a?l?l?$', '', content_text)
#                 if content_text.strip():
#                     content.append(f'{TOOL_CALL_S} {msg["function_call"]["name"]}\n{content_text}')
#         elif msg['role'] == FUNCTION:
#             if tool_response:
#                 content.append(f'{TOOL_RESULT_S} {msg["name"]}\n{msg["content"]}')
#         elif msg['role'] == "preamble":
#             content.append(f'{PREAMBLE_S}\n{msg["content"]}')
#         else:
#             raise TypeError

#     if content:
#         full_text = '\n'.join(content)

#     return full_text


def process_messages(messages: List[Dict], language: Optional[str] = None) -> List[Dict[str, str]]:
    """Process message list, including [TOOL_CALL]|[TOOL_RESPONSE]|[ANSWER] splitting and language directive addition.

    This is the inverse of get_content_from_messages, reassembling messages containing
    [TOOL_RESPONSE] back into msg['role'] == 'function' and msg.get('function_call') format.

    Args:
        messages: Message list
        language: Optional language parameter
        include_function_name: List of function_name keywords to include, defaults to ['find', 'get']
    """
    # Set default include function_name list
    include_function_name = ['find', 'get']

    processed_messages = []

    # Collect indices of all ASSISTANT messages
    assistant_indices = [i for i, msg in enumerate(messages) if msg.role == ASSISTANT]
    total_assistant_messages = len(assistant_indices)
    cutoff_point = max(0, total_assistant_messages - 5)

    # Process each message
    for i, msg in enumerate(messages):
        if msg.role == ASSISTANT:
            # Determine the position of this ASSISTANT message among all ASSISTANT messages (0-indexed)
            assistant_position = assistant_indices.index(i)

            # Split by [THINK|TOOL_CALL]|[TOOL_RESPONSE]|[ANSWER] using regex
            parts = re.split(r'\[(THINK|PREAMBLE|TOOL_CALL|TOOL_RESPONSE|ANSWER)\]', msg.content)

            # Reassemble content, choosing processing based on message position
            filtered_content = ""
            current_tag = None
            is_recent_message = assistant_position >= cutoff_point  # Recent messages

            for i in range(0, len(parts)):
                if i % 2 == 0:  # Text content
                    text = parts[i].strip()
                    if not text:
                        continue

                    # Do not forward historical tool call text
                    if current_tag == "TOOL_RESPONSE":
                        if is_recent_message:
                            # Recent ASSISTANT messages: preserve full TOOL_RESPONSE info (using abbreviated mode)
                            if len(text) <= 1000:
                                filtered_content += f"[TOOL_RESPONSE] {text}\n"
                            else:
                                # Extract first, middle, and last sections, 250 chars each
                                first_part = text[:250]
                                middle_start = len(text) // 2 - 125
                                middle_part = text[middle_start:middle_start + 250]
                                last_part = text[-250:]
                                
                                # Calculate omitted character count
                                omitted_count = len(text) - 750
                                omitted_text = f"...{omitted_count} chars omitted..."
                                
                                # Concatenate content
                                truncated_text = f"{first_part}\n{omitted_text}\n{middle_part}\n{omitted_text}\n{last_part}"
                                filtered_content += f"[TOOL_RESPONSE] {truncated_text}\n"
                        # Messages beyond the recent window: do not preserve TOOL_RESPONSE data (skip entirely)
                    elif current_tag == "TOOL_CALL":
                        if is_recent_message:
                            # Recent ASSISTANT messages: preserve TOOL_CALL info
                            filtered_content += f"[TOOL_CALL] {text}\n"
                        # Messages beyond the recent window: do not preserve TOOL_CALL data (skip entirely)
                    elif current_tag == "ANSWER":
                        # All ASSISTANT messages preserve ANSWER data
                        filtered_content += f"[ANSWER] {text}\n"
                    elif current_tag != "THINK" and current_tag != "PREAMBLE":
                        filtered_content += text + "\n"
                else:  # Tag
                    current_tag = parts[i]

            # Get the final processed content, stripping leading/trailing whitespace
            final_content = filtered_content.strip()
            if final_content:
                processed_messages.append({"role": msg.role, "content": final_content})
            else:
                # If processed content is empty, use original content
                processed_messages.append({"role": msg.role, "content": msg.content})
        else:
            processed_messages.append({"role": msg.role, "content": msg.content})

    # Inverse operation: reassemble messages containing [THINK|TOOL_RESPONSE] back into
    # msg['role'] == 'function' and msg.get('function_call') format.
    # This is the inverse of get_content_from_messages.
    final_messages = []
    for msg in processed_messages:
        if msg["role"] == ASSISTANT:
            # Split message content
            parts = re.split(r'\[(THINK|PREAMBLE|TOOL_CALL|TOOL_RESPONSE|ANSWER)\]', msg["content"])

            current_tag = None
            tool_id_counter = 0  # Unique tool call counter
            tool_id_list = []
            for i in range(0, len(parts)):
                if i % 2 == 0:  # Text content
                    text = parts[i].strip()
                    if not text:
                        continue
                    # Do not forward historical tool call text

                    if current_tag == "TOOL_RESPONSE":
                        # Parse TOOL_RESPONSE format: [TOOL_RESPONSE] function_name\ncontent
                        lines = text.split('\n', 1)
                        function_name = lines[0].strip() if lines else ""
                        response_content = lines[1].strip() if len(lines) > 1 else ""

                        # Filter out function_names that do not contain specified keywords
                        should_include = False
                        if function_name:
                            for exclude_name in include_function_name:
                                if exclude_name in function_name:
                                    should_include = True
                                    break

                        if should_include and len(tool_id_list)>0:
                            tool_id = tool_id_list.pop(0)
                            # Wrap TOOL_RESPONSE as a tool_result message, following the corresponding tool_use
                            final_messages.append({
                                "role": TOOL,
                                "tool_call_id": tool_id,  # Keep consistent with the preceding tool_use id
                                "name": function_name,
                                "content": response_content
                            })
                    elif current_tag == "TOOL_CALL":
                        # Parse TOOL_CALL format: [TOOL_CALL] function_name\narguments
                        lines = text.split('\n', 1)
                        function_name = lines[0].strip() if lines else ""
                        arguments = lines[1].strip() if len(lines) > 1 else ""

                        # Filter out function_names that do not contain specified keywords
                        should_include = False
                        if function_name:
                            for exclude_name in include_function_name:
                                if exclude_name in function_name:
                                    should_include = True
                                    break

                        if should_include:
                            tool_id = f"tool_id_{tool_id_counter}"  # Use unique counter
                            tool_id_list.append(tool_id)
                            tool_id_counter += 1  # Increment counter
                            final_messages.append({
                                "role": ASSISTANT,
                                "content": "",
                                "tool_calls": [{
                                    "id":tool_id,
                                    "function": {
                                        "name": function_name,
                                        "arguments": arguments
                                    }
                                }]
                            })
                    elif current_tag != "THINK" and current_tag != "PREAMBLE":
                        final_messages.append({
                            "role": ASSISTANT,
                            "content": text
                        })
                else:  # Tag
                    current_tag = parts[i]
        else:
            # Non-assistant messages or messages without [TOOL_RESPONSE] are added directly
            final_messages.append(msg)
    return final_messages


def get_user_last_message_content(messages: list) -> Optional[dict]:
    """Get the last message content from a message list."""
    if not messages or len(messages) == 0:
        return ""
    last_message = messages[-1]
    if last_message and last_message.get('role') == 'user':
        return last_message["content"]
    return ""

def format_messages_to_chat_history(messages: List[Dict[str, str]]) -> str:
    """Format messages as plain text chat history.

    Args:
        messages: Message list

    Returns:
        str: Formatted chat history
    """
    # Only take the last 15 messages
    chat_history = []
    for message in messages:
        role = message.get('role', '')
        content = message.get('content', '')
        name = message.get('name', '')
        if role == USER:
            chat_history.append(f"user: {content}")
        elif role == TOOL:
            chat_history.append(f"{name} response: {content}")
        elif role == ASSISTANT:
            if len(content) >0:
                chat_history.append(f"assistant: {content}")
            if message.get('tool_calls'):
                for tool_call in message.get('tool_calls'):
                    function_name = tool_call.get('function').get('name')
                    arguments = tool_call.get('function').get('arguments')
                    chat_history.append(f"{function_name} call: {arguments}")

    recent_chat_history = chat_history[-16:-1] if len(chat_history) > 16 else chat_history[:-1]
    return "\n".join(recent_chat_history)


def create_project_directory(dataset_ids: Optional[List[str]], bot_id: str, skills: Optional[List[str]] = None) -> Optional[str]:
    """Common logic for creating project directories."""

    # If dataset_ids is empty, do not create a directory
    if not dataset_ids:
        dataset_ids = []

    try:
        from utils.multi_project_manager import create_robot_project
        from pathlib import Path
        return create_robot_project(dataset_ids, bot_id, skills=skills)
    except Exception as e:
        logger.error(f"Error creating project directory: {e}")
        return None


def extract_api_key_from_auth(authorization: Optional[str]) -> Optional[str]:
    """Extract API key from Authorization header."""
    if not authorization:
        return None

    # Remove "Bearer " prefix
    if authorization.startswith("Bearer "):
        return authorization[7:]
    else:
        return authorization


def generate_v2_auth_token(bot_id: str) -> str:
    """Generate authentication token for v2 API."""
    token_input = f"{MASTERKEY}:{bot_id}"
    return hashlib.md5(token_input.encode()).hexdigest()


async def fetch_bot_config(bot_id: str) -> Dict[str, Any]:
    """Fetch bot config from the backend API."""
    try:
        url = f"{BACKEND_HOST}/v1/agent_bot_config/{bot_id}"

        auth_token = generate_v2_auth_token(bot_id)
        headers = {
            "content-type": "application/json",
            "authorization": f"Bearer {auth_token}"
        }
        # Use async HTTP request
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers, timeout=30) as response:
                if response.status != 200:
                    raise HTTPException(
                        status_code=400,
                        detail=f"Failed to fetch bot config: API returned status code {response.status}"
                    )

                # Parse response
                response_data = await response.json()

                if not response_data.get("success"):
                    raise HTTPException(
                        status_code=400,
                        detail=f"Failed to fetch bot config: {response_data.get('message', 'Unknown error')}"
                    )

                return response_data.get("data", {})

    except aiohttp.ClientError as e:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to connect to backend API: {str(e)}"
        )
    except Exception as e:
        if isinstance(e, HTTPException):
            raise
        raise HTTPException(
            status_code=500,
            detail=f"Failed to fetch bot config: {str(e)}"
        )


async def _sync_call_llm(llm_config, messages) -> str:
    """Sync LLM helper function, executed in thread pool - using LangChain."""
    try:
        # Create LangChain LLM instance
        model_name = llm_config.get('model')
        model_server = llm_config.get('model_server')
        api_key = llm_config.get('api_key')
        # Detect or use specified provider
        model_provider,base_url = detect_provider(model_name,model_server)

        model_kwargs, dropped_params, default_temperature_applied = sanitize_model_kwargs(
            model_name=model_name,
            model_provider=model_provider,
            base_url=base_url,
            api_key=api_key,
            source="_sync_call_llm"
        )
        if dropped_params:
            logger.info(
                "_sync_call_llm dropped_params=%s model=%s provider=%s default_temperature_applied=%s",
                dropped_params,
                model_name,
                model_provider,
                default_temperature_applied
            )
        llm_instance = init_chat_model(**model_kwargs)

        # Convert messages to LangChain format
        langchain_messages = []
        for msg in messages:
            if msg['role'] == 'system':
                langchain_messages.append(SystemMessage(content=msg['content']))
            elif msg['role'] == 'user':
                langchain_messages.append(HumanMessage(content=msg['content']))
            elif msg['role'] == 'assistant':
                langchain_messages.append(AIMessage(content=msg['content']))
        
        # Call LangChain model
        response = await llm_instance.ainvoke(langchain_messages)

        # Return response content
        return response.content if response.content else ""

    except Exception as e:
        logger.error(f"Error calling guideline LLM with LangChain: {e}")
        return ""

def get_language_text(language: str):
    if language == "jp":
        language = "ja"
    language_map = {
        'zh': '请用中文回复',
        'en': 'Please reply in English',
        'ja': '日本語で回答してください',
    }
    return language_map.get(language.lower(), '')

def get_preamble_text(language: str, system_prompt: str):
    # First check if system_prompt has a preamble tag
    if system_prompt:
        preamble_pattern = r'<preamble>\s*(.*?)\s*</preamble>'
        preamble_matches = re.findall(preamble_pattern, system_prompt, re.DOTALL)
        if preamble_matches:
            # Extract preamble content
            preamble_content = preamble_matches[0].strip()
            if preamble_content:
                # Remove preamble tag from system_prompt
                cleaned_system_prompt = re.sub(preamble_pattern, '', system_prompt, flags=re.DOTALL)
                return preamble_content, cleaned_system_prompt

    # If no preamble block found, use default preamble choices
    if language == "jp":
        language = "ja"
    preamble_choices_map = {
        'zh': [
            "好的，让我来帮您看看。",
            "明白了，请稍等。",
            "好的，我理解了。",
            "没问题，我来处理。",
            "收到，正在为您查询。",
            "了解，让我想想。",
            "好的，我来帮您解答。",
            "明白了，稍等片刻。",
            "好的，正在处理中。",
            "了解了，让我为您分析。"
        ],
        'en': [
            "Just a moment.",
            "Got it.",
            "Let me check that for you.",
            "Sorry to hear that.",
            "Thanks for your patience.",
            "I understand.",
            "Let me help you with that.",
            "Please wait a moment.",
            "I'll look into that for you.",
            "Gotcha, let me see.",
            "Understood, one moment please.",
            "I'll help you with this.",
            "Let me figure that out.",
            "Thanks for waiting.",
            "I'll check on that."
        ],
        'ja': [
            "少々お待ちください。",
            "承知いたしました。",
            "わかりました。",
            "確認いたします。",
            "少々お時間をください。",
            "了解しました。",
            "調べてみますね。",
            "お待たせしました。",
            "対応いたします。",
            "わかりましたね。",
            "承知いたしました。",
            "確認させてください。",
            "少々お待ちいただけますか。",
            "お調べいたします。",
            "対応いたしますね。"
        ]
    };
    default_preamble = "\n".join(preamble_choices_map.get(language.lower(), []))
    return default_preamble, system_prompt  # Return default preamble and original system_prompt


async def call_preamble_llm(config: AgentConfig) -> str:
    """Call LLM to process preamble analysis.

    Args:
        messages: Message list
        preamble_choices_text: Guideline text
        model_name: Model name
        api_key: API key
        model_server: Model server URL

    Returns:
        str: Model response result
    """
    # Read preamble prompt template
    try:
        with open('./prompt/preamble_prompt.md', 'r', encoding='utf-8') as f:
            preamble_template = f.read()
    except Exception as e:
        logger.error(f"Error reading guideline prompt template: {e}")
        return ""
    
    api_key = config.api_key
    model_name = config.model_name
    model_server = config.model_server
    language = config.language
    preamble_choices_text = config.preamble_text
    last_message = get_user_last_message_content(config.messages)
    chat_history = format_messages_to_chat_history(convert_to_openai_messages(config._session_history))

    # Replace placeholders in the template
    system_prompt = preamble_template.replace('{preamble_choices_text}', preamble_choices_text).replace('{chat_history}', chat_history).replace('{last_message}', last_message).replace('{language}', get_language_text(language))
    # Configure LLM
    llm_config = {
        'model': model_name,
        'api_key': api_key,
        'model_server': model_server,  # Use the passed model_server parameter
    }

    # Call model
    messages = [{'role': 'user', 'content': system_prompt}]

    try:
        # Use semaphore to control concurrent API calls
        async with api_semaphore:
            # Call async LLM function directly
            response = await _sync_call_llm(llm_config, messages)

            # Extract content wrapped in ```json and ``` from response
            json_pattern = r'```json\s*\n(.*?)\n```'
            json_matches = re.findall(json_pattern, response, re.DOTALL)

            if json_matches:
                try:
                    # Parse the first found JSON object
                    json_data = json.loads(json_matches[0])
                    logger.info(f"Successfully processed preamble")
                    return json_data["preamble"]  # Return the parsed preamble
                except json.JSONDecodeError as e:
                    logger.error(f"Error parsing JSON from preamble analysis: {e}")
                    return ""
            else:
                logger.warning(f"No JSON format found in preamble analysis")
            return ""

    except Exception as e:
        logger.error(f"Error calling guideline LLM: {e}")
        return ""



async def call_guideline_llm(chat_history: str, guidelines_prompt: str, model_name: str, api_key: str, model_server: str) -> str:
    """Call LLM to process guideline analysis.

    Args:
        chat_history: Chat history
        guidelines_text: Guideline text
        model_name: Model name
        api_key: API key
        model_server: Model server URL
        user_identifier: User identifier

    Returns:
        str: Model response result
    """

    # Configure LLM
    llm_config = {
        'model': model_name,
        'api_key': api_key,
        'model_server': model_server,  # Use the passed model_server parameter
    }

    # Call model
    messages = [{'role': 'user', 'content': guidelines_prompt}]

    try:
        # Use semaphore to control concurrent API calls
        async with api_semaphore:
            # Call async LLM function directly
            response = await _sync_call_llm(llm_config, messages)
            return response

    except Exception as e:
        logger.error(f"Error calling guideline LLM: {e}")
        return ""


def _get_optimal_batch_size(guidelines_count: int) -> int:
    """Determine the optimal batch size (concurrency) based on guidelines count."""
    if guidelines_count <= 10:
        return 1
    elif guidelines_count <= 20:
        return 2
    elif guidelines_count <= 30:
        return 3
    else:
        return 5

def extract_block_from_system_prompt(system_prompt: str) -> tuple[str, str, str, str, List]:
    """
    Extract guideline and terms content from the system prompt.

    Args:
        system_prompt: System prompt text

    Returns:
        tuple[str, List[Dict], List[Dict]]: (cleaned system_prompt, guidelines_list, terms_list)
    """
    if not system_prompt:
        return "", [], []

    guidelines = ""
    tools = ""
    scenarios = ""

    terms_list = []

    # Parse blocks using XML tag format
    blocks_to_remove = []

    # Parse <guidelines>
    guidelines_pattern = r'<guidelines>\s*(.*?)\s*</guidelines>'
    match = re.search(guidelines_pattern, system_prompt, re.DOTALL)
    if match:
        guidelines = match.group(1).strip()
        blocks_to_remove.append(match.group(0))

    # Parse <tools>
    tools_pattern = r'<tools>\s*(.*?)\s*</tools>'
    match = re.search(tools_pattern, system_prompt, re.DOTALL)
    if match:
        tools = match.group(1).strip()
        blocks_to_remove.append(match.group(0))

    # Parse <scenarios>
    scenarios_pattern = r'<scenarios>\s*(.*?)\s*</scenarios>'
    match = re.search(scenarios_pattern, system_prompt, re.DOTALL)
    if match:
        scenarios = match.group(1).strip()
        blocks_to_remove.append(match.group(0))

    # Parse <terms>
    terms_pattern = r'<terms>\s*(.*?)\s*</terms>'
    match = re.search(terms_pattern, system_prompt, re.DOTALL)
    if match:
        try:
            terms = parse_terms_text(match.group(1).strip())
            terms_list.extend(terms)
            blocks_to_remove.append(match.group(0))
        except Exception as e:
            logger.error(f"Error parsing terms: {e}")

    # Remove parsed blocks from system_prompt
    cleaned_prompt = system_prompt
    for block in blocks_to_remove:
        cleaned_prompt = cleaned_prompt.replace(block, '', 1)

    # Clean up excess blank lines
    cleaned_prompt = re.sub(r'\n\s*\n\s*\n', '\n\n', cleaned_prompt).strip()
    return cleaned_prompt, guidelines, tools, scenarios, terms_list


def parse_terms_text(text: str) -> List[Dict[str, Any]]:
    """
    Parse terms text, supporting multiple formats.

    Args:
        text: Terms text content

    Returns:
        List[Dict]: List of terms
    """
    terms = []

    # Try to parse as JSON format
    if text.strip().startswith('[') or text.strip().startswith('{'):
        try:
            data = json.loads(text)
            if isinstance(data, list):
                for item in data:
                    if isinstance(item, dict):
                        terms.append(item)
            elif isinstance(data, dict):
                terms.append(data)
            return terms
        except json.JSONDecodeError:
            pass

    # Parse line format, supporting multiple separators
    lines = [line.strip() for line in text.split('\n') if line.strip()]

    current_term = {}

    for line in lines:
        # Skip comment lines
        if line.startswith('#') or line.startswith('//'):
            continue

        # Try to parse "1) Name: term_name1, Description: desc, Synonyms: syn1, syn2" format
        numbered_term_pattern = r'(?:\d+\)\s*)?Name:\s*([^,]+)(?:,\s*Description:\s*([^,]+))?(?:,\s*Synonyms:\s*(.+))?'
        match = re.match(numbered_term_pattern, line, re.IGNORECASE)
        if match:
            name = match.group(1).strip()
            description = match.group(2).strip() if match.group(2) else ''
            synonyms_text = match.group(3).strip() if match.group(3) else ''

            # Build term object
            term_data = {'name': name}
            if description:
                term_data['description'] = description
            if synonyms_text:
                synonyms = re.split(r'[,;|]', synonyms_text)
                term_data['synonyms'] = [s.strip() for s in synonyms if s.strip()]

            if current_term:  # Save the previous term
                terms.append(current_term)
            current_term = term_data
            continue

    # Add the last term
    if current_term:
        terms.append(current_term)

    return terms
