"""绘图辅助函数 — 在 slide 上添加文本、形状、表格、图片、页眉/页脚"""

import io
import re
from pathlib import Path
from urllib.request import urlopen, Request
import base64

from PIL import Image as PILImage
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE


def _rgb(hex_str: str) -> RGBColor:
    """'1E2761' -> RGBColor"""
    return RGBColor.from_string(hex_str)


def set_slide_bg(slide, color_hex: str):
    """设置幻灯片纯色背景"""
    bg = slide.background
    fill = bg.fill
    fill.solid()
    fill.fore_color.rgb = _rgb(color_hex)


def add_text(slide, text: str, x, y, w, h, *,
             font_size=14, font_name="微软雅黑", color="212121",
             bold=False, italic=False,
             align="left", valign="top",
             word_wrap=True, margin_pt=None):
    """在 slide 上添加文本框，返回 shape"""
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    width = Inches(w) if isinstance(w, (int, float)) else w
    height = Inches(h) if isinstance(h, (int, float)) else h

    txBox = slide.shapes.add_textbox(left, top, width, height)
    tf = txBox.text_frame
    tf.word_wrap = word_wrap

    if margin_pt is not None:
        tf.margin_left = Pt(margin_pt)
        tf.margin_right = Pt(margin_pt)
        tf.margin_top = Pt(margin_pt)
        tf.margin_bottom = Pt(margin_pt)

    # 段落
    p = tf.paragraphs[0]
    p.text = text

    align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}
    p.alignment = align_map.get(align, PP_ALIGN.LEFT)

    # 字体
    run = p.runs[0] if p.runs else p.add_run()
    if not p.runs:
        run.text = text
    run.font.size = Pt(font_size)
    run.font.name = font_name
    run.font.color.rgb = _rgb(color)
    run.font.bold = bold
    run.font.italic = italic

    # 垂直对齐
    from pptx.oxml.ns import qn
    bodyPr = tf._txBody.find(qn("a:bodyPr"))
    if bodyPr is not None:
        anchor_map = {"top": "t", "middle": "ctr", "bottom": "b"}
        bodyPr.set("anchor", anchor_map.get(valign, "t"))

    return txBox


def add_rich_text(slide, parts, x, y, w, h, *,
                  font_name="微软雅黑", align="left", valign="top",
                  line_spacing=1.2, margin_pt=None):
    """添加富文本框 — parts 为 [{"text": ..., "size": ..., "color": ..., "bold": ...}, ...]
    支持 newline=True 换行"""
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    width = Inches(w) if isinstance(w, (int, float)) else w
    height = Inches(h) if isinstance(h, (int, float)) else h

    txBox = slide.shapes.add_textbox(left, top, width, height)
    tf = txBox.text_frame
    tf.word_wrap = True

    if margin_pt is not None:
        tf.margin_left = Pt(margin_pt)
        tf.margin_right = Pt(margin_pt)
        tf.margin_top = Pt(margin_pt)
        tf.margin_bottom = Pt(margin_pt)

    align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}

    p = tf.paragraphs[0]
    p.alignment = align_map.get(align, PP_ALIGN.LEFT)

    for i, part in enumerate(parts):
        if part.get("newline") and i > 0:
            p = tf.add_paragraph()
            p.alignment = align_map.get(align, PP_ALIGN.LEFT)
            if line_spacing != 1.0:
                p.line_spacing = Pt(int(part.get("size", 14) * line_spacing))

        run = p.add_run()
        run.text = part["text"]
        run.font.size = Pt(part.get("size", 14))
        run.font.name = part.get("font", font_name)
        run.font.color.rgb = _rgb(part.get("color", "212121"))
        run.font.bold = part.get("bold", False)
        run.font.italic = part.get("italic", False)

    return txBox


def add_shape(slide, shape_type, x, y, w, h, *,
              fill=None, line_color=None, line_width=None):
    """添加形状"""
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    width = Inches(w) if isinstance(w, (int, float)) else w
    height = Inches(h) if isinstance(h, (int, float)) else h

    shape = slide.shapes.add_shape(shape_type, left, top, width, height)

    if fill:
        shape.fill.solid()
        shape.fill.fore_color.rgb = _rgb(fill)
    else:
        shape.fill.background()

    if line_color:
        shape.line.color.rgb = _rgb(line_color)
        shape.line.width = Pt(line_width or 1)
    else:
        shape.line.fill.background()

    return shape


def add_rect(slide, x, y, w, h, **kwargs):
    return add_shape(slide, MSO_SHAPE.RECTANGLE, x, y, w, h, **kwargs)


def add_rounded_rect(slide, x, y, w, h, **kwargs):
    return add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h, **kwargs)


