#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：erp_service.py
模块描述：ERP上传服务，封装报告上传、项目资料上传、协作文档上传等ERP接口调用

移植自独立工具 FunctionalTestReportGeneration/src/erp_uploader.py，
重构为 ErpService 类，配置改为参数注入，CLI 交互函数不移植（由API层处理）。

作者：czj
创建日期：2026-08-12
最后修改：2026-08-12
"""

import logging
import os
import time
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import requests

from .functional.config import (
    ERP_API_KEY,
    ERP_BASE_URL,
    ERP_DEVELOPTESTING_ID,
    ERP_TYPE_ID,
    ERP_RETRY_INTERVAL,
    ERP_MAX_RETRIES,
    ERP_UPLOAD_IMAGE_URL,
    ERP_CREATE_REPORT_URL,
    ERP_STUFF_URL,
    ERP_UPLOAD_PROJECT_URL,
    ERP_UPLOAD_COOPERATION_URL,
    ERP_REQUEST_TIMEOUT,
    ERP_SAVE_IMAGES_TEMP,
    ERP_VALIDATE_IMAGES,
    ERP_SAVE_HTML_TEMP,
    ERP_IMAGE_URL_PREFIX,
)


logger = logging.getLogger(__name__)


class ErpService:
    """
    ERP上传服务

    封装ERP系统接口调用，提供：
    - 报告上传（Word→HTML→图片上传→创建测试报告）
    - 项目资料上传
    - 协作文档上传
    - 人员列表获取与姓名匹配
    """

    def __init__(
        self,
        api_key: str = None,
        base_url: str = None,
        developtesting_id: int = None,
        type_id: int = None,
        image_url_prefix: str = None,
        temp_dir: str = None,
    ):
        """
        Args:
            api_key: ERP API密钥
            base_url: ERP基础URL
            developtesting_id: 默认测试单ID
            type_id: 报告类型ID
            image_url_prefix: 图片URL前缀
            temp_dir: 临时目录（用于保存调试图片/HTML）
        """
        self.api_key = api_key or ERP_API_KEY
        self.base_url = base_url or ERP_BASE_URL
        self.developtesting_id = developtesting_id or ERP_DEVELOPTESTING_ID
        self.type_id = type_id or ERP_TYPE_ID
        self.image_url_prefix = image_url_prefix or ERP_IMAGE_URL_PREFIX
        self.temp_dir = Path(temp_dir) if temp_dir else None

    # ==================== 人员列表 ====================

    def get_stuff_list(self) -> Optional[list]:
        """
        获取ERP系统人员列表（含重试机制）

        Returns:
            人员列表，格式: [{"id": 1, "name": "超管员", ...}, ...]
            失败返回None
        """
        url = f"{self.base_url}{ERP_STUFF_URL}"

        for attempt in range(ERP_MAX_RETRIES):
            try:
                headers = {"X-Api-Key": self.api_key}
                logger.info(f"获取人员列表: {url}")

                resp = requests.get(url, headers=headers, timeout=ERP_REQUEST_TIMEOUT)
                result = resp.json()

                if result.get("success") == 1:
                    stuff_list = result["data"]
                    logger.info(f"✓ 获取人员列表成功，共 {len(stuff_list)} 人")
                    return stuff_list
                else:
                    error_code = result.get("error", 0)
                    if 400 <= error_code < 500:
                        logger.error(f"✗ 获取人员列表失败(客户端错误 {error_code})")
                        return None
                    if attempt < ERP_MAX_RETRIES - 1:
                        logger.warning(f"重试获取人员列表 ({attempt+1}/{ERP_MAX_RETRIES})")
                        time.sleep(ERP_RETRY_INTERVAL)
                    else:
                        logger.error(f"✗ 获取人员列表失败(已达最大重试次数)")

            except requests.exceptions.Timeout:
                logger.warning(f"获取人员列表超时，重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                if attempt < ERP_MAX_RETRIES - 1:
                    time.sleep(ERP_RETRY_INTERVAL)
                else:
                    logger.error(f"✗ 获取人员列表超时(已达最大重试次数)")
            except Exception as e:
                logger.error(f"✗ 获取人员列表异常: {str(e)}")
                break

        return None

    def match_names_to_ids(self, names: list, stuff_list: list) -> list:
        """
        将姓名列表匹配为人员ID列表

        Args:
            names: 姓名列表
            stuff_list: 人员列表数据（来自 get_stuff_list）

        Returns:
            匹配成功的人员ID列表
        """
        matched_ids = []
        not_found_names = []

        for name in names:
            name = name.strip()
            if not name:
                continue

            found = False
            for person in stuff_list:
                if person["name"] == name:
                    matched_ids.append(person["id"])
                    logger.info(f"  ✓ 抄送人匹配: {name} → ID: {person['id']}")
                    found = True
                    break

            if not found:
                not_found_names.append(name)
                logger.warning(f"  ✗ 未找到匹配人员: {name}")

        if not_found_names:
            logger.warning(f"以下人员未找到: {', '.join(not_found_names)}")

        return matched_ids

    # ==================== 图片上传 ====================

    def upload_image_to_erp(self, img_bytes: bytes, idx: int) -> Optional[str]:
        """
        上传单张图片到ERP（含重试机制）

        Args:
            img_bytes: 图片字节数据
            idx: 图片索引

        Returns:
            图片完整URL，失败返回None
        """
        url = f"{self.base_url}{ERP_UPLOAD_IMAGE_URL}"

        for attempt in range(ERP_MAX_RETRIES):
            try:
                headers = {"X-Api-Key": self.api_key}
                files = {"file": ("image.png", img_bytes, "image/png")}

                resp = requests.post(url, headers=headers, files=files, timeout=ERP_REQUEST_TIMEOUT)
                result = resp.json()

                if result.get("success") == 1:
                    original_url = result["data"]["url"]
                    full_url = f"{self.image_url_prefix}{original_url}"
                    logger.info(f"✓ 图片{idx}上传成功: {full_url}")
                    return full_url
                else:
                    error_code = result.get("error", 0)
                    if 400 <= error_code < 500:
                        logger.error(f"✗ 图片{idx}上传失败(客户端错误 {error_code})")
                        return None
                    if attempt < ERP_MAX_RETRIES - 1:
                        logger.warning(f"图片{idx}上传重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                        time.sleep(ERP_RETRY_INTERVAL)
                    else:
                        logger.error(f"✗ 图片{idx}上传失败(已达最大重试次数)")

            except requests.exceptions.Timeout:
                logger.warning(f"图片{idx}上传超时，重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                if attempt < ERP_MAX_RETRIES - 1:
                    time.sleep(ERP_RETRY_INTERVAL)
                else:
                    logger.error(f"✗ 图片{idx}上传超时")
            except Exception as e:
                logger.error(f"✗ 图片{idx}上传异常: {str(e)}")
                break

        return None

    # ==================== 报告创建 ====================

    def create_report_in_erp(
        self,
        content: str,
        developtesting_id: int = None,
        copyuser_list: Optional[list] = None,
        createuser_id: Optional[int] = None,
    ) -> Optional[int]:
        """
        在ERP中创建测试报告（含重试机制）

        Args:
            content: HTML格式的报告内容
            developtesting_id: 测试单ID（可选，默认使用构造时的配置）
            copyuser_list: 抄送人ID列表
            createuser_id: 创建人ID

        Returns:
            报告ID，失败返回None
        """
        if developtesting_id is None:
            developtesting_id = self.developtesting_id
        if copyuser_list is None:
            copyuser_list = []

        url = f"{self.base_url}{ERP_CREATE_REPORT_URL}"

        for attempt in range(ERP_MAX_RETRIES):
            try:
                data = {
                    "developtesting_id": developtesting_id,
                    "type_id": self.type_id,
                    "content": content,
                    "descript": "",
                    "copyuserList": copyuser_list,
                }
                if createuser_id is not None:
                    data["createuser_id"] = createuser_id

                headers = {
                    "X-Api-Key": self.api_key,
                    "Content-Type": "application/json",
                }

                logger.info(f"创建ERP报告: developtesting_id={developtesting_id}, content_len={len(content)}")
                resp = requests.post(url, headers=headers, json=data, timeout=ERP_REQUEST_TIMEOUT)
                result = resp.json()

                if result.get("success") == 1:
                    report_id = result["data"]["create"]
                    logger.info(f"✓ 报告创建成功! ERP报告ID: {report_id}")
                    return report_id
                else:
                    error_code = result.get("error", 0)
                    if 400 <= error_code < 500:
                        logger.error(f"✗ 报告创建失败(客户端错误 {error_code}): {result.get('msg')}")
                        return None
                    if attempt < ERP_MAX_RETRIES - 1:
                        logger.warning(f"报告创建重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                        time.sleep(ERP_RETRY_INTERVAL)
                    else:
                        logger.error(f"✗ 报告创建失败(已达最大重试次数)")

            except requests.exceptions.Timeout:
                logger.warning(f"报告创建超时，重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                if attempt < ERP_MAX_RETRIES - 1:
                    time.sleep(ERP_RETRY_INTERVAL)
                else:
                    logger.error(f"✗ 报告创建超时")
            except Exception as e:
                logger.error(f"✗ 报告创建异常: {str(e)}")
                break

        return None

    # ==================== Word转HTML ====================

    def word_to_html_with_images(self, file_path: str) -> Optional[str]:
        """
        将Word转HTML，图片单独上传获取URL

        Args:
            file_path: Word文件路径

        Returns:
            HTML字符串，图片已替换为ERP URL；失败返回None
        """
        logger.info(f"Word转HTML: {file_path}")

        image_urls = []
        image_index = [0]

        def upload_image_handler(image):
            with image.open() as f:
                img_bytes = f.read()
            idx = image_index[0]
            image_index[0] += 1

            logger.info(f"处理图片{idx}: type={image.content_type}, size={len(img_bytes)}")

            # 验证图片
            if ERP_VALIDATE_IMAGES:
                self._validate_and_save_image(img_bytes, idx, image.content_type)

            # 上传图片
            img_url = self.upload_image_to_erp(img_bytes, idx)
            if img_url:
                image_urls.append(img_url)
                return {"src": img_url}
            return {"src": ""}

        try:
            import mammoth

            with open(file_path, "rb") as docx_file:
                result = mammoth.convert_to_html(
                    docx_file,
                    convert_image=mammoth.images.inline(upload_image_handler),
                )
                html_content = result.value

            logger.info(f"✓ Word转HTML完成: 图片{len(image_urls)}张, HTML长{len(html_content)}字符")

            # 保存HTML到temp目录供调试
            if ERP_SAVE_HTML_TEMP and self.temp_dir:
                self._save_html_temp(html_content)

            return html_content

        except Exception as e:
            logger.error(f"✗ Word转HTML失败: {str(e)}")
            import traceback
            logger.error(traceback.format_exc())
            return None

    # ==================== 完整上传流程 ====================

    def upload_report_to_erp(
        self,
        file_path: str,
        developtesting_id: int = None,
        copyuser_list: Optional[list] = None,
        createuser_id: Optional[int] = None,
    ) -> bool:
        """
        上传报告到ERP的完整流程

        Args:
            file_path: Word报告文件路径
            developtesting_id: 测试单ID
            copyuser_list: 抄送人ID列表
            createuser_id: 创建人ID

        Returns:
            是否上传成功
        """
        if developtesting_id is None:
            developtesting_id = self.developtesting_id

        logger.info("=" * 50)
        logger.info("ERP上传流程开始")
        logger.info(f"报告文件: {file_path}")
        logger.info(f"测试单ID: {developtesting_id}")
        logger.info("=" * 50)

        # 1. Word转HTML
        html_content = self.word_to_html_with_images(file_path)
        if html_content is None:
            logger.error("✗ Word转HTML失败，上传中止")
            return False

        # 2. 创建ERP报告
        report_id = self.create_report_in_erp(
            html_content,
            developtesting_id=developtesting_id,
            copyuser_list=copyuser_list,
            createuser_id=createuser_id,
        )
        if report_id is None:
            logger.error("✗ ERP报告创建失败")
            return False

        logger.info("=" * 50)
        logger.info("✓✓✓ 报告上传完成! ✓✓✓")
        logger.info("=" * 50)
        return True

    # ==================== 项目资料上传 ====================

    def upload_file_to_project(
        self, file_path: str, project_id: int, name: str
    ) -> Optional[dict]:
        """
        上传文件到ERP项目资料（含重试机制）

        Args:
            file_path: 要上传的文件路径
            project_id: 关联的项目ID
            name: 文件名称

        Returns:
            上传成功返回文件信息字典，失败返回None
        """
        url = f"{self.base_url}{ERP_UPLOAD_PROJECT_URL}"

        logger.info(f"项目资料上传: {file_path}, project_id={project_id}, name={name}")

        if not os.path.exists(file_path):
            logger.error(f"✗ 文件不存在: {file_path}")
            return None

        for attempt in range(ERP_MAX_RETRIES):
            try:
                data = {"project_id": project_id, "name": name}
                headers = {"X-Api-Key": self.api_key}

                with open(file_path, "rb") as f:
                    files = {"file": (os.path.basename(file_path), f, "application/octet-stream")}
                    resp = requests.post(url, headers=headers, files=files, data=data, timeout=ERP_REQUEST_TIMEOUT)

                result = resp.json()
                if result.get("success") == 1:
                    file_info = result["data"]
                    logger.info(f"✓ 项目资料上传成功! 文件ID: {file_info.get('id')}")
                    return file_info
                else:
                    error_code = result.get("error", 0)
                    if 400 <= error_code < 500:
                        logger.error(f"✗ 项目资料上传失败(客户端错误 {error_code})")
                        return None
                    if attempt < ERP_MAX_RETRIES - 1:
                        logger.warning(f"项目资料上传重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                        time.sleep(ERP_RETRY_INTERVAL)
                    else:
                        logger.error(f"✗ 项目资料上传失败(已达最大重试次数)")

            except requests.exceptions.Timeout:
                logger.warning(f"项目资料上传超时，重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                if attempt < ERP_MAX_RETRIES - 1:
                    time.sleep(ERP_RETRY_INTERVAL)
            except Exception as e:
                logger.error(f"✗ 项目资料上传异常: {str(e)}")
                break

        return None

    # ==================== 协作文档上传 ====================

    def upload_file_to_cooperation(
        self, file_path: str, project_id: int, name: str, group_id: Optional[int] = None
    ) -> Optional[dict]:
        """
        上传文件到ERP协作文档（创建OnlyOffice在线协作文档），含重试机制

        Args:
            file_path: 要上传的文件路径
            project_id: 关联的项目ID
            name: 文件名称
            group_id: 分组ID（可选）

        Returns:
            上传成功返回协作文档信息字典，失败返回None
        """
        url = f"{self.base_url}{ERP_UPLOAD_COOPERATION_URL}"

        logger.info(f"协作文档上传: {file_path}, project_id={project_id}, name={name}")

        if not os.path.exists(file_path):
            logger.error(f"✗ 文件不存在: {file_path}")
            return None

        for attempt in range(ERP_MAX_RETRIES):
            try:
                data = {"project_id": project_id, "name": name}
                if group_id is not None:
                    data["group_id"] = group_id

                headers = {"X-Api-Key": self.api_key}

                with open(file_path, "rb") as f:
                    files = {"file": (os.path.basename(file_path), f, "application/octet-stream")}
                    resp = requests.post(url, headers=headers, files=files, data=data, timeout=ERP_REQUEST_TIMEOUT)

                result = resp.json()
                if result.get("success") == 1:
                    file_info = result["data"]
                    logger.info(f"✓ 协作文档上传成功! 文档ID: {file_info.get('id')}")
                    return file_info
                else:
                    error_code = result.get("error", 0)
                    if 400 <= error_code < 500:
                        logger.error(f"✗ 协作文档上传失败(客户端错误 {error_code})")
                        if error_code == 40000015:
                            logger.error("  → 提示：请确认 API Key 已授权 upload_cooperation 权限")
                        return None
                    if attempt < ERP_MAX_RETRIES - 1:
                        logger.warning(f"协作文档上传重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                        time.sleep(ERP_RETRY_INTERVAL)
                    else:
                        logger.error(f"✗ 协作文档上传失败(已达最大重试次数)")

            except requests.exceptions.Timeout:
                logger.warning(f"协作文档上传超时，重试 ({attempt+1}/{ERP_MAX_RETRIES})")
                if attempt < ERP_MAX_RETRIES - 1:
                    time.sleep(ERP_RETRY_INTERVAL)
            except Exception as e:
                logger.error(f"✗ 协作文档上传异常: {str(e)}")
                break

        return None

    # ==================== 内部辅助方法 ====================

    def _validate_and_save_image(self, img_bytes: bytes, idx: int, content_type: str) -> bool:
        """验证图片有效性并保存到temp目录"""
        if ERP_VALIDATE_IMAGES:
            try:
                from PIL import Image
                img = Image.open(BytesIO(img_bytes))
                img.verify()
                img = Image.open(BytesIO(img_bytes))
                logger.info(f"图片验证通过: {img.format}, {img.size[0]}x{img.size[1]}")
            except Exception as e:
                logger.warning(f"⚠ 图片验证失败: {str(e)}")
                return False

        if ERP_SAVE_IMAGES_TEMP and self.temp_dir:
            try:
                ext_map = {
                    "image/png": ".png", "image/jpeg": ".jpg",
                    "image/jpg": ".jpg", "image/gif": ".gif", "image/bmp": ".bmp",
                }
                ext = ext_map.get(content_type, ".png")
                save_dir = self.temp_dir / "erp_upload_images"
                save_dir.mkdir(parents=True, exist_ok=True)
                save_path = save_dir / f"image_{idx}{ext}"
                save_path.write_bytes(img_bytes)
                logger.info(f"图片已保存: {save_path}")
            except Exception as e:
                logger.warning(f"⚠ 保存图片失败: {str(e)}")

        return True

    def _save_html_temp(self, html_content: str):
        """保存HTML到temp目录供调试"""
        try:
            from datetime import datetime
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            save_dir = self.temp_dir / "erp_upload_html"
            save_dir.mkdir(parents=True, exist_ok=True)
            save_path = save_dir / f"report_{timestamp}.html"
            save_path.write_text(html_content, encoding="utf-8")
            logger.info(f"✓ HTML已保存到: {save_path}")
        except Exception as e:
            logger.warning(f"⚠ 保存HTML失败: {str(e)}")