"""3MF 文件元数据读取与处理模块。

提供从 3MF 归档文件中提取切片信息、耗材数据、模型设置等元数据的功能，
以及基于元数据生成构建摘要（时间预估、耗材用量等）的辅助函数。
"""

from __future__ import annotations

import zipfile
from pathlib import Path
from typing import Iterable
from xml.etree import ElementTree as ET

from .archive import GCODE_MEMBER_RE, list_gcode_members
from .models import BuildOptions, BuildSummary, PlateJob, PlateSource, ThreeMfSummary


def read_slice_plate_metadata(archive: zipfile.ZipFile) -> dict[int, dict[str, str]]:
    """读取 3MF 归档中各打印板的切片元数据。

    从 Metadata/slice_info.config 中解析每个 <plate> 元素内的 <metadata> 键值对，
    按盘索引号 index 组织为字典。

    参数:
        archive: 已打开的 3MF 压缩文件对象。

    返回:
        dict[int, dict[str, str]]: plate_index -> {metadata_key: metadata_value}，
        若配置文件不存在则返回空字典。
    """
    try:
        data = archive.read("Metadata/slice_info.config")
    except KeyError:
        return {}
    root = ET.fromstring(data.decode("utf-8-sig", errors="replace"))
    result: dict[int, dict[str, str]] = {}
    for plate in root.findall("plate"):
        plate_data: dict[str, str] = {}
        for metadata in plate.findall("metadata"):
            key = metadata.attrib.get("key")
            value = metadata.attrib.get("value")
            if key is not None and value is not None:
                plate_data[key] = value
        # 使用 index 字段确定盘编号，缺失或无效时自动递增
        index_text = plate_data.get("index")
        try:
            index = int(index_text) if index_text else len(result) + 1
        except ValueError:
            index = len(result) + 1
        result[index] = plate_data
    return result


def read_filament_metadata(archive: zipfile.ZipFile) -> dict[int, dict[str, str]]:
    """读取 3MF 归档中各打印板的耗材（filament）元数据。

    从 Metadata/slice_info.config 中提取每个 <plate> 下 <filament> 元素的属性，
    按盘索引号组织。

    参数:
        archive: 已打开的 3MF 压缩文件对象。

    返回:
        dict[int, dict[str, str]]: plate_index -> {filament_attr: value}，
        若配置文件不存在则返回空字典。
    """
    try:
        data = archive.read("Metadata/slice_info.config")
    except KeyError:
        return {}
    root = ET.fromstring(data.decode("utf-8-sig", errors="replace"))
    result: dict[int, dict[str, str]] = {}
    for plate in root.findall("plate"):
        plate_index = 1
        # 先获取当前盘的 index 编号
        for metadata in plate.findall("metadata"):
            if metadata.attrib.get("key") == "index":
                try:
                    plate_index = int(metadata.attrib.get("value", "1"))
                except ValueError:
                    plate_index = 1
                break
        filament = plate.find("filament")
        if filament is not None:
            result[plate_index] = dict(filament.attrib)
    return result


def read_model_settings_gcode_members(archive: zipfile.ZipFile) -> list[str]:
    """读取模型设置中配置的 G-code 文件成员名称列表。

    从 Metadata/model_settings.config 中提取所有 plate 的 gcode_file 元数据值，
    用于确定需要处理的 G-code 归档成员。

    参数:
        archive: 已打开的 3MF 压缩文件对象。

    返回:
        list[str]: 符合 G-code 命名规范的成员路径列表，若配置不存在则返回空列表。
    """
    try:
        data = archive.read("Metadata/model_settings.config")
    except KeyError:
        return []
    try:
        root = ET.fromstring(data.decode("utf-8-sig", errors="replace"))
    except Exception:
        return []
    members: list[str] = []
    for plate in root.findall("plate"):
        for metadata in plate.findall("metadata"):
            if metadata.attrib.get("key") != "gcode_file":
                continue
            # 规范化路径分隔符并去掉前导斜杠
            value = metadata.attrib.get("value", "").strip().replace("\\", "/").lstrip("/")
            if value and GCODE_MEMBER_RE.match(value):
                members.append(value)
            break
    return members


def safe_float(value: str | None) -> float | None:
    """安全地将字符串转换为浮点数，失败时返回 None。

    参数:
        value: 待转换的字符串或 None。

    返回:
        float | None: 转换成功返回浮点数，否则返回 None。
    """
    if value is None:
        return None
    try:
        return float(value)
    except ValueError:
        return None


def _sum_optional(values: Iterable[float | None]) -> float | None:
    """对一组可选浮点值求和，全为 None 时返回 None。

    参数:
        values: 包含 float 或 None 的可迭代对象。

    返回:
        float | None: 有效值的总和，若无任何有效值则返回 None。
    """
    total = 0.0
    found = False
    for value in values:
        if value is not None:
            total += value
            found = True
    return total if found else None


