"""
讯飞语音评测批量处理工具

功能说明:
- 读取 Excel 文件，其中包含音频链接（userAudio 字段）和参考文本（refText 字段）
- 调用讯飞 API 对音频进行评测，获取总分和明细
- 在原 Excel 中添加"讯飞总分"和"讯飞明细"两个字段
- 输出文件命名为: {原文件名}_add_xunfei_result.xlsx
- 支持串行和并发两种处理模式

环境变量配置:
- XUNFEI_APPID: 讯飞应用 ID
- XUNFEI_API_SECRET: 讯飞 API 密钥
- XUNFEI_API_KEY: 讯飞 API Key

讯飞技术文档: https://www.xfyun.cn/doc/Ise/IseAPI.html
"""

import pandas as pd
import os
import requests
import tempfile
from pathlib import Path
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
from queue import Queue
import logging
import websocket
import datetime
import hashlib
import base64
import hmac
from urllib.parse import urlencode
import ssl
from wsgiref.handlers import format_date_time
from datetime import datetime
from time import mktime
import xml.etree.ElementTree as ET

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('xunfei_batch_processing.log'),
        logging.StreamHandler()
    ]
)

# 从 .env 文件加载环境变量
from dotenv import load_dotenv
load_dotenv()

# ==================== 全局配置 ====================
# DEBUG 模式开关（控制详细日志输出）
DEBUG_MODE = False


def debug_print(message):
    """
    DEBUG 信息输出函数

    Args:
        message (str): 要输出的调试信息
    """
    if DEBUG_MODE:
        print(f"[DEBUG] {message}")


# ==================== 讯飞 API 相关代码 ====================