def add_line(slide, x, y, w, *, color="E0E0E0", width=1):
    """添加水平线"""
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    w_emu = Inches(w) if isinstance(w, (int, float)) else w

    shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, w_emu, Pt(width))
    shape.fill.solid()
    shape.fill.fore_color.rgb = _rgb(color)
    shape.line.fill.background()
    return shape


# ── Picture ────────────────────────────────────────────────────────────

def _resolve_image_source(image_src):
    """将图片来源统一为 (BytesIO stream, (width, height)) 元组

    支持三种来源:
    - 本地文件路径 (str / Path)
    - URL (http:// / https://)
    - base64 字符串 ("data:image/...;base64,..." 或裸 base64)

    返回 (stream, (img_w, img_h))。stream 可直接传给 python-pptx。
    """
    src = str(image_src)

    if src.startswith(("http://", "https://")):
        req = Request(src, headers={"User-Agent": "ppt-station/1.0"})
        resp = urlopen(req, timeout=30)
        raw_bytes = resp.read()
    elif src.startswith("data:"):
        # data URI: "data:image/png;base64,iVBOR..."
        b64_data = src.split(";base64,", 1)[1]
        raw_bytes = base64.b64decode(b64_data)
    else:
        # 先尝试本地文件，不存在则当作裸 base64
        p = Path(src).expanduser().resolve()
        if p.exists():
            raw_bytes = p.read_bytes()
        else:
            try:
                raw_bytes = base64.b64decode(src)
            except Exception:
                raise FileNotFoundError(f"图片文件不存在: {p}")

    # 用独立 BytesIO 读尺寸，避免 PIL 关闭影响主 stream
    with PILImage.open(io.BytesIO(raw_bytes)) as pil:
        size = pil.size

    return io.BytesIO(raw_bytes), size


def add_picture(slide, image_src, x, y, w, h, *, sizing="stretch"):
    """在 slide 上插入图片，返回 Picture shape

    Args:
        image_src: 图片来源 — 本地路径 / URL / base64 字符串
        x, y, w, h: 目标区域（英寸或 Emu）
        sizing:
            "stretch"  — 拉伸填满目标区域（可能变形）
            "contain"  — 等比缩放适配，居中显示（可能留白）
            "cover"    — 等比缩放裁切填满（无变形无留白）
    """
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    width = Inches(w) if isinstance(w, (int, float)) else w
    height = Inches(h) if isinstance(h, (int, float)) else h

    stream, (img_w, img_h) = _resolve_image_source(image_src)

    if sizing == "stretch":
        return slide.shapes.add_picture(stream, left, top, width, height)

    target_ratio = w / h if isinstance(w, (int, float)) and isinstance(h, (int, float)) else (width / height)
    img_ratio = img_w / img_h

    if sizing == "contain":
        if img_ratio > target_ratio:
            # 图片更宽 → 宽度撑满，高度居中
            final_w = w if isinstance(w, (int, float)) else width
            final_h = (w / img_ratio) if isinstance(w, (int, float)) else int(width / img_ratio)
        else:
            # 图片更高 → 高度撑满，宽度居中
            final_h = h if isinstance(h, (int, float)) else height
            final_w = (h * img_ratio) if isinstance(h, (int, float)) else int(height * img_ratio)

        if isinstance(w, (int, float)):
            offset_x = x + (w - final_w) / 2
            offset_y = y + (h - final_h) / 2
            return slide.shapes.add_picture(
                stream,
                Inches(offset_x), Inches(offset_y),
                Inches(final_w), Inches(final_h))
        else:
            offset_x = left + (width - final_w) // 2
            offset_y = top + (height - final_h) // 2
            return slide.shapes.add_picture(stream, offset_x, offset_y, final_w, final_h)

    if sizing == "cover":
        # 先拉伸填满目标区域
        pic = slide.shapes.add_picture(stream, left, top, width, height)
        # 通过 srcRect crop 恢复宽高比
        if img_ratio > target_ratio:
            # 图片更宽 → 裁左右
            crop = (1 - target_ratio / img_ratio) / 2
            _set_crop(pic, left=crop, right=crop)
        elif img_ratio < target_ratio:
            # 图片更高 → 裁上下
            crop = (1 - img_ratio / target_ratio) / 2
            _set_crop(pic, top=crop, bottom=crop)
        return pic

    raise ValueError(f"未知 sizing 模式: {sizing}")


def _set_crop(pic, *, left=0, right=0, top=0, bottom=0):
    """通过 OOXML srcRect 设置图片裁切"""
    from pptx.oxml.ns import qn
    from lxml import etree

    blipFill = pic._element.find(qn("p:blipFill"))
    if blipFill is None:
        return

    srcRect = blipFill.find(qn("a:srcRect"))
    if srcRect is None:
        srcRect = etree.SubElement(blipFill, qn("a:srcRect"))

    # OOXML srcRect 用千分比（1000 = 1%，100000 = 100%）
    if left:
        srcRect.set("l", str(int(left * 100000)))
    if right:
        srcRect.set("r", str(int(right * 100000)))
    if top:
        srcRect.set("t", str(int(top * 100000)))
    if bottom:
        srcRect.set("b", str(int(bottom * 100000)))