def read_3mf_summary(source_3mf: Path) -> ThreeMfSummary:
    """读取单个 3MF 文件的完整摘要信息。

    汇总文件内所有打印板的预估时间、重量、耗材用量等数据。

    参数:
        source_3mf: 3MF 源文件路径。

    返回:
        ThreeMfSummary: 包含盘数量、预估秒数、重量克数、耗材米数和克数的摘要对象。

    抛出:
        ValueError: 若文件中未找到任何 G-code 成员。
    """
    with zipfile.ZipFile(source_3mf, "r") as archive:
        members = list_gcode_members(archive)
        if not members:
            raise ValueError(f"No Metadata/plate_N.gcode member was found in {source_3mf}")
        plate_metadata = read_slice_plate_metadata(archive)
        filament_metadata = read_filament_metadata(archive)

    predictions: list[float | None] = []
    weights: list[float | None] = []
    used_m_values: list[float | None] = []
    used_g_values: list[float | None] = []
    for member in members:
        match = GCODE_MEMBER_RE.match(member)
        plate_index = int(match.group(1)) if match else 1
        metadata = plate_metadata.get(plate_index, {})
        filament = filament_metadata.get(plate_index, {})
        # 提取各项元数据值
        prediction = safe_float(metadata.get("prediction"))
        weight = safe_float(metadata.get("weight"))
        used_m = safe_float(filament.get("used_m"))
        used_g = safe_float(filament.get("used_g"))
        predictions.append(prediction)
        # 重量优先取 weight，若无则回退到 used_g
        weights.append(weight if weight is not None else used_g)
        used_m_values.append(used_m)
        # 耗材克数优先取 used_g，若无则回退到 weight
        used_g_values.append(used_g if used_g is not None else weight)

    return ThreeMfSummary(
        source_3mf=source_3mf,
        plate_count=len(members),
        prediction_seconds=_sum_optional(predictions),
        weight_grams=_sum_optional(weights),
        filament_used_m=_sum_optional(used_m_values),
        filament_used_g=_sum_optional(used_g_values),
    )


def multiply_summary(summary: ThreeMfSummary, copies: int) -> BuildSummary:
    """将单个 3MF 摘要按指定份数倍增，生成构建摘要。

    用于计算同一文件多次打印时的累计耗时和耗材用量。

    参数:
        summary: 单个 3MF 文件的摘要对象。
        copies: 打印份数（至少为 1）。

    返回:
        BuildSummary: 倍增后的构建摘要。
    """
    multiplier = max(1, int(copies))
    return BuildSummary(
        plate_count=summary.plate_count * multiplier,
        total_prediction_seconds=None if summary.prediction_seconds is None else summary.prediction_seconds * multiplier,
        total_weight_grams=None if summary.weight_grams is None else summary.weight_grams * multiplier,
        total_filament_used_m=None if summary.filament_used_m is None else summary.filament_used_m * multiplier,
        total_filament_used_g=None if summary.filament_used_g is None else summary.filament_used_g * multiplier,
    )


def summarize_jobs(jobs: Iterable[PlateJob]) -> BuildSummary:
    """汇总所有换板作业的构建摘要。

    遍历所有 PlateJob，依次读取各自的 3MF 摘要并按份数倍增后累加。

    参数:
        jobs: PlateJob 对象的可迭代集合。

    返回:
        BuildSummary: 所有作业合并后的构建摘要。
    """
    plate_count = 0
    predictions: list[float | None] = []
    weights: list[float | None] = []
    used_m_values: list[float | None] = []
    used_g_values: list[float | None] = []
    for job in jobs:
        summary = multiply_summary(read_3mf_summary(job.source_3mf), job.copies)
        plate_count += summary.plate_count
        predictions.append(summary.total_prediction_seconds)
        weights.append(summary.total_weight_grams)
        used_m_values.append(summary.total_filament_used_m)
        used_g_values.append(summary.total_filament_used_g)
    return BuildSummary(
        plate_count=plate_count,
        total_prediction_seconds=_sum_optional(predictions),
        total_weight_grams=_sum_optional(weights),
        total_filament_used_m=_sum_optional(used_m_values),
        total_filament_used_g=_sum_optional(used_g_values),
    )


def update_first_slice_info(base_data: bytes, sources: list[PlateSource], options: BuildOptions) -> bytes:
    """更新打包后 3MF 中首个 slice_info.config 的元数据。

    根据 metadata_mode 选项决定是保留源数据还是累加所有来源的预估时间和耗材用量。

    参数:
        base_data: 原始 slice_info.config 的字节数据。
        sources: 所有 PlateSource 的列表。
        options: 构建选项（含 metadata_mode 字段）。

    返回:
        bytes: 更新后的 slice_info.config 字节数据。
    """
    if options.metadata_mode == "source":
        # 源模式：保持原始数据不变
        return base_data
    try:
        root = ET.fromstring(base_data.decode("utf-8-sig", errors="replace"))
    except Exception:
        return base_data
    # 累加所有来源的预估时间和耗材用量
    total_prediction = sum(value for value in (source.prediction_seconds for source in sources) if value is not None)
    total_weight = sum(value for value in (source.weight_grams for source in sources) if value is not None)
    total_used_m = sum(value for value in (source.filament_used_m for source in sources) if value is not None)
    total_used_g = sum(value for value in (source.filament_used_g for source in sources) if value is not None)
    first_plate = root.find("plate")
    if first_plate is None:
        return base_data
    # 更新第一个 plate 的 metadata 和 filament 字段
    for metadata in first_plate.findall("metadata"):
        key = metadata.attrib.get("key")
        if key == "prediction" and total_prediction > 0:
            metadata.set("value", str(int(round(total_prediction))))
        elif key == "weight" and total_weight > 0:
            metadata.set("value", f"{total_weight:.2f}")
    filament = first_plate.find("filament")
    if filament is not None:
        if total_used_m > 0:
            filament.set("used_m", f"{total_used_m:.2f}")
        if total_used_g > 0:
            filament.set("used_g", f"{total_used_g:.2f}")
    return ET.tostring(root, encoding="utf-8", xml_declaration=True)