class XunfeiISEAPI:
    """讯飞语音评测 API 封装类"""

    def __init__(self):
        """从环境变量读取 API 配置"""
        self.host_url = "ws://ise-api.xfyun.cn/v2/open-ise"
        self.appid = os.environ.get('XUNFEI_APPID', '')
        self.api_secret = os.environ.get('XUNFEI_API_SECRET', '')
        self.api_key = os.environ.get('XUNFEI_API_KEY', '')

        # 检查环境变量是否配置
        if not all([self.appid, self.api_secret, self.api_key]):
            raise ValueError(
                "请配置讯飞 API 环境变量: XUNFEI_APPID, XUNFEI_API_SECRET, XUNFEI_API_KEY"
            )

        self.result = None
        self.error = None

    def _detect_audio_format(self, audio_file_path):
        """检测音频文件格式"""
        try:
            # 通过文件扩展名检测
            file_ext = os.path.splitext(audio_file_path)[1].lower()
            if file_ext == '.wav':
                return 'wav'
            elif file_ext == '.mp3':
                return 'mp3'

            # 通过文件头检测
            with open(audio_file_path, 'rb') as f:
                header = f.read(12)
                if len(header) >= 12:
                    # WAV文件头: RIFF....WAVE
                    if header[:4] == b'RIFF' and header[8:12] == b'WAVE':
                        return 'wav'
                    # MP3文件头: ID3 或 0xFF 0xFB/0xFA
                    elif header[:3] == b'ID3' or (header[0] == 0xFF and (header[1] & 0xE0) == 0xE0):
                        return 'mp3'

            # 默认返回wav
            return 'wav'
        except Exception as e:
            print(f"[WARNING] 音频格式检测失败: {str(e)}, 默认使用WAV格式")
            return 'wav'

    def _remove_wav_header(self, audio_file_path):
        """去除WAV文件头部，返回纯音频数据"""
        try:
            with open(audio_file_path, 'rb') as f:
                # 读取WAV文件头
                riff_header = f.read(12)  # RIFF header (12 bytes)
                if len(riff_header) < 12 or riff_header[:4] != b'RIFF' or riff_header[8:12] != b'WAVE':
                    print(f"[WARNING] 不是有效的WAV文件，直接返回原始数据")
                    f.seek(0)
                    return f.read()

                # 跳过format chunk
                while True:
                    chunk_header = f.read(8)
                    if len(chunk_header) < 8:
                        break

                    chunk_id = chunk_header[:4]
                    chunk_size = int.from_bytes(chunk_header[4:8], byteorder='little')

                    if chunk_id == b'data':
                        # 找到data chunk，返回音频数据
                        audio_data = f.read(chunk_size)
                        debug_print(f"WAV头部已去除，音频数据大小: {len(audio_data)} bytes")
                        return audio_data
                    else:
                        # 跳过其他chunk
                        f.seek(chunk_size, 1)
                        if chunk_size % 2:  # 如果chunk大小是奇数，需要跳过一个字节对齐
                            f.seek(1, 1)

                # 如果没找到data chunk，返回从当前位置开始的所有数据
                print(f"[WARNING] 未找到data chunk，返回剩余数据")
                return f.read()

        except Exception as e:
            print(f"[ERROR] WAV头部处理失败: {str(e)}, 返回原始文件数据")
            with open(audio_file_path, 'rb') as f:
                return f.read()

    def _generate_url(self):
        """生成WebSocket连接URL"""
        now_time = datetime.now()
        now_date = format_date_time(mktime(now_time.timetuple()))

        # 拼接鉴权原始字符串
        origin_base = "host: " + "ise-api.xfyun.cn" + "\n"
        origin_base += "date: " + now_date + "\n"
        origin_base += "GET " + "/v2/open-ise " + "HTTP/1.1"

        # sha256加密
        signature_sha = hmac.new(self.api_secret.encode('utf-8'), origin_base.encode('utf-8'),
                                 digestmod=hashlib.sha256).digest()
        signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')

        authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
            self.api_key, "hmac-sha256", "host date request-line", signature_sha)
        authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')

        # 将请求的鉴权参数组合为字典
        dict_data = {
            "authorization": authorization,
            "date": now_date,
            "host": "ise-api.xfyun.cn"
        }
        ws_url = self.host_url + '?' + urlencode(dict_data)
        return ws_url

    def _on_message(self, ws, message):
        """处理WebSocket消息"""
        try:
            debug_print(f"收到消息: {message}")
            response = json.loads(message)
            debug_print(f"解析后的响应: {json.dumps(response, indent=2, ensure_ascii=False)}")

            # 检查响应结构
            if "data" not in response:
                print(f"[ERROR] 响应中缺少 'data' 字段")
                self.error = f"响应格式错误: 缺少 'data' 字段"
                ws.close()
                return

            data = response["data"]
            if "status" not in data:
                print(f"[ERROR] data 中缺少 'status' 字段")
                self.error = f"响应格式错误: 缺少 'status' 字段"
                ws.close()
                return

            status = data["status"]
            debug_print(f"状态码: {status}")

            if status == 2:  # 评测完成
                if "data" not in data:
                    print(f"[ERROR] data 中缺少评测结果数据")
                    self.error = f"响应格式错误: 缺少评测结果数据"
                    ws.close()
                    return

                xml_data = base64.b64decode(data["data"])
                xml_string = xml_data.decode("utf-8")
                debug_print(f"解码后的XML: {xml_string}")
                self.result = self._parse_xml_result(xml_string)
                debug_print(f"解析后的结果: {json.dumps(self.result, indent=2, ensure_ascii=False)}")
                ws.close()
        except json.JSONDecodeError as e:
            print(f"[ERROR] JSON解析失败: {str(e)}")
            print(f"[ERROR] 原始消息: {message}")
            self.error = f"JSON解析错误: {str(e)}"
            ws.close()
        except Exception as e:
            print(f"[ERROR] 消息处理异常: {str(e)}")
            print(f"[ERROR] 异常类型: {type(e).__name__}")
            print(f"[ERROR] 原始消息: {message}")
            self.error = f"消息处理错误: {str(e)}"
            ws.close()

    def _on_error(self, ws, error):
        """处理WebSocket错误"""
        print(f"[ERROR] WebSocket错误: {str(error)}")
        print(f"[ERROR] 错误类型: {type(error).__name__}")
        self.error = f"WebSocket错误: {str(error)}"

    def _on_close(self, ws, reason, res):
        """WebSocket连接关闭"""
        debug_print(f"WebSocket连接关闭 - 原因: {reason}, 响应: {res}")
        pass

    def _on_open(self, ws, audio_file, text="nice to meet you."):
        """WebSocket连接打开，发送音频数据"""
        try:
            debug_print("WebSocket连接已打开")
            debug_print(f"音频文件: {audio_file}")
            debug_print(f"评测文本: {text}")

            # 检测音频格式
            audio_format = self._detect_audio_format(audio_file)
            debug_print(f"检测到音频格式: {audio_format}")

            # 根据音频格式设置aue参数
            if audio_format == 'wav':
                aue_param = "raw"  # WAV文件使用raw
            else:  # mp3
                aue_param = "lame"  # MP3文件使用lame

            debug_print(f"使用aue参数: {aue_param}")

            # 发送初始配置
            send_dict = {
                "common": {
                    "app_id": self.appid
                },
                "business": {
                    "category": "read_sentence",
                    "rstcd": "utf8",
                    "sub": "ise",
                    "group": "pupil",
                    "ent": "en_vip",
                    "tte": "utf-8",
                    "cmd": "ssb",
                    "auf": "audio/L16;rate=16000",
                    "aue": aue_param,
                    "text": '\uFEFF' + f"[content]\n{text}",
                    "ise_unite": "1",
                    "extra_ability": "pitch"
                },
                "data": {
                    "status": 0,
                    "data": ""
                }
            }
            debug_print(f"发送初始配置: {json.dumps(send_dict, indent=2, ensure_ascii=False)}")
            ws.send(json.dumps(send_dict))

            # 根据音频格式处理音频数据
            if audio_format == 'wav':
                # WAV文件需要去除头部
                audio_data = self._remove_wav_header(audio_file)
                debug_print(f"WAV文件头部已去除，音频数据大小: {len(audio_data)} bytes")
            else:
                # MP3文件直接读取
                with open(audio_file, "rb") as f:
                    audio_data = f.read()
                debug_print(f"MP3文件直接读取，音频数据大小: {len(audio_data)} bytes")

            # 优化音频发送逻辑
            frame_count = 0
            data_size = len(audio_data)

            # 根据数据大小动态调整缓冲区大小和延迟
            if data_size > 50000:  # 大于50KB的数据使用更大的缓冲区
                buffer_size = 12800  # 20倍缓冲区
                sleep_time = 0.02   # 减少延迟到20ms
            else:
                buffer_size = 1280  # 原始缓冲区
                sleep_time = 0.01   # 小文件使用更小延迟

            debug_print(f"使用缓冲区大小: {buffer_size}, 延迟: {sleep_time}s")

            # 发送音频数据
            offset = 0
            while offset < data_size:
                # 读取缓冲区大小的数据
                buffer = audio_data[offset:offset + buffer_size]
                offset += len(buffer)

                if offset >= data_size:
                    # 发送最后一帧
                    my_dict = {
                        "business": {"cmd": "auw", "aus": 4, "aue": aue_param},
                        "data": {"status": 2, "data": str(base64.b64encode(buffer).decode())}
                    }
                    debug_print("发送最后一帧")
                    ws.send(json.dumps(my_dict))
                    break

                # 发送中间帧
                send_dict = {
                    "business": {
                        "cmd": "auw",
                        "aus": 1,
                        "aue": aue_param
                    },
                    "data": {
                        "status": 1,
                        "data": str(base64.b64encode(buffer).decode()),
                        "data_type": 1,
                        "encoding": "raw"
                    }
                }
                frame_count += 1
                if frame_count % 20 == 0:  # 减少日志频率
                    debug_print(f"已发送 {frame_count} 帧音频数据")
                ws.send(json.dumps(send_dict))
                time.sleep(sleep_time)  # 使用动态延迟

            debug_print(f"音频发送完成，总共发送 {frame_count} 帧")

        except Exception as e:
            print(f"[ERROR] 音频发送异常: {str(e)}")
            print(f"[ERROR] 异常类型: {type(e).__name__}")
            self.error = f"音频发送错误: {str(e)}"
            ws.close()

    def _parse_xml_result(self, xml_string):
        """解析XML评测结果"""
        try:
            root = ET.fromstring(xml_string)

            result = {
                "total_score": 0,
                "words": [],
                "sentences": []
            }

            # 解析句子级别评分
            for sentence in root.findall('.//sentence'):
                sentence_info = {
                    "content": sentence.get('content', ''),
                    "total_score": float(sentence.get('total_score', 0)),
                    "fluency_score": float(sentence.get('fluency_score', 0)),
                    "integrity_score": float(sentence.get('integrity_score', 0)),
                    "phone_score": float(sentence.get('phone_score', 0))
                }
                result["sentences"].append(sentence_info)
                result["total_score"] = sentence_info["total_score"]

            # 解析单词级别评分
            for word in root.findall('.//word'):
                word_info = {
                    "content": word.get('content', ''),
                    "total_score": float(word.get('total_score', 0)),
                    "dp_message": int(word.get('dp_message', 0)),
                    "time_len": int(word.get('time_len', 0)),
                    "syllables": []
                }

                # 解析音节评分
                for syllable in word.findall('.//syllable'):
                    syllable_info = {
                        "content": syllable.get('content', ''),
                        "total_score": float(syllable.get('total_score', 0)),
                        "phones": []
                    }

                    # 解析音素评分
                    for phone in syllable.findall('.//phone'):
                        phone_info = {
                            "content": phone.get('content', ''),
                            "total_score": float(phone.get('total_score', 0)),
                            "dp_message": int(phone.get('dp_message', 0))
                        }
                        syllable_info["phones"].append(phone_info)

                    word_info["syllables"].append(syllable_info)

                result["words"].append(word_info)

            return result

        except Exception as e:
            return {"error": f"XML解析错误: {str(e)}"}

    def evaluate_audio(self, audio_file_path, text="nice to meet you.", timeout=30):
        """
        评测音频文件

        Args:
            audio_file_path (str): 音频文件路径
            text (str): 评测文本内容
            timeout (int): 超时时间（秒）

        Returns:
            dict: 评测结果JSON
        """
        debug_print(f"开始评测音频文件: {audio_file_path}")
        debug_print(f"评测文本: {text}")

        # 检查音频文件是否存在
        if not os.path.exists(audio_file_path):
            error_msg = f"音频文件不存在: {audio_file_path}"
            print(f"[ERROR] {error_msg}")
            return {"error": error_msg}

        # 重置结果
        self.result = None
        self.error = None

        try:
            # 生成WebSocket URL
            ws_url = self._generate_url()
            debug_print(f"WebSocket URL: {ws_url}")

            # 创建WebSocket连接
            websocket.enableTrace(False)
            ws = websocket.WebSocketApp(
                ws_url,
                on_message=self._on_message,
                on_error=self._on_error,
                on_close=self._on_close,
                on_open=lambda ws: self._on_open(ws, audio_file_path, text)
            )

            debug_print("开始WebSocket连接...")
            # 运行WebSocket连接
            ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})

            debug_print("WebSocket连接结束")
            # 返回结果
            if self.error:
                print(f"[ERROR] 评测失败: {self.error}")
                return {"error": self.error}
            elif self.result:
                debug_print("评测成功")
                return self.result
            else:
                error_msg = "未收到评测结果"
                print(f"[ERROR] {error_msg}")
                return {"error": error_msg}

        except Exception as e:
            error_msg = f"评测过程出错: {str(e)}"
            print(f"[ERROR] {error_msg}")
            print(f"[ERROR] 异常类型: {type(e).__name__}")
            return {"error": error_msg}


