import os
import json
import logging
import re
import uuid
import boto3
import requests
from botocore.config import Config
from io import BytesIO
from datetime import datetime
from openai import OpenAI
from typing import Dict, Any
from app.core.data_parse.time_utils import get_east_asia_time_str, get_east_asia_timestamp, get_east_asia_isoformat, get_east_asia_date_str

# 导入配置和业务逻辑模块
from app.config.config import DevelopmentConfig, ProductionConfig
from app.core.data_parse.parse_system import (
    BusinessCard, check_duplicate_business_card, 
    create_main_card_with_duplicates, update_career_path,
    normalize_mobile_numbers,
    create_origin_source_entry, update_origin_source
)
from app.models.parse_models import ParseTaskRepository
from app import db

# 使用配置变量,缺省认为在生产环境运行
config = ProductionConfig()
# 使用配置变量
minio_url = f"{'https' if config.MINIO_SECURE else 'http'}://{config.MINIO_HOST}"
minio_access_key = config.MINIO_USER
minio_secret_key = config.MINIO_PASSWORD
minio_bucket = config.MINIO_BUCKET
use_ssl = config.MINIO_SECURE


def get_minio_client():
    """获取MinIO客户端连接"""
    try:
        logging.info(f"尝试连接MinIO服务器: {minio_url}")
        
        minio_client = boto3.client(
            's3',
            endpoint_url=minio_url,
            aws_access_key_id=minio_access_key,
            aws_secret_access_key=minio_secret_key,
            config=Config(
                signature_version='s3v4',
                retries={'max_attempts': 3, 'mode': 'standard'},
                connect_timeout=10,
                read_timeout=30
            )
        )
        
        # 确保存储桶存在
        buckets = minio_client.list_buckets()
        bucket_names = [bucket['Name'] for bucket in buckets.get('Buckets', [])]
        logging.info(f"成功连接到MinIO服务器，现有存储桶: {bucket_names}")
        
        if minio_bucket not in bucket_names:
            logging.info(f"创建存储桶: {minio_bucket}")
            minio_client.create_bucket(Bucket=minio_bucket)
            
        return minio_client
    except Exception as e:
        logging.error(f"MinIO连接错误: {str(e)}")
        return None


def upload_md_to_minio(web_md, filename=None):
    """
    将markdown文本上传到MinIO
    
    Args:
        web_md (str): markdown格式的文本内容
        filename (str, optional): 指定的文件名，如果不提供则自动生成
        
    Returns:
        str: MinIO中的文件路径，如果上传失败返回None
    """
    try:
        # 生成文件名
        if not filename:
            timestamp = get_east_asia_timestamp()
            unique_id = uuid.uuid4().hex[:8]
            filename = f"webpage_talent_{timestamp}_{unique_id}.md"
        elif not filename.endswith('.md'):
            filename += '.md'
        
        # 获取MinIO客户端
        minio_client = get_minio_client()
        if not minio_client:
            logging.error("无法获取MinIO客户端")
            return None
        
        # 将文本转换为字节流
        md_bytes = web_md.encode('utf-8')
        md_stream = BytesIO(md_bytes)
        
        # 上传到MinIO
        minio_path = f"webpage_talent/{filename}"
        logging.info(f"开始上传MD文件到MinIO: {minio_path}")
        
        minio_client.put_object(
            Bucket=minio_bucket,
            Key=minio_path,
            Body=md_stream,
            ContentType='text/markdown',
            Metadata={
                'original_filename': filename,
                'upload_time': get_east_asia_isoformat(),
                'content_type': 'webpage_talent_md'
            }
        )
        
        logging.info(f"MD文件成功上传到MinIO: {minio_path}")
        return minio_path
        
    except Exception as e:
        logging.error(f"上传MD文件到MinIO失败: {str(e)}", exc_info=True)
        return None


def add_webpage_talent(talent_list, web_md):
    """
    添加网页人才信息，包括保存网页内容和创建名片记录
    
    Args:
        talent_list (list): 人才信息列表，每个item包含业务卡片格式的数据
        web_md (str): 网页markdown文本内容
        
    Returns:
        dict: 处理结果，包含成功和失败的记录统计
    """
    try:
        # 参数验证
        if not talent_list or not isinstance(talent_list, list):
            return {
                'code': 400,
                'success': False,
                'message': 'talent_list参数必须是非空数组',
                'data': None
            }
        
        if not web_md or not isinstance(web_md, str):
            return {
                'code': 400,
                'success': False,
                'message': 'web_md参数必须是非空字符串',
                'data': None
            }
        
        # 上传markdown文件到MinIO
        logging.info("开始上传网页内容到MinIO")
        minio_md_path = upload_md_to_minio(web_md)
        
        if not minio_md_path:
            return {
                'code': 500,
                'success': False,
                'message': '上传网页内容到MinIO失败',
                'data': None
            }
        
        # 处理结果统计
        results = {
            'total_count': len(talent_list),
            'success_count': 0,
            'failed_count': 0,
            'success_records': [],
            'failed_records': [],
            'minio_md_path': minio_md_path
        }
        
        # 循环处理每个人才记录
        for index, talent_data in enumerate(talent_list):
            try:
                logging.info(f"开始处理第{index + 1}个人才记录: {talent_data.get('name_zh', 'Unknown')}")
                
                # 验证必要字段
                if not talent_data.get('name_zh'):
                    error_msg = f"第{index + 1}个记录缺少name_zh字段"
                    logging.warning(error_msg)
                    results['failed_records'].append({
                        'index': index + 1,
                        'data': talent_data,
                        'error': error_msg
                    })
                    results['failed_count'] += 1
                    continue
                
                # 设置origin_source为原始资料记录
                talent_data['origin_source'] = [create_origin_source_entry('webpage_talent', minio_md_path)]
                
                # 处理business_card记录
                card_result = process_single_talent_card(talent_data, minio_md_path)
                
                if card_result['success']:
                    results['success_records'].append({
                        'index': index + 1,
                        'data': card_result['data'],
                        'message': card_result['message']
                    })
                    results['success_count'] += 1
                    logging.info(f"成功处理第{index + 1}个人才记录")
                else:
                    results['failed_records'].append({
                        'index': index + 1,
                        'data': talent_data,
                        'error': card_result['message']
                    })
                    results['failed_count'] += 1
                    logging.error(f"处理第{index + 1}个人才记录失败: {card_result['message']}")
                    
            except Exception as e:
                error_msg = f"处理第{index + 1}个人才记录时发生异常: {str(e)}"
                logging.error(error_msg, exc_info=True)
                results['failed_records'].append({
                    'index': index + 1,
                    'data': talent_data,
                    'error': error_msg
                })
                results['failed_count'] += 1
        
        # 生成最终结果
        if results['success_count'] == results['total_count']:
            # 全部成功时，返回业务卡片记录数组，与add_business_card保持一致
            success_data = [record['data'] for record in results['success_records']]
            return {
                'code': 200,
                'success': True,
                'message': f'所有{results["total_count"]}条人才记录处理成功',
                'data': success_data
            }
        elif results['success_count'] > 0:
            return {
                'code': 206,  # Partial Content
                'success': True,
                'message': f'部分处理成功：{results["success_count"]}/{results["total_count"]}条记录成功',
                'data': results
            }
        else:
            return {
                'code': 500,
                'success': False,
                'message': f'所有{results["total_count"]}条人才记录处理失败',
                'data': results
            }
            
    except Exception as e:
        error_msg = f"add_webpage_talent函数执行失败: {str(e)}"
        logging.error(error_msg, exc_info=True)
        return {
            'code': 500,
            'success': False,
            'message': error_msg,
            'data': None
        }