# ── Content Page Setup ─────────────────────────────────────────────────

def setup_content_page(slide, data, theme):
    """内容页通用初始化：背景 + 页眉标题

    返回常用布局参数元组 (m, sw, content_y, content_w, footer_y)。
    """
    m = theme["margin"]
    sw = theme.get("slide_w", 13.333)
    content_y = theme.get("content_y", 1.00)
    content_w = theme.get("content_w", sw - 2 * m)
    footer_y = theme.get("footer_y", 7.10)

    set_slide_bg(slide, theme["bg_light"])
    add_page_header(slide, data["title"], theme)

    return m, sw, content_y, content_w, footer_y


# ── Page Header / Footer (投行级标准) ──────────────────────────────

def add_page_header(slide, title, theme):
    """投行级标准页眉: 16pt 标题 + 全宽分隔线

    使用 theme tokens: page_title_size, header_y, divider_y, divider_h, content_w
    """
    m = theme["margin"]
    sw = theme.get("slide_w", 13.333)
    page_title_size = theme.get("page_title_size", 16)
    header_y = theme.get("header_y", 0.25)
    divider_y = theme.get("divider_y", 0.80)
    divider_h = theme.get("divider_h", 0.015)
    content_w = theme.get("content_w", sw - 2 * m)

    # 页面标题
    add_text(slide, title,
             x=m, y=header_y, w=content_w, h=0.5,
             font_size=page_title_size, color=theme["text_dark"],
             bold=True, font_name=theme["header_font"])

    # 全宽分隔线
    add_rect(slide, m, divider_y, content_w, divider_h, fill=theme["primary"])


def add_page_footer(slide, theme, source="", page_num=None, brand=""):
    """投行级标准页脚: 左侧来源 8pt + 右侧页码/品牌

    使用 theme tokens: footer_size, footer_y, content_w
    """
    m = theme["margin"]
    sw = theme.get("slide_w", 13.333)
    footer_size = theme.get("footer_size", 8)
    footer_y = theme.get("footer_y", 7.10)
    content_w = theme.get("content_w", sw - 2 * m)

    # 页脚分隔细线
    add_rect(slide, m, footer_y - 0.08, content_w, 0.005, fill=theme["border"])

    # 左侧: 来源
    if source:
        source_text = f"Source: {source}" if not source.startswith("Source") else source
        add_text(slide, source_text,
                 x=m, y=footer_y, w=content_w * 0.7, h=0.25,
                 font_size=footer_size, color=theme["text_muted"],
                 italic=True, font_name=theme["body_font"])

    # 右侧: 页码/品牌
    right_parts = []
    if brand:
        right_parts.append(brand)
    if page_num is not None:
        right_parts.append(str(page_num))
    if right_parts:
        add_text(slide, "  |  ".join(right_parts),
                 x=m + content_w * 0.3, y=footer_y, w=content_w * 0.7, h=0.25,
                 font_size=footer_size, color=theme["text_muted"],
                 align="right", font_name=theme["body_font"])


# ── Table (投行级) ──────────────────────────────────────────────────────

_NUM_PATTERN = re.compile(r'^[\s]*[-+]?[\d,]+\.?\d*\s*[%xX倍万亿]?\s*$')


def _is_numeric_cell(val: str) -> bool:
    """检测单元格值是否为数值型（用于右对齐）"""
    if not val:
        return False
    return bool(_NUM_PATTERN.match(val))