def evaluate_audio_file(audio_file_path, text="nice to meet you."):
    """
    简化的音频评测函数

    Args:
        audio_file_path (str): 音频文件路径
        text (str): 评测文本内容

    Returns:
        dict: 评测结果JSON
    """
    api = XunfeiISEAPI()
    return api.evaluate_audio(audio_file_path, text)


# ==================== 批量处理相关代码 ====================

def download_audio_file(audio_url, temp_dir, max_retries=3, timeout=30):
    """
    下载音频文件到临时目录（增强版本）

    Args:
        audio_url (str): 音频文件URL
        temp_dir (str): 临时目录路径
        max_retries (int): 最大重试次数
        timeout (int): 请求超时时间（秒）

    Returns:
        str: 下载的音频文件路径，失败返回None
    """
    if not audio_url or pd.isna(audio_url):
        logging.warning("音频URL为空或无效")
        return None

    # 从URL中提取文件名
    try:
        file_name = os.path.basename(audio_url.split('?')[0])  # 去除URL参数
        if not file_name or '.' not in file_name:
            file_name = f"audio_{hash(audio_url) % 100000}.wav"  # 生成默认文件名

        file_path = os.path.join(temp_dir, file_name)

        # 重试机制
        for attempt in range(max_retries):
            try:
                logging.info(f"正在下载音频文件 (尝试 {attempt + 1}/{max_retries}): {audio_url}")

                # 设置请求头，模拟浏览器
                headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
                }

                response = requests.get(audio_url, timeout=timeout, headers=headers, stream=True)
                response.raise_for_status()

                # 检查内容类型
                content_type = response.headers.get('content-type', '')
                if not any(audio_type in content_type.lower() for audio_type in ['audio', 'wav', 'mp3', 'ogg', 'flac']):
                    logging.warning(f"可能不是音频文件，Content-Type: {content_type}")

                # 写入文件
                with open(file_path, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)

                # 验证文件大小
                file_size = os.path.getsize(file_path)
                if file_size == 0:
                    raise ValueError("下载的文件为空")

                logging.info(f"音频文件下载成功: {file_path} (大小: {file_size} bytes)")
                return file_path

            except requests.exceptions.Timeout:
                logging.warning(f"下载超时 (尝试 {attempt + 1}/{max_retries}): {audio_url}")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # 指数退避
                    continue
            except requests.exceptions.RequestException as e:
                logging.warning(f"下载请求异常 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
            except Exception as e:
                logging.error(f"下载过程中发生未知错误 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue

        logging.error(f"音频文件下载失败，已达到最大重试次数: {audio_url}")
        return None

    except Exception as e:
        logging.error(f"下载音频文件时发生异常: {str(e)}")
        return None


def format_xunfei_details(xunfei_result):
    """
    格式化讯飞评测结果为明细字符串

    Args:
        xunfei_result (dict): 讯飞API返回的结果

    Returns:
        str: 格式化的明细字符串
    """
    if not xunfei_result or 'error' in xunfei_result:
        return ""

    try:
        words = xunfei_result.get('words', [])
        if not words:
            return ""

        details = []
        for word in words:
            content = word.get('content', '')
            total_score = word.get('total_score', 0)
            details.append(f"{content} {int(total_score)}")

        return "\n".join(details)

    except Exception as e:
        print(f"格式化讯飞明细失败: {str(e)}")
        return ""


def get_xunfei_total_score(xunfei_result):
    """
    获取讯飞评测总分

    Args:
        xunfei_result (dict): 讯飞API返回的结果

    Returns:
        int: 总分，失败返回0
    """
    if not xunfei_result or 'error' in xunfei_result:
        return 0

    try:
        return int(xunfei_result.get('total_score', 0))
    except Exception as e:
        print(f"获取讯飞总分失败: {str(e)}")
        return 0


def process_single_row(row_data, temp_dir, results_dict, lock, rate_limiter=None):
    """
    处理单行数据（并发版本，增强错误处理和时间分析）

    Args:
        row_data (tuple): (index, row) 数据
        temp_dir (str): 临时目录路径
        results_dict (dict): 结果字典
        lock (threading.Lock): 线程锁
        rate_limiter (Queue): 速率限制器

    Returns:
        None
    """
    index, row = row_data
    start_time = time.time()
    timing_info = {}

    try:
        # 1. 速率限制等待时间
        rate_limit_start = time.time()
        if rate_limiter:
            rate_limiter.get()  # 获取令牌
        timing_info['rate_limit_wait'] = time.time() - rate_limit_start

        logging.info(f"开始处理第 {index + 1} 行数据")

        # 2. 数据预处理时间
        preprocess_start = time.time()
        ref_text = str(row['refText']) if pd.notna(row['refText']) else ""
        audio_url = str(row['userAudio']) if pd.notna(row['userAudio']) else ""

        # 数据验证
        if not ref_text:
            raise ValueError("refText 为空或无效")

        if not audio_url:
            raise ValueError("userAudio 为空或无效")
        timing_info['preprocess'] = time.time() - preprocess_start

        # 3. 音频下载时间
        download_start = time.time()
        audio_file_path = download_audio_file(audio_url, temp_dir)
        timing_info['audio_download'] = time.time() - download_start

        if not audio_file_path:
            raise ValueError("音频文件下载失败")

        try:
            # 4. 讯飞API调用时间
            api_start = time.time()
            logging.info(f"正在调用讯飞API评测: {ref_text}")
            xunfei_result = evaluate_audio_file(audio_file_path, ref_text)
            timing_info['api_call'] = time.time() - api_start

            if not xunfei_result:
                raise ValueError("讯飞API返回空结果")

            # 5. 结果处理时间
            result_process_start = time.time()
            xunfei_details = format_xunfei_details(xunfei_result)
            xunfei_total_score = get_xunfei_total_score(xunfei_result)
            timing_info['result_process'] = time.time() - result_process_start

            # 6. 数据更新时间
            update_start = time.time()
            with lock:
                results_dict[index] = {
                    '讯飞总分': xunfei_total_score,
                    '讯飞明细': xunfei_details
                }
            timing_info['data_update'] = time.time() - update_start

            # 计算总耗时
            total_time = time.time() - start_time
            timing_info['total'] = total_time

            # 详细的时间分析日志
            logging.info(f"第 {index + 1} 行处理成功 - 总分: {xunfei_total_score} | "
                        f"总耗时: {total_time:.2f}s | "
                        f"速率等待: {timing_info['rate_limit_wait']:.2f}s | "
                        f"预处理: {timing_info['preprocess']:.3f}s | "
                        f"音频下载: {timing_info['audio_download']:.2f}s | "
                        f"API调用: {timing_info['api_call']:.2f}s | "
                        f"结果处理: {timing_info['result_process']:.3f}s | "
                        f"数据更新: {timing_info['data_update']:.3f}s")

        except Exception as api_error:
            total_time = time.time() - start_time
            logging.error(f"第 {index + 1} 行讯飞API调用失败: {str(api_error)} | "
                         f"总耗时: {total_time:.2f}s | "
                         f"音频下载: {timing_info.get('audio_download', 0):.2f}s | "
                         f"API调用: {timing_info.get('api_call', 0):.2f}s")
            with lock:
                results_dict[index] = {
                    '讯飞总分': 0,
                    '讯飞明细': "",
                    'error': f'API调用失败: {str(api_error)}'
                }

        finally:
            # 7. 清理时间
            cleanup_start = time.time()
            try:
                if audio_file_path and os.path.exists(audio_file_path):
                    os.remove(audio_file_path)
                    logging.debug(f"已删除临时文件: {audio_file_path}")
            except Exception as cleanup_error:
                logging.warning(f"清理临时文件失败: {str(cleanup_error)}")
            timing_info['cleanup'] = time.time() - cleanup_start

            # 释放速率限制令牌
            if rate_limiter:
                try:
                    rate_limiter.put(None, timeout=1)  # 归还令牌
                except:
                    pass  # 队列可能已满，忽略

    except Exception as e:
        total_time = time.time() - start_time
        logging.error(f"第 {index + 1} 行处理异常: {str(e)} | 总耗时: {total_time:.2f}s")
        with lock:
            results_dict[index] = {
                '讯飞总分': 0,
                '讯飞明细': "",
                'error': f'处理异常: {str(e)}'
            }

        # 释放速率限制令牌
        if rate_limiter:
            try:
                rate_limiter.put(None, timeout=1)
            except:
                pass


def process_excel_with_xunfei_concurrent(input_file_path, output_dir="output/audio", max_workers=5, rate_limit_per_second=5):
    """
    处理Excel文件，添加讯飞评测结果（并发版本，增强控制）

    Args:
        input_file_path (str): 输入Excel文件路径
        output_dir (str): 输出目录路径，默认为 output/audio
        max_workers (int): 最大并发线程数，默认5
        rate_limit_per_second (int): 每秒最大请求数，默认5

    Returns:
        bool: 处理是否成功
    """
    start_time = time.time()

    try:
        # 读取Excel文件
        logging.info(f"正在读取Excel文件: {input_file_path}")
        df = pd.read_excel(input_file_path)

        # 检查必要的列是否存在
        required_columns = ['refText', 'userAudio']
        missing_columns = [col for col in required_columns if col not in df.columns]
        if missing_columns:
            logging.error(f"Excel文件缺少必要的列: {missing_columns}")
            return False

        # 数据预处理和验证
        total_rows = len(df)
        valid_rows = 0
        for index, row in df.iterrows():
            if pd.notna(row.get('refText')) and pd.notna(row.get('userAudio')):
                valid_rows += 1

        logging.info(f"总行数: {total_rows}, 有效行数: {valid_rows}")

        if valid_rows == 0:
            logging.warning("没有找到有效的数据行")
            return False

        # 添加新列
        df['讯飞总分'] = 0
        df['讯飞明细'] = ""

        # 创建优化的速率限制器
        effective_rate_limit = max(rate_limit_per_second, max_workers)
        rate_limiter = Queue(maxsize=effective_rate_limit * 2)

        # 预填充令牌
        for _ in range(effective_rate_limit):
            rate_limiter.put(None)

        # 启动优化的速率限制器补充线程
        def rate_limiter_refill():
            interval = 1.0 / effective_rate_limit
            while True:
                time.sleep(interval)
                try:
                    rate_limiter.put(None, block=False)
                except:
                    pass

        rate_thread = threading.Thread(target=rate_limiter_refill, daemon=True)
        rate_thread.start()

        logging.info(f"速率限制设置: {effective_rate_limit} req/s (原始: {rate_limit_per_second}, 队列大小: {effective_rate_limit * 2})")

        # 创建临时目录用于下载音频文件
        with tempfile.TemporaryDirectory() as temp_dir:
            logging.info(f"创建临时目录: {temp_dir}")
            logging.info(f"开始并发处理，最大并发数: {max_workers}, 有效速率限制: {effective_rate_limit} req/s")

            # 准备数据
            row_data_list = [(index, row) for index, row in df.iterrows()]

            # 创建结果字典和线程锁
            results_dict = {}
            lock = threading.Lock()

            # 使用线程池进行并发处理
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                # 提交所有任务
                future_to_index = {
                    executor.submit(process_single_row, row_data, temp_dir, results_dict, lock, rate_limiter): row_data[0]
                    for row_data in row_data_list
                }

                # 等待任务完成并显示进度
                completed_count = 0
                success_count = 0
                error_count = 0

                for future in as_completed(future_to_index):
                    completed_count += 1
                    index = future_to_index[future]

                    try:
                        future.result()  # 获取结果，如果有异常会抛出

                        # 检查处理结果
                        with lock:
                            result = results_dict.get(index, {})
                            if result.get('error') is None:
                                success_count += 1
                            else:
                                error_count += 1

                        # 显示进度
                        if completed_count % 10 == 0 or completed_count == total_rows:
                            elapsed_time = time.time() - start_time
                            avg_time_per_item = elapsed_time / completed_count
                            remaining_time = avg_time_per_item * (total_rows - completed_count)

                            logging.info(f"进度: {completed_count}/{total_rows} ({completed_count/total_rows*100:.1f}%) "
                                       f"成功: {success_count}, 失败: {error_count}, "
                                       f"预计剩余时间: {remaining_time:.1f}秒")

                    except Exception as e:
                        error_count += 1
                        logging.error(f"任务 {index + 1} 执行异常: {str(e)}")
                        with lock:
                            if index not in results_dict:
                                results_dict[index] = {
                                    '讯飞总分': 0,
                                    '讯飞明细': "",
                                    'error': f'任务执行异常: {str(e)}'
                                }

            # 将结果更新到DataFrame
            logging.info("正在更新结果到DataFrame...")
            for index in results_dict:
                result = results_dict[index]
                df.at[index, '讯飞总分'] = result.get('讯飞总分', 0)
                df.at[index, '讯飞明细'] = result.get('讯飞明细', "")

                # 如果有错误，可以选择记录到备注列（如果存在）
                if result.get('error') and '备注' in df.columns:
                    existing_note = str(df.at[index, '备注']) if pd.notna(df.at[index, '备注']) else ""
                    error_note = f"讯飞API错误: {result['error']}"
                    df.at[index, '备注'] = f"{existing_note}\n{error_note}".strip()

        # 创建输出目录
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        # 生成输出文件路径
        input_path = Path(input_file_path)
        output_file_path = output_path / f"{input_path.stem}_add_xunfei_result.xlsx"

        # 保存结果
        logging.info(f"正在保存结果到: {output_file_path}")
        df.to_excel(output_file_path, index=False)

        # 计算总耗时
        total_time = time.time() - start_time

        # 统计处理结果
        final_success_count = sum(1 for result in results_dict.values() if result.get('error') is None)
        final_error_count = len(results_dict) - final_success_count

        logging.info("=" * 50)
        logging.info("并发处理完成！")
        logging.info(f"处理统计: 成功 {final_success_count} 条，失败 {final_error_count} 条，总计 {len(results_dict)} 条")
        logging.info(f"总耗时: {total_time:.2f} 秒")
        logging.info(f"平均处理时间: {total_time/len(results_dict):.2f} 秒/条")
        logging.info(f"输出文件: {output_file_path}")
        logging.info("=" * 50)

        return True

    except Exception as e:
        logging.error(f"处理Excel文件时出错: {str(e)}")
        return False


def process_excel_with_xunfei(input_file_path, output_dir="output/audio"):
    """
    处理Excel文件，添加讯飞评测结果（串行版本）

    Args:
        input_file_path (str): 输入Excel文件路径
        output_dir (str): 输出目录路径，默认为 output/audio

    Returns:
        bool: 处理是否成功
    """
    try:
        # 读取Excel文件
        print(f"正在读取Excel文件: {input_file_path}")
        df = pd.read_excel(input_file_path)

        # 检查必要的列是否存在
        required_columns = ['refText', 'userAudio']
        missing_columns = [col for col in required_columns if col not in df.columns]
        if missing_columns:
            print(f"错误: Excel文件缺少必要的列: {missing_columns}")
            return False

        # 添加新列
        df['讯飞总分'] = 0
        df['讯飞明细'] = ""

        # 创建临时目录用于下载音频文件
        with tempfile.TemporaryDirectory() as temp_dir:
            print(f"创建临时目录: {temp_dir}")

            # 处理每一行数据
            total_rows = len(df)
            for index, row in df.iterrows():
                print(f"\n处理进度: {index + 1}/{total_rows}")

                ref_text = str(row['refText']) if pd.notna(row['refText']) else ""
                audio_url = str(row['userAudio']) if pd.notna(row['userAudio']) else ""

                if not ref_text or not audio_url:
                    print(f"第 {index + 1} 行数据不完整，跳过")
                    continue

                print(f"参考文本: {ref_text}")
                print(f"音频URL: {audio_url}")

                # 下载音频文件
                audio_file_path = download_audio_file(audio_url, temp_dir)
                if not audio_file_path:
                    print(f"第 {index + 1} 行音频下载失败，跳过")
                    continue

                # 调用讯飞API进行评测
                print("正在调用讯飞API进行评测...")
                try:
                    xunfei_result = evaluate_audio_file(audio_file_path, ref_text)
                    print(f"讯飞API返回结果: {json.dumps(xunfei_result, indent=2, ensure_ascii=False)}")

                    # 提取总分和明细
                    total_score = get_xunfei_total_score(xunfei_result)
                    details = format_xunfei_details(xunfei_result)

                    # 更新DataFrame
                    df.at[index, '讯飞总分'] = total_score
                    df.at[index, '讯飞明细'] = details

                    print(f"讯飞总分: {total_score}")
                    print(f"讯飞明细: {details}")

                except Exception as e:
                    print(f"第 {index + 1} 行讯飞API调用失败: {str(e)}")
                    continue

                # 删除临时音频文件
                try:
                    os.remove(audio_file_path)
                except:
                    pass

                # 添加延时避免API调用过于频繁
                time.sleep(1)

        # 创建输出目录
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        # 生成输出文件路径
        input_path = Path(input_file_path)
        output_file_path = output_path / f"{input_path.stem}_add_xunfei_result.xlsx"

        # 保存结果
        print(f"\n正在保存结果到: {output_file_path}")
        df.to_excel(output_file_path, index=False)
        print("处理完成！")

        return True

    except Exception as e:
        print(f"处理Excel文件时出错: {str(e)}")
        return False


if __name__ == "__main__":
    # ==================== 配置参数 ====================
    input_file = "user_audio_data_20251210_152807_sample.xlsx"
    output_directory = "output/audio"  # 输出目录，可以修改
    use_concurrent = True  # True: 使用并发版本，False: 使用串行版本

    # DEBUG 模式开关（True: 显示详细调试信息，False: 仅显示关键信息）
    enable_debug = False  # 可以设置为 True 来查看详细的 DEBUG 日志

    # 设置全局 DEBUG_MODE
    globals()['DEBUG_MODE'] = enable_debug

    # 检查环境变量
    required_env_vars = ['XUNFEI_APPID', 'XUNFEI_API_SECRET', 'XUNFEI_API_KEY']
    missing_vars = [var for var in required_env_vars if not os.environ.get(var)]

    if missing_vars:
        print(f"错误: 缺少必要的环境变量: {missing_vars}")
        print("请在 .env 文件或系统环境变量中配置:")
        print("  XUNFEI_APPID=你的应用ID")
        print("  XUNFEI_API_SECRET=你的API密钥")
        print("  XUNFEI_API_KEY=你的API Key")
    elif not os.path.exists(input_file):
        print(f"文件不存在: {input_file}")
        print("请确保Excel文件存在并包含 'refText' 和 'userAudio' 列")
    else:
        if use_concurrent:
            print("使用并发版本处理（5路并发，5 req/s）...")
            success = process_excel_with_xunfei_concurrent(
                input_file,
                output_dir=output_directory,
                max_workers=5,
                rate_limit_per_second=5
            )
        else:
            print("使用串行版本处理...")
            success = process_excel_with_xunfei(input_file, output_dir=output_directory)

        if success:
            print("处理成功！")
        else:
            print("处理失败！")
