#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
将「大纲」或「frontend-slides 生成的 HTML 演示文稿」转为 .pptx。
支持：1）python-pptx 生成后后处理注入背景色/文字色；2）从 OOXML 模板克隆每页 slide XML 再合并成 pptx，最大限度保留样式。
"""
import html
import logging
import re
import zipfile
from io import BytesIO
from typing import Any

import requests
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.util import Inches

logger = logging.getLogger(__name__)

# 默认主题：科技蓝（与 HTML 模板一致）
DEFAULT_BG_HEX = "0C1929"
DEFAULT_TITLE_HEX = "F0F9FF"
DEFAULT_BODY_HEX = "CBD5E1"


def _strip_html(raw: str) -> str:
    """去掉 HTML 标签并解码实体，得到纯文本。"""
    if not raw:
        return ""
    s = re.sub(r"<[^>]+>", " ", raw)
    s = re.sub(r"\s+", " ", s).strip()
    return html.unescape(s)


def parse_html_to_outline(html_content: str) -> list[dict[str, Any]]:
    """
    从 frontend-slides 风格的单文件 HTML 中解析出大纲 [{ title, points }, ...]。
    兼容结构：<section class="slide"> 内含标题（h1/h2）与要点（ul li 或 p）。
    若无法解析则返回空列表。
    """
    if not html_content or not html_content.strip():
        return []
    outline: list[dict[str, Any]] = []
    # 按 section.slide 切分；class 可能为 "slide" / 'slide' / "slide title-slide" 等（不捕获组，避免 split 结果错位）
    section_start = re.compile(
        r"<section[^>]*\bclass\s*=\s*(?:[\"'][^\"']*\bslide\b[^\"']*[\"'])[^>]*>",
        re.IGNORECASE,
    )
    parts = section_start.split(html_content)
    for i, block in enumerate(parts):
        if i == 0:
            continue
        end = block.find("</section>")
        if end != -1:
            block = block[:end]
        title = ""
        points: list[str] = []
        image_url = ""
        # 第一个 h1 或 h2 作为标题
        m = re.search(r"<h[12][^>]*>(.*?)</h[12]>", block, re.DOTALL | re.IGNORECASE)
        if m:
            title = _strip_html(m.group(1))
        # 所有 li 作为要点
        for m in re.finditer(r"<li[^>]*>(.*?)</li>", block, re.DOTALL | re.IGNORECASE):
            pt = _strip_html(m.group(1))
            if pt:
                points.append(pt)
        # 若无 li，可用 p 作为单段“要点”
        if not points:
            for m in re.finditer(r"<p[^>]*>(.*?)</p>", block, re.DOTALL | re.IGNORECASE):
                pt = _strip_html(m.group(1))
                if pt and not pt.startswith("<!--"):
                    points.append(pt)
        # 本页配图：.slide-image img 或任意 img 的 src
        img_m = re.search(r'<img[^>]+src\s*=\s*["\']([^"\']+)["\']', block, re.IGNORECASE)
        if img_m:
            image_url = html.unescape(img_m.group(1).strip())
        outline.append({
            "title": title or f"Slide {len(outline) + 1}",
            "points": points,
            "image_url": image_url,
        })
    return outline


def _fetch_image_bytes(url: str, timeout: int = 15) -> BytesIO | None:
    """从 URL 下载图片并返回 BytesIO，失败返回 None。"""
    if not url or not url.strip().lower().startswith(("http://", "https://")):
        return None
    try:
        resp = requests.get(url.strip(), timeout=timeout, stream=True)
        resp.raise_for_status()
        bio = BytesIO(resp.content)
        bio.seek(0)
        return bio
    except Exception as e:
        logger.warning("Fetch image for pptx failed %s: %s", url[:80], e)
        return None


def outline_to_pptx(outline: list[dict[str, Any]], output_path: str) -> None:
    """
    根据大纲生成 .pptx 并写入 output_path。
    outline: [{ "title": str, "points": [str, ...], "image_url"?: str }, ...]，含 image_url 时在页内插入配图。
    """
    prs = Presentation()
    title_layout = prs.slide_layouts[0]
    content_layout = prs.slide_layouts[1]
    # 配图在幻灯片上的位置与尺寸（右侧，与 HTML 左文右图一致）
    IMG_LEFT = Inches(5.5)
    IMG_TOP = Inches(1.6)
    IMG_WIDTH = Inches(4.0)
    IMG_HEIGHT = Inches(4.2)

    for i, item in enumerate(outline):
        if not isinstance(item, dict):
            continue
        title = (item.get("title") or "").strip() or f"Slide {i + 1}"
        points = item.get("points")
        if not isinstance(points, list):
            points = []
        image_url = (item.get("image_url") or "").strip()

        if i == 0 and not points:
            slide = prs.slides.add_slide(title_layout)
            slide.shapes.title.text = title
            if image_url:
                img_io = _fetch_image_bytes(image_url)
                if img_io:
                    try:
                        slide.shapes.add_picture(img_io, IMG_LEFT, IMG_TOP, IMG_WIDTH, IMG_HEIGHT)
                    except Exception as e:
                        logger.warning("Add picture to title slide failed: %s", e)
            continue

        slide = prs.slides.add_slide(content_layout)
        slide.shapes.title.text = title
        body = slide.shapes.placeholders[1]
        tf = body.text_frame
        tf.clear()
        for j, pt in enumerate(points[:8]):
            text = (str(pt) or "").strip()
            if j == 0:
                tf.paragraphs[0].text = text
            else:
                p = tf.add_paragraph()
                p.text = text
                p.level = 0
        if image_url:
            img_io = _fetch_image_bytes(image_url)
            if img_io:
                try:
                    slide.shapes.add_picture(img_io, IMG_LEFT, IMG_TOP, IMG_WIDTH, IMG_HEIGHT)
                except Exception as e:
                    logger.warning("Add picture to slide %s failed: %s", i + 1, e)

    prs.save(output_path)
    # 后处理：向已生成的 pptx 注入背景色与文字颜色，最大限度保留与 HTML 一致的风格
    _apply_pptx_theme(output_path, DEFAULT_BG_HEX, DEFAULT_TITLE_HEX, DEFAULT_BODY_HEX)


def _apply_pptx_theme(
    pptx_path: str,
    bg_hex: str = DEFAULT_BG_HEX,
    title_hex: str = DEFAULT_TITLE_HEX,
    body_hex: str = DEFAULT_BODY_HEX,
) -> None:
    """
    打开已存在的 .pptx（zip），对每页 slide 的 XML 注入背景色（p:bg）与文字颜色（a:rPr solidFill），
    写回 zip。bg_hex/title_hex/body_hex 为 6 位 RRGGBB（无 #）。用字符串插入避免破坏命名空间。
    """
    bg_hex = bg_hex.upper().replace("#", "")[:6]
    title_hex = title_hex.upper().replace("#", "")[:6]
    body_hex = body_hex.upper().replace("#", "")[:6]
    bg_block = (
        f'<p:bg><p:bgPr><a:solidFill><a:srgbClr val="{bg_hex}"/></a:solidFill></p:bgPr></p:bg>'
    )
    rpr_title = (
        f'<a:rPr><a:solidFill><a:srgbClr val="{title_hex}"/></a:solidFill></a:rPr>'
    )
    rpr_body = (
        f'<a:rPr><a:solidFill><a:srgbClr val="{body_hex}"/></a:solidFill></a:rPr>'
    )

    try:
        with zipfile.ZipFile(pptx_path, "r") as zf:
            slide_names = [n for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml")]
    except Exception as e:
        logger.warning("Apply pptx theme: open zip failed %s: %s", pptx_path, e)
        return

    modified = {}
    for name in sorted(slide_names):
        try:
            with zipfile.ZipFile(pptx_path, "r") as zf:
                xml_str = zf.read(name).decode("utf-8")
            # 若无 p:bg，在 <p:spTree> 前插入背景块
            if "<p:bg>" not in xml_str and "<p:spTree>" in xml_str:
                xml_str = xml_str.replace("<p:spTree>", bg_block + "\n    <p:spTree>", 1)
            # 为 <a:r> 内无 <a:rPr> 的补上文字颜色（第一个 a:r 用标题色，其余用正文色）
            run_count = [0]
            def add_rpr(m):
                run_count[0] += 1
                full = m.group(0)
                if "<a:rPr>" in full:
                    return full
                rpr = rpr_title if run_count[0] == 1 else rpr_body
                return "<a:r>" + rpr + m.group(1) + "</a:r>"
            xml_str = re.sub(r"<a:r>(.*?)</a:r>", add_rpr, xml_str, flags=re.DOTALL)
            modified[name] = xml_str.encode("utf-8")
        except Exception as e:
            logger.warning("Apply pptx theme slide %s: %s", name, e)

    if not modified:
        return
    try:
        with zipfile.ZipFile(pptx_path, "r") as zf:
            all_names = zf.namelist()
            other_data = {n: zf.read(n) for n in all_names if n not in modified}
        other_data.update(modified)
        with zipfile.ZipFile(pptx_path, "w", zipfile.ZIP_DEFLATED) as zf:
            for n in sorted(other_data.keys()):
                zf.writestr(n, other_data[n])
    except Exception as e:
        logger.warning("Apply pptx theme write back failed: %s", e)


def html_to_pptx(html_content: str, output_path: str) -> list[dict[str, Any]]:
    """
    将 HTML 解析为大纲并生成 .pptx。返回解析得到的大纲；若解析失败则抛出 ValueError。
    """
    outline = parse_html_to_outline(html_content)
    if not outline:
        raise ValueError("未能从 HTML 中解析出幻灯片结构（需包含 section.slide）")
    outline_to_pptx(outline, output_path)
    return outline