def download_and_upload_image_to_minio(pic_url, person_name):
    """
    从URL下载图片并上传到MinIO
    
    Args:
        pic_url (str): 图片的URL地址
        person_name (str): 人员姓名，用于生成文件名
        
    Returns:
        str: MinIO中的图片路径，如果失败返回None
    """
    try:
        if not pic_url or not pic_url.strip():
            logging.info("pic_url为空，跳过图片下载")
            return None
            
        # 验证URL格式
        if not pic_url.startswith(('http://', 'https://')):
            logging.warning(f"无效的图片URL格式: {pic_url}")
            return None
        
        logging.info(f"开始下载图片: {pic_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(pic_url, headers=headers, timeout=30)
        response.raise_for_status()
        
        # 检查响应内容类型
        content_type = response.headers.get('content-type', '').lower()
        if not content_type.startswith('image/'):
            logging.warning(f"URL返回的不是图片内容，content-type: {content_type}")
            return None
        
        # 获取图片数据
        image_data = response.content
        if len(image_data) == 0:
            logging.warning("下载的图片数据为空")
            return None
        
        # 从URL或content-type推断文件扩展名
        file_extension = '.jpg'  # 默认扩展名
        if 'png' in content_type:
            file_extension = '.png'
        elif 'gif' in content_type:
            file_extension = '.gif'
        elif 'webp' in content_type:
            file_extension = '.webp'
        elif pic_url.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
            file_extension = '.' + pic_url.split('.')[-1].lower()
        
        # 生成文件名
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        unique_id = uuid.uuid4().hex[:8]
        safe_name = re.sub(r'[^\w\s-]', '', person_name.replace(' ', '_'))[:20]  # 清理人名用作文件名
        filename = f"talent_photo_{safe_name}_{timestamp}_{unique_id}{file_extension}"
        
        # 获取MinIO客户端
        minio_client = get_minio_client()
        if not minio_client:
            logging.error("无法获取MinIO客户端")
            return None
        
        # 将图片数据转换为字节流
        image_stream = BytesIO(image_data)
        
        # 上传到MinIO
        minio_path = f"talent_photos/{filename}"
        logging.info(f"开始上传图片到MinIO: {minio_path}")
        
        minio_client.put_object(
            Bucket=minio_bucket,
            Key=minio_path,
            Body=image_stream,
            ContentType=content_type,
            Metadata={
                'original_url': pic_url,
                'person_name': person_name,
                'upload_time': get_east_asia_isoformat(),
                'content_type': 'talent_photo'
            }
        )
        
        logging.info(f"图片成功上传到MinIO: {minio_path}")
        return minio_path
        
    except requests.RequestException as e:
        logging.error(f"下载图片失败 ({pic_url}): {str(e)}")
        return None
    except Exception as e:
        logging.error(f"上传图片到MinIO失败: {str(e)}", exc_info=True)
        return None


def process_single_talent_card(talent_data, minio_md_path):
    """
    处理单个人才的名片记录创建
    
    Args:
        talent_data (dict): 人才信息数据
        minio_md_path (str): MinIO中网页内容的路径
        
    Returns:
        dict: 处理结果
    """
    try:
        # 下载并上传图片到MinIO
        image_path = None
        if talent_data.get('pic_url'):
            person_name = talent_data.get('name_zh', 'unknown')
            image_path = download_and_upload_image_to_minio(talent_data['pic_url'], person_name)
            if image_path:
                logging.info(f"成功处理人员 {person_name} 的图片，MinIO路径: {image_path}")
            else:
                logging.warning(f"人员 {person_name} 的图片处理失败")
        
        # 检查重复记录
        try:
            duplicate_check = check_duplicate_business_card(talent_data)
            logging.info(f"重复记录检查结果: {duplicate_check['reason']}")
        except Exception as e:
            logging.error(f"重复记录检查失败: {str(e)}", exc_info=True)
            # 如果检查失败，默认创建新记录
            duplicate_check = {
                'is_duplicate': False,
                'action': 'create_new',
                'existing_card': None,
                'reason': f'重复检查失败，创建新记录: {str(e)}'
            }
        
        # 根据重复检查结果执行不同操作
        if duplicate_check['action'] == 'update':
            # 更新现有记录
            existing_card = duplicate_check['existing_card']
            
            # 更新基本信息
            existing_card.name_en = talent_data.get('name_en', existing_card.name_en)
            existing_card.title_zh = talent_data.get('title_zh', existing_card.title_zh)
            existing_card.title_en = talent_data.get('title_en', existing_card.title_en)
            
            # 处理手机号码字段，支持多个手机号码
            if 'mobile' in talent_data:
                new_mobile = normalize_mobile_numbers(talent_data.get('mobile', ''))
                if new_mobile:
                    # 合并手机号码
                    from app.core.data_parse.parse_system import merge_mobile_numbers
                    existing_card.mobile = merge_mobile_numbers(existing_card.mobile, new_mobile)
                elif talent_data.get('mobile') == '':
                    existing_card.mobile = ''
            
            existing_card.phone = talent_data.get('phone', existing_card.phone)
            existing_card.email = talent_data.get('email', existing_card.email)
            existing_card.hotel_zh = talent_data.get('hotel_zh', existing_card.hotel_zh)
            existing_card.hotel_en = talent_data.get('hotel_en', existing_card.hotel_en)
            existing_card.address_zh = talent_data.get('address_zh', existing_card.address_zh)
            existing_card.address_en = talent_data.get('address_en', existing_card.address_en)
            existing_card.postal_code_zh = talent_data.get('postal_code_zh', existing_card.postal_code_zh)
            existing_card.postal_code_en = talent_data.get('postal_code_en', existing_card.postal_code_en)
            existing_card.brand_zh = talent_data.get('brand_zh', existing_card.brand_zh)
            existing_card.brand_en = talent_data.get('brand_en', existing_card.brand_en)
            existing_card.affiliation_zh = talent_data.get('affiliation_zh', existing_card.affiliation_zh)
            existing_card.affiliation_en = talent_data.get('affiliation_en', existing_card.affiliation_en)
            
            # 处理生日字段
            if talent_data.get('birthday'):
                try:
                    existing_card.birthday = datetime.strptime(talent_data.get('birthday'), '%Y-%m-%d').date()
                except ValueError:
                    # 如果日期格式不正确，保持原值
                    pass
            
            # 处理年龄字段
            if 'age' in talent_data:
                try:
                    if talent_data['age'] is not None and str(talent_data['age']).strip():
                        age_value = int(talent_data['age'])
                        if 0 < age_value <= 150:  # 合理的年龄范围检查
                            existing_card.age = age_value
                    else:
                        existing_card.age = None
                except (ValueError, TypeError):
                    # 如果年龄格式不正确，保持原值
                    pass
            
            existing_card.native_place = talent_data.get('native_place', existing_card.native_place)
            existing_card.gender = talent_data.get('gender', existing_card.gender)  # 新增性别字段
            existing_card.residence = talent_data.get('residence', existing_card.residence)
            existing_card.brand_group = talent_data.get('brand_group', existing_card.brand_group)
            existing_card.updated_by = 'webpage_talent_system'
            
            # 更新图片路径
            if image_path:
                existing_card.image_path = image_path
            
            # 更新人才档案
            if talent_data.get('talent_profile'):
                existing_card.talent_profile = talent_data.get('talent_profile')
            
            # 更新origin_source字段，将新的记录添加到JSON数组中
            existing_card.origin_source = update_origin_source(existing_card.origin_source, 'webpage_talent', minio_md_path)
            
            # 更新职业轨迹，传递图片路径
            existing_card.career_path = update_career_path(existing_card, talent_data)
            
            db.session.commit()
            
            logging.info(f"已更新现有名片记录，ID: {existing_card.id}")
            
            return {
                'success': True,
                'message': f'名片信息已更新。{duplicate_check["reason"]}',
                'data': existing_card.to_dict()
            }
            
        elif duplicate_check['action'] == 'create_with_duplicates':
            # 创建新记录作为主记录，并保存疑似重复记录信息
            main_card, duplicate_record = create_main_card_with_duplicates(
                talent_data, 
                image_path,  # 传递图片路径
                duplicate_check['suspected_duplicates'],
                duplicate_check['reason'],
                task_type='新任命'  # 传递task_type参数
            )
            
            main_card.updated_by = 'webpage_talent_system'
            
            # 设置origin_source为原始资料记录
            main_card.origin_source = [create_origin_source_entry('webpage_talent', minio_md_path)]
            db.session.commit()
            
            return {
                'success': True,
                'message': f'创建新记录成功，发现疑似重复记录待处理。{duplicate_check["reason"]}',
                'data': {
                    'main_card': main_card.to_dict(),
                    'duplicate_record_id': duplicate_record.id,
                    'suspected_duplicates_count': len(duplicate_check['suspected_duplicates']),
                    'processing_status': 'pending',
                    'duplicate_reason': duplicate_record.duplicate_reason,
                    'created_at': duplicate_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
                }
            }
            
        else:
            # 创建新记录
            # 准备初始职业轨迹
            initial_entry = {
                'date': get_east_asia_date_str(),
                'hotel_zh': talent_data.get('hotel_zh', ''),
                'hotel_en': talent_data.get('hotel_en', ''),
                'title_zh': talent_data.get('title_zh', ''),
                'title_en': talent_data.get('title_en', ''),
                'image_path': image_path or '',  # 使用下载的图片路径
                'source': 'webpage_extraction'
            }
            initial_career_path = [initial_entry]
            
            # 处理年龄字段，确保是有效的整数或None
            age_value = None
            if talent_data.get('age'):
                try:
                    age_value = int(talent_data.get('age'))
                    if age_value <= 0 or age_value > 150:  # 合理的年龄范围检查
                        age_value = None
                except (ValueError, TypeError):
                    age_value = None
            
            business_card = BusinessCard()
            business_card.name_zh = talent_data.get('name_zh', '')
            business_card.name_en = talent_data.get('name_en', '')
            business_card.title_zh = talent_data.get('title_zh', '')
            business_card.title_en = talent_data.get('title_en', '')
            business_card.mobile = normalize_mobile_numbers(talent_data.get('mobile', ''))
            business_card.phone = talent_data.get('phone', '')
            business_card.email = talent_data.get('email', '')
            business_card.hotel_zh = talent_data.get('hotel_zh', '')
            business_card.hotel_en = talent_data.get('hotel_en', '')
            business_card.address_zh = talent_data.get('address_zh', '')
            business_card.address_en = talent_data.get('address_en', '')
            business_card.postal_code_zh = talent_data.get('postal_code_zh', '')
            business_card.postal_code_en = talent_data.get('postal_code_en', '')
            business_card.brand_zh = talent_data.get('brand_zh', '')
            business_card.brand_en = talent_data.get('brand_en', '')
            business_card.affiliation_zh = talent_data.get('affiliation_zh', '')
            business_card.affiliation_en = talent_data.get('affiliation_en', '')
            business_card.birthday = datetime.strptime(talent_data.get('birthday'), '%Y-%m-%d').date() if talent_data.get('birthday') else None
            business_card.age = age_value
            business_card.native_place = talent_data.get('native_place', '')
            business_card.gender = talent_data.get('gender', '')  # 新增性别字段
            business_card.residence = talent_data.get('residence', '')
            business_card.image_path = image_path  # 使用下载的图片路径
            business_card.career_path = initial_career_path
            business_card.brand_group = talent_data.get('brand_group', '')
            business_card.origin_source = [create_origin_source_entry('webpage_talent', minio_md_path)]
            business_card.talent_profile = talent_data.get('talent_profile', '')  # 人才档案
            business_card.status = 'active'
            business_card.updated_by = 'webpage_talent_system'
            
            db.session.add(business_card)
            db.session.commit()
            
            logging.info(f"名片信息已保存到数据库，ID: {business_card.id}")
            
            return {
                'success': True,
                'message': f'名片信息保存成功。{duplicate_check["reason"]}',
                'data': business_card.to_dict()
            }
            
    except Exception as e:
        db.session.rollback()
        error_msg = f"处理单个人才名片记录失败: {str(e)}"
        logging.error(error_msg, exc_info=True)
        
        return {
            'success': False,
            'message': error_msg,
            'data': None
        }


def process_webpage_with_QWen(markdown_text, publish_time):
    """
    使用阿里云的 Qwen VL Max 模型解析单个人员的 markdown 文本中的名片信息
    
    Args:
        markdown_text (str): 单个人员的 markdown 格式文本内容
        publish_time (str): 发布时间，用于career_path中的date字段
        
    Returns:
        list: 解析的名片信息列表（通常包含1个人员信息）
    """
    # 阿里云 Qwen API 配置
    QWEN_API_KEY = os.environ.get('QWEN_API_KEY', 'sk-8f2320dafc9e4076968accdd8eebd8e9')
    
    try:
        logging.info(f"开始处理网页文本，文本长度: {len(markdown_text) if markdown_text else 0} 字符")
        
        # 初始化 OpenAI 客户端，配置为阿里云 API
        client = OpenAI(
            api_key=QWEN_API_KEY,
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            timeout=60.0,  # 设置60秒超时
        )
        
        # 构建针对单个人员网页文本的优化提示语
        prompt = """你是酒店行业人事任命信息提取专家。请仔细分析提供的网页Markdown文本内容，精确提取其中的单个人员任命信息。

## 重要说明
1. **单人员处理**: 文本内容通常包含一个人的任命信息，可能包含数字编号（如**1**）作为标识。
2. **照片链接识别**: 人物照片链接通常出现在人物姓名的前面，通过空行分隔。请优先关联距离最近的照片链接。
3. **字段限制**: 只需要提取指定的8个字段，其他信息忽略。
4. **准确性要求**: 请确保提取的信息准确无误，包括总经理、副总裁、总监等各级管理人员的信息。

## 提取要求
- 区分中英文内容，分别提取
- 保持提取信息的原始格式（如大小写、标点）
- 对于无法识别或文本中不存在的信息，返回空字符串
- 文本中没有的信息，请不要猜测
- 从酒店任命公告中识别人员基本信息和职位信息

## 照片链接识别规则
- 照片通常以 ![描述](URL) 格式出现
- 照片链接通常位于人物姓名的前面，通过空行分隔
- 如果照片无法明确对应人物，则pic_url为空字符串

## 需提取的字段（仅这8个字段）
1. 中文姓名 (name_zh) - 人物的中文姓名
2. 英文姓名 (name_en) - 人物的英文姓名，如果没有则为空字符串
3. 中文职位/头衔 (title_zh) - 人物的中文职位或头衔
4. 英文职位/头衔 (title_en) - 人物的英文职位或头衔，如果没有则为空字符串
5. 中文酒店/公司名称 (hotel_zh) - 人物所在的中文酒店或公司名称
6. 英文酒店/公司名称 (hotel_en) - 人物所在的英文酒店或公司名称，如果没有则为空字符串
7. 品牌组合 (brand_group) - 酒店所属的品牌集团，如万豪、希尔顿等
8. 照片链接 (pic_url) - 人物的照片URL链接，根据上述识别规则提取

## 输出格式
请以严格的JSON格式返回结果，不要添加任何额外解释文字。返回JSON对象格式（不是数组），包含单个人员的信息。

示例：
```json
{
  "name_zh": "张三",
  "name_en": "Zhang San",
  "title_zh": "总经理",
  "title_en": "General Manager",
  "hotel_zh": "北京万豪酒店",
  "hotel_en": "Beijing Marriott Hotel",
  "brand_group": "万豪",
  "pic_url": "https://example.com/photo1.jpg"
}
```

## 特别提醒
- 专注于提取单个人员的完整信息
- 确保提取的信息准确且完整
- 如果某个字段在文本中不存在，请返回空字符串

以下是需要分析的单个人员网页Markdown文本内容：

""" + markdown_text
        
        # 调用 Qwen VL Max API，添加重试机制
        max_retries = 3
        retry_count = 0
        response_content = None
        
        while retry_count < max_retries:
            try:
                logging.info(f"发送网页文本请求到 Qwen VL Max 模型 (尝试 {retry_count + 1}/{max_retries})")
                
                # 设置更详细的超时和重试配置
                completion = client.chat.completions.create(
                    model="qwen-vl-max-latest",
                    messages=[
                        {
                            "role": "user",
                            "content": [
                                {"type": "text", "text": prompt}
                            ]
                        }
                    ],
                    temperature=0.1,  # 降低温度增加精确性
                    response_format={"type": "json_object"},  # 要求输出JSON格式
                    timeout=60  # 设置60秒超时
                )
                
                # 解析响应
                response_content = completion.choices[0].message.content
                logging.info(f"成功从 Qwen 模型获取单个人员文本响应: {response_content}")
                break  # 成功获取响应，跳出重试循环
                
            except Exception as api_error:
                retry_count += 1
                error_msg = f"Qwen API 调用失败 (尝试 {retry_count}/{max_retries}): {str(api_error)}"
                logging.warning(error_msg)
                
                if retry_count >= max_retries:
                    # 所有重试都失败了
                    logging.error(f"Qwen API 调用失败，已重试 {max_retries} 次，最终错误: {str(api_error)}")
                    raise Exception(f"Qwen API 调用失败，已重试 {max_retries} 次: {str(api_error)}")
                else:
                    # 等待一段时间后重试
                    import time
                    wait_time = 2 * retry_count
                    logging.info(f"等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)  # 递增等待时间
                    continue
        
        # 检查是否成功获取响应
        if not response_content:
            error_msg = "未能从 Qwen API 获取有效响应"
            logging.error(error_msg)
            raise Exception(error_msg)
        
        # 直接解析 QWen 返回的 JSON 响应
        try:
            extracted_data = json.loads(response_content)
            logging.info("成功解析 Qwen 单个人员文本响应中的 JSON")
        except json.JSONDecodeError as e:
            error_msg = f"JSON 解析失败: {str(e)}, 响应内容: {response_content[:200]}..."
            logging.error(error_msg)
            raise Exception(error_msg)

        # 确保返回的是单个人员对象，转换为列表格式以保持一致性
        if isinstance(extracted_data, list):
            # 如果意外返回数组，取第一个元素
            if len(extracted_data) > 0:
                person_data = extracted_data[0]
                logging.warning("Qwen返回了数组格式，取第一个人员信息")
            else:
                logging.error("Qwen返回了空数组")
                return []
        elif isinstance(extracted_data, dict):
            # 正常情况，返回的是单个人员对象
            person_data = extracted_data
        else:
            logging.error(f"Qwen返回了不支持的数据格式: {type(extracted_data)}")
            return []
        
        # 确保人员对象包含所有必要字段
        required_fields = [
            'name_zh', 'name_en', 'title_zh', 'title_en', 
            'hotel_zh', 'hotel_en', 'brand_group', 'pic_url'
        ]
        
        for field in required_fields:
            if field not in person_data:
                person_data[field] = ""
        
        # 为人员添加career_path字段
        career_entry = {
            'date': publish_time,
            'hotel_en': person_data.get('hotel_en', ''),
            'hotel_zh': person_data.get('hotel_zh', ''),
            'image_path': '',
            'source': 'webpage_extraction',
            'title_en': person_data.get('title_en', ''),
            'title_zh': person_data.get('title_zh', '')
        }
        
        person_data['career_path'] = [career_entry]
        logging.info(f"为人员 {person_data.get('name_zh', 'Unknown')} 添加了career_path记录: {career_entry}")
        
        # 返回列表格式以保持与其他函数的一致性
        logging.info(f"process_webpage_with_QWen 函数执行完成，返回 {len([person_data])} 条记录")
        return [person_data]
        
    except Exception as e:
        error_msg = f"Qwen VL Max 模型网页文本解析失败: {str(e)}"
        logging.error(error_msg, exc_info=True)
        raise Exception(error_msg) 


def _convert_webpage_to_card_format(webpage_data: Dict[str, Any], publish_time: str) -> Dict[str, Any]:
    """
    将网页解析的数据转换为标准名片格式，与任务解析结果.txt中的data字段格式一致
    
    Args:
        webpage_data (Dict[str, Any]): 网页解析的原始数据
        publish_time (str): 发布时间
        
    Returns:
        Dict[str, Any]: 标准化后的名片格式数据
    """
    # 构建隶属关系
    affiliation = []
    company = webpage_data.get('hotel_zh', '')
    if company:
        affiliation.append({
            "company": company,
            "group": webpage_data.get('brand_group', '')
        })
    
    # 构建职业轨迹
    career_path = []
    position = webpage_data.get('title_zh', '')
    if position and company:
        career_path.append({
            "date": publish_time if publish_time else get_east_asia_date_str(),
            "hotel_en": webpage_data.get('hotel_en', ''),
            "hotel_zh": company,
            "image_path": webpage_data.get('pic_url', ''),
            "source": "webpage_talent_extraction",
            "title_en": webpage_data.get('title_en', ''),
            "title_zh": position
        })
    
    # 按照任务解析结果.txt的data字段格式组装数据
    standardized = {
        "address_en": webpage_data.get('address_en', ''),
        "address_zh": webpage_data.get('address_zh', ''),
        "affiliation": affiliation,
        "age": webpage_data.get('age', ''),
        "birthday": webpage_data.get('birthday', ''),
        "brand_group": webpage_data.get('brand_group', ''),
        "career_path": career_path,
        "email": webpage_data.get('email', ''),
        "hotel_en": webpage_data.get('hotel_en', ''),
        "hotel_zh": company,
        "mobile": webpage_data.get('mobile', ''),
        "name_en": webpage_data.get('name_en', ''),
        "name_zh": webpage_data.get('name_zh', ''),
        "native_place": webpage_data.get('native_place', ''),
        "phone": webpage_data.get('phone', ''),
        "postal_code_en": webpage_data.get('postal_code_en', ''),
        "postal_code_zh": webpage_data.get('postal_code_zh', ''),
        "residence": webpage_data.get('residence', ''),
        "title_en": webpage_data.get('title_en', ''),
        "title_zh": position
    }
    
    return standardized


def batch_process_md(markdown_file_list, publish_time=None, task_id=None, task_type=None):
    """
    批量处理包含多个人员信息的markdown文件
    
    Args:
        markdown_file_list (list): MinIO对象保存地址组成的数组，每个元素包含publish_time字段（已废弃，现在从数据库读取）
        publish_time (str, optional): 发布时间，用于career_path中的date字段（已废弃，从task_source中获取）
        task_id (str, optional): 任务ID，用于从数据库读取task_source
        task_type (str, optional): 任务类型
        
    Returns:
        dict: 批量处理结果，格式与parse_result保持一致
    """
    # 初始化变量
    task_record = None
    task_source = []
    success_count = 0
    failed_count = 0
    parsed_record_ids = []
    
    try:
        # 根据task_id从parse_task_repository表读取记录
        if not task_id:
            return {
                'code': 400,
                'success': False,
                'message': '缺少task_id参数',
                'data': None
            }
        
        # 导入数据库模型
        from app.models.parse_models import ParseTaskRepository
        from app import db
        
        # 查询对应的任务记录
        task_record = ParseTaskRepository.query.get(task_id)
        if not task_record:
            return {
                'code': 404,
                'success': False,
                'message': f'未找到task_id为{task_id}的任务记录',
                'data': None
            }
        
        # 获取task_source作为需要处理的数据列表
        task_source = task_record.task_source
        if not task_source or not isinstance(task_source, list):
            return {
                'code': 400,
                'success': False,
                'message': 'task_source为空或格式不正确',
                'data': None
            }
        
        logging.info(f"开始批量处理markdown文件，共 {len(task_source)} 条记录")
        
        # 逐一处理每条数据
        for i, data in enumerate(task_source):
            try:
                # 只处理parse_flag为1的记录
                if not isinstance(data, dict) or data.get('parse_flag') != 1:
                    logging.debug(f"跳过第 {i+1} 条数据，parse_flag不为1或格式不正确")
                    continue
                
                logging.info(f"处理第 {i+1}/{len(task_source)} 条数据")
                
                # 从数据中获取必要信息
                minio_path = data.get('minio_path', '')
                file_publish_time = data.get('publish_time', publish_time)
                
                if not minio_path:
                    logging.warning(f"第 {i+1} 条数据缺少minio_path")
                    failed_count += 1
                    # 更新task_source中对应记录的parse_flag和status
                    data['parse_flag'] = 1
                    data['status'] = '解析失败'
                    # 立即更新数据库记录
                    try:
                        task_record.task_source = task_source
                        task_record.task_status = '解析中'
                        db.session.commit()
                        logging.info(f"第 {i+1} 条数据处理失败，已更新数据库记录")
                    except Exception as update_error:
                        logging.error(f"更新数据库记录失败: {str(update_error)}")
                        db.session.rollback()
                    continue
                
                # 处理单个markdown文件
                file_result = process_single_markdown_file(minio_path, file_publish_time, task_id, task_type)
                
                if file_result.get('success', False):
                    # 提取处理结果中的人员信息
                    persons_data = file_result.get('data', {}).get('all_results', [])
                    
                    # 收集parsed_record_ids
                    file_parsed_record_ids = file_result.get('data', {}).get('parsed_record_ids', [])
                    if file_parsed_record_ids:
                        parsed_record_ids.extend(file_parsed_record_ids)
                    
                    if persons_data and isinstance(persons_data, list):
                        # 为每个人员创建一个结果记录
                        for person_idx, person_data in enumerate(persons_data):
                            success_count += 1
                            logging.info(f"成功提取人员 {person_idx+1}: {person_data.get('name_zh', 'Unknown')}")
                        
                        # 更新task_source中对应记录的parse_flag和status
                        data['parse_flag'] = 0
                        data['status'] = '解析成功'
                    else:
                        # 没有提取到有效数据，这算作一个失败记录
                        failed_count += 1
                        logging.warning(f"第 {i+1} 个文件未提取到人员信息")
                        
                        # 更新task_source中对应记录的parse_flag和status
                        data['parse_flag'] = 1
                        data['status'] = '解析失败'
                else:
                    # 文件处理失败，算作一个失败记录
                    failed_count += 1
                    error_msg = file_result.get('message', '处理失败')
                    logging.error(f"处理第 {i+1} 个文件失败: {error_msg}")
                    
                    # 更新task_source中对应记录的parse_flag和status
                    data['parse_flag'] = 1
                    data['status'] = '解析失败'
                
                # 立即更新数据库记录
                try:
                    task_record.task_source = task_source
                    task_record.task_status = '解析中'
                    db.session.commit()
                    logging.info(f"第 {i+1} 条数据处理完成，已更新数据库记录")
                except Exception as update_error:
                    logging.error(f"更新数据库记录失败: {str(update_error)}")
                    db.session.rollback()
                    
            except Exception as item_error:
                failed_count += 1
                error_msg = f"处理markdown文件失败: {str(item_error)}"
                logging.error(error_msg, exc_info=True)
                
                # 更新task_source中对应记录的parse_flag和status
                if isinstance(data, dict):
                    data['parse_flag'] = 1
                    data['status'] = '解析失败'
                
                # 立即更新数据库记录
                try:
                    task_record.task_source = task_source
                    task_record.task_status = '解析中'
                    db.session.commit()
                    logging.info(f"第 {i+1} 条数据处理异常，已更新数据库记录")
                except Exception as update_error:
                    logging.error(f"更新数据库记录失败: {str(update_error)}")
                    db.session.rollback()
        
        # 根据处理结果更新task_status
        if failed_count == 0:
            task_status = '解析成功'
        elif success_count == 0:
            task_status = '解析失败'
        else:
            task_status = '部分解析成功'
        
    except Exception as e:
        error_msg = f"batch_process_md函数执行失败: {str(e)}"
        logging.error(error_msg, exc_info=True)
        
        # 即使出现异常，也要确保更新数据库
        if task_record and task_source:
            try:
                task_record.task_source = task_source
                task_record.task_status = '解析失败'
                task_record.parse_count = success_count
                task_record.parse_result = {
                    'success_count': success_count,
                    'failed_count': failed_count,
                    'parsed_record_ids': parsed_record_ids,
                    'processed_time': get_east_asia_isoformat(),
                    'error': error_msg
                }
                db.session.commit()
                logging.info(f"异常情况下成功更新task_id为{task_id}的任务记录")
            except Exception as update_error:
                logging.error(f"异常情况下更新任务记录失败: {str(update_error)}")
                db.session.rollback()
        
        return {
            'code': 500,
            'success': False,
            'message': error_msg,
            'data': {
                'success_count': success_count,
                'failed_count': failed_count,
                'parsed_record_ids': parsed_record_ids
            }
        }
    
    # 无论处理过程如何，都要保证更新数据库
    try:
        if task_record and task_source:
            task_record.task_source = task_source
            task_record.task_status = task_status
            task_record.parse_count = success_count
            task_record.parse_result = {
                'success_count': success_count,
                'failed_count': failed_count,
                'parsed_record_ids': parsed_record_ids,
                'processed_time': get_east_asia_isoformat()
            }
            db.session.commit()
            logging.info(f"成功更新task_id为{task_id}的任务记录，task_status={task_status}，处理成功{success_count}条，失败{failed_count}条")
        else:
            logging.error("无法更新数据库：task_record或task_source为空")
    except Exception as update_error:
        logging.error(f"更新任务记录失败: {str(update_error)}")
        db.session.rollback()
        # 即使数据库更新失败，也要返回处理结果
    
    # 组装最终结果
    if failed_count == 0:
        return {
            'code': 200,
            'success': True,
            'message': f'批量处理完成，全部 {success_count} 个文件处理成功',
            'data': {
                'success_count': success_count,
                'failed_count': failed_count,
                'parsed_record_ids': parsed_record_ids
            }
        }
    elif success_count == 0:
        return {
            'code': 500,
            'success': False,
            'message': f'批量处理失败，全部 {failed_count} 个文件处理失败',
            'data': {
                'success_count': success_count,
                'failed_count': failed_count,
                'parsed_record_ids': parsed_record_ids
            }
        }
    else:
        return {
            'code': 206,  # Partial Content
            'success': True,
            'message': f'批量处理部分成功，成功 {success_count} 个，失败 {failed_count} 个',
            'data': {
                'success_count': success_count,
                'failed_count': failed_count,
                'parsed_record_ids': parsed_record_ids
            }
        }


def get_markdown_from_minio(minio_client, minio_path):
    """
    从MinIO获取markdown文件内容
    
    Args:
        minio_client: MinIO客户端
        minio_path (str): MinIO中的文件路径或完整URL
        
    Returns:
        str: 文件内容，如果失败返回None
    """
    try:
        logging.info(f"从MinIO获取文件: {minio_path}")
        
        # 如果是完整的URL，提取对象键
        object_key = _extract_object_key_from_url(minio_path)
        if object_key is None:
            logging.error(f"无法从URL中提取有效的对象键: {minio_path}")
            return None
        if object_key != minio_path:
            logging.info(f"从URL提取的对象键: {object_key}")
        
        # 从MinIO下载文件
        response = minio_client.get_object(Bucket=minio_bucket, Key=object_key)
        
        # 读取文件内容
        content = response['Body'].read()
        
        # 解码为字符串
        if isinstance(content, bytes):
            # 尝试不同的编码方式
            try:
                markdown_content = content.decode('utf-8')
            except UnicodeDecodeError:
                try:
                    markdown_content = content.decode('gbk')
                except UnicodeDecodeError:
                    markdown_content = content.decode('utf-8', errors='ignore')
                    logging.warning(f"文件 {minio_path} 编码检测失败，使用UTF-8忽略错误模式")
        else:
            markdown_content = str(content)
        
        logging.info(f"成功获取文件内容，长度: {len(markdown_content)} 字符")
        return markdown_content
        
    except Exception as e:
        logging.error(f"从MinIO获取文件 {minio_path} 失败: {str(e)}", exc_info=True)
        return None


def _extract_object_key_from_url(minio_url):
    """
    从MinIO完整URL中提取对象键名
    
    Args:
        minio_url (str): 完整的MinIO URL，如 "http://host:port/bucket/path/to/file.md"
        
    Returns:
        str: 对象键名，如 "path/to/file.md"，失败时返回原始字符串或None
    """
    try:
        if not minio_url or not isinstance(minio_url, str):
            return None
            
        # 移除协议部分 (http:// 或 https://)
        if minio_url.startswith('https://'):
            url_without_protocol = minio_url[8:]
        elif minio_url.startswith('http://'):
            url_without_protocol = minio_url[7:]
        else:
            # 如果没有协议前缀，假设是相对路径，直接返回
            return minio_url
        
        # 分割路径部分
        parts = url_without_protocol.split('/')
        
        # 至少需要包含 host:port/bucket/object
        if len(parts) < 3:
            return None
        
        # 跳过host:port和bucket，获取对象路径
        object_key = '/'.join(parts[2:])
        
        return object_key if object_key else None
        
    except Exception as e:
        logging.error(f"解析MinIO URL失败: {str(e)}")
        return None


def save_section_to_minio(minio_client, section_content, original_minio_path, section_number):
    """
    将分割后的markdown内容保存到MinIO
    
    Args:
        minio_client: MinIO客户端
        section_content (str): 分割后的markdown内容
        original_minio_path (str): 原始文件的MinIO路径
        section_number (str): 分隔符编号
        
    Returns:
        str: 新保存文件的MinIO路径，如果失败返回None
    """
    try:
        # 生成新的文件名
        timestamp = get_east_asia_timestamp()
        unique_id = uuid.uuid4().hex[:8]
        
        # 从原始路径提取基础信息
        path_parts = original_minio_path.split('/')
        if len(path_parts) > 1:
            directory = '/'.join(path_parts[:-1])
            original_filename = path_parts[-1]
            # 移除扩展名
            base_name = original_filename.rsplit('.', 1)[0] if '.' in original_filename else original_filename
        else:
            directory = 'webpage_talent_sections'
            base_name = 'section'
        
        # 构建新的文件名
        new_filename = f"{base_name}_section_{section_number}_{timestamp}_{unique_id}.md"
        new_minio_path = f"{directory}/{new_filename}"
        
        logging.info(f"开始保存分割内容到MinIO: {new_minio_path}")
        
        # 将内容转换为字节流
        content_bytes = section_content.encode('utf-8')
        content_stream = BytesIO(content_bytes)
        
        # 上传到MinIO
        minio_client.put_object(
            Bucket=minio_bucket,
            Key=new_minio_path,
            Body=content_stream,
            ContentType='text/markdown',
            Metadata={
                'original_file': original_minio_path,
                'section_number': section_number,
                'upload_time': get_east_asia_isoformat(),
                'content_type': 'webpage_talent_section'
            }
        )
        
        logging.info(f"分割内容成功保存到MinIO: {new_minio_path}")
        return new_minio_path
        
    except Exception as e:
        logging.error(f"保存分割内容到MinIO失败: {str(e)}", exc_info=True)
        return None


def process_single_markdown_file(minio_path, publish_time, task_id=None, task_type=None):
    """
    处理单个markdown文件，从MinIO获取内容并直接处理
    
    Args:
        minio_path (str): MinIO中的文件路径
        publish_time (str): 发布时间
        
    Returns:
        dict: 处理结果
    """
    try:
        # 获取MinIO客户端
        minio_client = get_minio_client()
        if not minio_client:
            return {
                'success': False,
                'message': '无法连接到MinIO服务器',
                'data': None
            }
        
        # 从MinIO获取文件内容
        markdown_content = get_markdown_from_minio(minio_client, minio_path)
        if not markdown_content:
            return {
                'success': False,
                'message': f'无法从MinIO获取文件内容: {minio_path}',
                'data': None
            }
        
        # 直接处理整个文件
        logging.info("直接处理整个markdown文件")
        try:
            logging.info(f"开始调用 process_webpage_with_QWen 函数处理文件: {minio_path}")
            result = process_webpage_with_QWen(markdown_content, publish_time)
            logging.info(f"process_webpage_with_QWen 函数执行完成，返回结果: {len(result) if result else 0} 条记录")
            
            parsed_record_ids = []  # 收集成功解析的记录ID
            
            # 更新解析结果中的路径信息
            if result:
                for person in result:
                    person['pic_url'] = minio_path  # 设置原始文件路径
                    person['image_path'] = minio_path  # 设置image_path
                    
                    # 设置origin_source为JSON数组格式
                    current_date = get_east_asia_date_str()
                    origin_source_entry = {
                        "task_type": "新任命",
                        "minio_path": minio_path,
                        "source_date": current_date
                    }
                    person['origin_source'] = [origin_source_entry]
                    
                    if 'career_path' in person and person['career_path']:
                        for career_entry in person['career_path']:
                            career_entry['image_path'] = minio_path  # 设置原始文件路径
                    
                    # 记录成功解析的人才信息到parsed_talents表
                    if task_id and task_type:
                        try:
                            from app.core.data_parse.parse_task import record_parsed_talent
                            standardized_data = _convert_webpage_to_card_format(person, publish_time)
                            
                            # 调用get_brand_group_by_hotel获取品牌和集团信息
                            if standardized_data.get('hotel_zh'):
                                try:
                                    from app.core.data_parse.parse_system import get_brand_group_by_hotel
                                    brand_result = get_brand_group_by_hotel(standardized_data['hotel_zh'])
                                    if brand_result.get('success') and brand_result.get('data'):
                                        brand_data = brand_result['data']
                                        # 赋值品牌和集团信息
                                        standardized_data['brand_zh'] = brand_data.get('brand_name_zh', '')
                                        standardized_data['brand_en'] = brand_data.get('brand_name_en', '')
                                        standardized_data['affiliation_zh'] = brand_data.get('group_name_zh', '')
                                        standardized_data['affiliation_en'] = brand_data.get('group_name_en', '')
                                        logging.info(f"成功获取品牌和集团信息: {brand_data}")
                                    else:
                                        logging.warning(f"获取品牌信息失败: {brand_result.get('message', '')}")
                                        # 设置默认值
                                        standardized_data['brand_zh'] = ''
                                        standardized_data['brand_en'] = ''
                                        standardized_data['affiliation_zh'] = ''
                                        standardized_data['affiliation_en'] = ''
                                except Exception as brand_error:
                                    logging.error(f"调用get_brand_group_by_hotel失败: {str(brand_error)}")
                                    # 设置默认值
                                    standardized_data['brand_zh'] = ''
                                    standardized_data['brand_en'] = ''
                                    standardized_data['affiliation_zh'] = ''
                                    standardized_data['affiliation_en'] = ''
                            else:
                                # 没有酒店信息，设置默认值
                                standardized_data['brand_zh'] = ''
                                standardized_data['brand_en'] = ''
                                standardized_data['affiliation_zh'] = ''
                                standardized_data['affiliation_en'] = ''
                            
                            # 在记录到parsed_talents表之前，设置image_path和origin_source
                            standardized_data['image_path'] = minio_path
                            
                            # 设置origin_source为JSON数组格式
                            current_date = get_east_asia_date_str()
                            origin_source_entry = {
                                "task_type": "新任命",
                                "minio_path": minio_path,
                                "source_date": current_date
                            }
                            standardized_data['origin_source'] = [origin_source_entry]
                            
                            record_result = record_parsed_talent(standardized_data, task_id, task_type)
                            if record_result.get('success'):
                                # 收集成功解析的记录ID
                                parsed_record = record_result.get('data', {})
                                if parsed_record and 'id' in parsed_record:
                                    parsed_record_ids.append(str(parsed_record['id']))
                                logging.info(f"成功记录人才信息到parsed_talents表: {person.get('name_zh', '')}")
                            else:
                                logging.warning(f"记录人才信息失败: {record_result.get('message', '')}")
                        except Exception as record_error:
                            logging.error(f"调用record_parsed_talent函数失败: {str(record_error)}")
            
            logging.info(f"单个markdown文件处理完成，成功解析 {len(result) if result else 0} 条记录")
            return {
                'success': True,
                'message': '单个markdown文件处理成功',
                'data': {
                    'total_sections': 1,
                    'processed_sections': 1,
                    'total_persons': len(result) if result else 0,
                    'all_results': result,
                    'section_results': [],
                    'failed_sections_info': [],
                    'parsed_record_ids': parsed_record_ids
                }
            }
        except Exception as e:
            error_msg = f"处理单个markdown文件失败: {str(e)}"
            logging.error(error_msg, exc_info=True)
            return {
                'success': False,
                'message': error_msg,
                'data': None
            }
            
    except Exception as e:
        error_msg = f"process_single_markdown_file函数执行失败: {str(e)}"
        logging.error(error_msg, exc_info=True)
        return {
            'success': False,
            'message': error_msg,
            'data': None
        } 