def add_table(slide, headers, rows, x, y, w, h, *,
              theme=None, header_bg=None, header_color="FFFFFF",
              font_name="微软雅黑", font_size=11,
              highlight_row=None, highlight_bg="EBF5FF",
              highlight_cols=None,
              ib_style=True):
    """添加带样式的表格

    Args:
        highlight_cols: 高亮列索引列表 (如 [2, 3])
        ib_style: True 启用投行级样式（无垂直边框、数值右对齐、斑马纹 token）
    """
    left = Inches(x) if isinstance(x, (int, float)) else x
    top = Inches(y) if isinstance(y, (int, float)) else y
    width = Inches(w) if isinstance(w, (int, float)) else w
    height = Inches(h) if isinstance(h, (int, float)) else h

    n_rows = len(rows) + 1
    n_cols = len(headers)

    table_shape = slide.shapes.add_table(n_rows, n_cols, left, top, width, height)
    table = table_shape.table

    # 均匀列宽
    col_w = int(width / n_cols)
    for j in range(n_cols):
        table.columns[j].width = col_w

    # 颜色 tokens
    hdr_bg = header_bg or (theme.get("table_header_bg", theme["primary"]) if theme else "36454F")
    zebra_even = theme.get("table_zebra_even", "F8FAFC") if theme else "F8FAFC"
    zebra_odd = theme.get("table_zebra_odd", "FFFFFF") if theme else "FFFFFF"
    highlight_col_bg = theme.get("table_zebra_even", "EBF5FF") if theme else "EBF5FF"

    # 预扫描: 检测哪些列是数值列（用于自动右对齐）
    numeric_cols = set()
    if ib_style and rows:
        for j in range(n_cols):
            num_count = sum(1 for row in rows if len(row) > j and _is_numeric_cell(str(row[j] or "")))
            if num_count > len(rows) * 0.5:
                numeric_cols.add(j)

    # 表头
    for j, hdr in enumerate(headers):
        cell = table.cell(0, j)
        cell.text = str(hdr)
        align = "right" if (ib_style and j in numeric_cols) else "center"
        _style_cell(cell, bg=hdr_bg, color=header_color,
                     font_name=font_name, font_size=font_size, bold=True,
                     align=align)

    # 数据行
    for i, row in enumerate(rows):
        is_highlight = (highlight_row is not None and i == highlight_row)
        for j, val in enumerate(row):
            cell = table.cell(i + 1, j)
            cell_text = str(val) if val is not None else ""
            cell.text = cell_text

            # 背景色优先级: highlight_row > highlight_cols > zebra
            if is_highlight:
                bg = highlight_bg
            elif highlight_cols and j in highlight_cols:
                bg = highlight_col_bg
            else:
                bg = zebra_even if i % 2 == 0 else zebra_odd

            # 对齐: 数值列右对齐, 首列左对齐, 其余居中
            if ib_style and j in numeric_cols:
                align = "right"
            elif j == 0:
                align = "left"
            else:
                align = "center"

            _style_cell(cell, bg=bg, color="212121",
                         font_name=font_name, font_size=font_size,
                         bold=is_highlight,
                         align=align)

    # 投行级: 移除垂直边框，仅保留水平线
    if ib_style:
        _remove_vertical_borders(table, n_rows, n_cols, hdr_bg)

    return table_shape


def _remove_vertical_borders(table, n_rows, n_cols, header_color):
    """移除垂直边框，仅保留表头上下 + 表格底部水平线"""
    from pptx.oxml.ns import qn
    from lxml import etree

    for i in range(n_rows):
        for j in range(n_cols):
            cell = table.cell(i, j)
            tc = cell._tc
            tcPr = tc.find(qn("a:tcPr"))
            if tcPr is None:
                tcPr = etree.SubElement(tc, qn("a:tcPr"))

            # 清除左右边框
            for border_name in ["lnL", "lnR"]:
                border = tcPr.find(qn(f"a:{border_name}"))
                if border is not None:
                    tcPr.remove(border)
                border = etree.SubElement(tcPr, qn(f"a:{border_name}"))
                border.set("w", "0")
                noFill = etree.SubElement(border, qn("a:noFill"))

            # 设置水平线: 表头底部 + 表格底部用主色细线，其余极浅
            for border_name in ["lnT", "lnB"]:
                border = tcPr.find(qn(f"a:{border_name}"))
                if border is not None:
                    tcPr.remove(border)

                is_header_bottom = (i == 0 and border_name == "lnB")
                is_table_bottom = (i == n_rows - 1 and border_name == "lnB")
                is_header_top = (i == 0 and border_name == "lnT")

                border = etree.SubElement(tcPr, qn(f"a:{border_name}"))
                if is_header_top or is_header_bottom or is_table_bottom:
                    border.set("w", "12700")  # 1pt
                    solidFill = etree.SubElement(border, qn("a:solidFill"))
                    srgbClr = etree.SubElement(solidFill, qn("a:srgbClr"))
                    srgbClr.set("val", header_color)
                else:
                    border.set("w", "6350")  # 0.5pt
                    solidFill = etree.SubElement(border, qn("a:solidFill"))
                    srgbClr = etree.SubElement(solidFill, qn("a:srgbClr"))
                    srgbClr.set("val", "E2E8F0")


def _style_cell(cell, *, bg, color, font_name, font_size, bold=False, align="left"):
    """设置单元格样式"""
    cell.fill.solid()
    cell.fill.fore_color.rgb = _rgb(bg)

    # 内边距
    cell.margin_left = Pt(6)
    cell.margin_right = Pt(6)
    cell.margin_top = Pt(4)
    cell.margin_bottom = Pt(4)

    align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}

    for paragraph in cell.text_frame.paragraphs:
        paragraph.alignment = align_map.get(align, PP_ALIGN.LEFT)
        for run in paragraph.runs:
            run.font.size = Pt(font_size)
            run.font.name = font_name
            run.font.color.rgb = _rgb(color)
            run.font.bold = bold
