#!/usr/bin/env python3
"""Build MKDRealtor.com cost proposal deck from WebsiteProposalTemplate.pptx.

Colors match Exit Realty Wheaton / proposal.css. Two cost sections:
  1) Build-out (one-time)
  2) Monthly maintenance
"""

from __future__ import annotations

import re
import shutil
from datetime import date
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE
from pptx.enum.text import PP_ALIGN
from pptx.oxml.ns import qn
from pptx.util import Inches, Pt

ROOT = Path(__file__).resolve().parent
TEMPLATE = ROOT / "WebsiteProposalTemplate.pptx"
OUTPUT = ROOT / "MonicaSite-CostProposal.pptx"
SHOTS = ROOT / "screenshots"
LOGO = ROOT / "aiml-operations-logo.png"

# proposal.css palette
MONICA = {
    "primary": "00626C",
    "primary_light": "008898",
    "primary_soft": "00A0AB",
    "navy": "212121",
    "ink": "212121",
    "sidebar": "080808",
    "muted": "6B7280",
    "surface": "F4F7F7",
    "border": "D9E3E4",
    "ok": "1A7F4B",
    "link": "7DD3DA",
    "white": "FFFFFF",
}

# Warm template accents → Monica blues
COLOR_MAP = {
    "63876D": MONICA["primary"],
    "F1C988": MONICA["primary_light"],
    "FF8D51": MONICA["primary_soft"],
    "CB5A48": MONICA["navy"],
    "9F4044": MONICA["ink"],
    "632F2F": MONICA["sidebar"],
    "181717": MONICA["ink"],
    "954F72": MONICA["primary_light"],
    "0563C1": MONICA["primary"],
}

TODAY = date(2026, 7, 15)
DATE_STR = TODAY.strftime("%d / %m / %Y")
COMPANY = "AI/ML Operations"
PROJECT = "MKDRealtor.com"

# --- Pricing (proposal estimates; adjust as needed) ---
BUILD_ITEMS = [
    ("Phase 0 — Scaffolding & deploy", "$1,800"),
    ("Phase 1 — Public site, leads, UTM, portal", "$4,800"),
    ("Phase 2 — Mailing list + email/SMS outreach", "$3,600"),
    ("Phase 3 — Postcards + live designer", "$2,400"),
    ("Phase 4 — Social automation (FB / IG / LI)", "$3,200"),
    ("Phase 5 — Hardening, monitoring, handoff", "$1,200"),
    ("Content polish & realtor training", "$1,000"),
]
BUILD_TOTAL = "$18,000"

MONTHLY_PLANS = {
    "headers": ["Essential", "Standard", "Recommended", "Full Care"],
    "rows": [
        ["Hosting & monitoring", "Yes", "Yes", "Yes", "Yes"],
        ["Security & dependency updates", "Yes", "Yes", "Yes", "Yes"],
        ["Bug fixes & uptime support", "Email", "Priority", "Priority", "Priority"],
        ["Minor content / copy tweaks", "—", "2 hrs/mo", "4 hrs/mo", "8 hrs/mo"],
        ["Campaign & social help", "—", "—", "Yes", "Yes"],
        ["Monthly health report", "—", "Yes", "Yes", "Yes"],
        ["$149/mo", "$249/mo", "$349/mo", "$499/mo"],
    ],
}

MONTHLY_PASS_THROUGH = [
    ("Email / SMS (SMTP2GO)", "Usage-based — billed at provider cost"),
    ("Postcards (Lob / Click2Mail)", "~$0.35–$0.77 per 4×6 piece"),
    ("Social APIs (Meta + LinkedIn)", "$0 — native APIs, no aggregator fee"),
    ("Infra (existing fleet)", "Near-zero marginal — shared Postgres only"),
]

# Screenshots from proposal/pages (see capture_screenshots.py)
UX_PAGES = [
    ("index", "UX gallery hub", "Overview"),
    ("public-home", "Marketing home", "Public · Phase 1"),
    ("public-about", "About", "Public · Phase 1"),
    ("public-contact", "Contact form", "Public · Phase 1"),
    ("public-unsubscribe", "Opt-out / unsubscribe", "Public · Phase 2"),
    ("public-404", "404 not found", "Public · system"),
    ("public-under-construction", "Under construction", "Public · system"),
    ("portal-login", "Portal login", "Portal · Phase 1"),
    ("portal-dashboard", "Dashboard", "Portal · Phase 1"),
    ("portal-leads", "Lead inbox", "Portal · Phase 1"),
    ("portal-lead-detail", "Lead detail", "Portal · Phase 1"),
    ("portal-analytics", "UTM analytics", "Portal · Phase 1"),
    ("portal-contacts", "Mailing list", "Portal · Phase 2"),
    ("portal-contacts-import", "Import contacts", "Portal · Phase 2"),
    ("portal-campaign", "Campaign composer", "Portal · Phase 2–3"),
    ("portal-campaign-report", "Email engagement", "Portal · Phase 2"),
    ("portal-postcard-designer", "Postcard designer", "Portal · Phase 3"),
    ("portal-social", "Social live preview", "Portal · Phase 4"),
    ("portal-social-accounts", "Social accounts", "Portal · Phase 4"),
]


def _rgb(hex6: str) -> RGBColor:
    return RGBColor.from_string(hex6)


def _logo_size(height_emu: int) -> tuple[int, int]:
    from PIL import Image

    with Image.open(LOGO) as im:
        aspect = im.width / im.height
    return int(height_emu * aspect), int(height_emu)


def add_brand_logo(slide, slide_width: int, slide_height: int, *, height: int | None = None) -> None:
    """Place AI/ML Operations logo bottom-right."""
    if not LOGO.exists():
        print(f"WARN missing logo: {LOGO}")
        return
    logo_h = height or Inches(0.32)
    logo_w, logo_h = _logo_size(logo_h)
    left = slide_width - Inches(0.4) - logo_w
    top = slide_height - Inches(0.48)
    slide.shapes.add_picture(str(LOGO), left, top, width=logo_w, height=logo_h)


def _set_run(paragraph, text: str, *, size_pt: float, bold: bool = False, color: str = "FFFFFF") -> None:
    paragraph.clear()
    run = paragraph.add_run()
    run.text = text
    run.font.size = Pt(size_pt)
    run.font.bold = bold
    run.font.color.rgb = _rgb(color)
    run.font.name = "Calibri"


def add_ux_gallery(prs: Presentation) -> int:
    """Append UX section divider + one slide per screenshot. Returns slide count added."""
    layout = prs.slide_layouts[0]  # Blank
    added = 0
    sw, sh = prs.slide_width, prs.slide_height

    # --- Section divider ---
    slide = prs.slides.add_slide(layout)
    bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, sw, sh)
    bg.fill.solid()
    bg.fill.fore_color.rgb = _rgb(MONICA["navy"])
    bg.line.fill.background()

    accent = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE, 0, Inches(2.6), sw, Inches(0.08)
    )
    accent.fill.solid()
    accent.fill.fore_color.rgb = _rgb(MONICA["primary_light"])
    accent.line.fill.background()

    title_box = slide.shapes.add_textbox(Inches(0.8), Inches(2.85), Inches(11.5), Inches(1.0))
    tf = title_box.text_frame
    tf.word_wrap = True
    _set_run(tf.paragraphs[0], "UX MOCKUPS", size_pt=44, bold=True, color="FFFFFF")

    sub = slide.shapes.add_textbox(Inches(0.8), Inches(3.9), Inches(11.5), Inches(1.2))
    stf = sub.text_frame
    stf.word_wrap = True
    _set_run(
        stf.paragraphs[0],
        "Click-through mockups of the public site and realtor portal — "
        "what the build-out delivers. Screenshots from proposal/pages (MKDRealtor.com).",
        size_pt=16,
        color="7DD3DA",
    )
    foot = slide.shapes.add_textbox(Inches(0.8), Inches(6.9), Inches(4), Inches(0.3))
    _set_run(foot.text_frame.paragraphs[0], DATE_STR, size_pt=11, color="6B7280")
    add_brand_logo(slide, sw, sh, height=Inches(0.34))
    added += 1

    # --- One slide per page ---
    missing = []
    for stem, title, eyebrow in UX_PAGES:
        path = SHOTS / f"{stem}.png"
        if not path.exists():
            missing.append(stem)
            continue

        slide = prs.slides.add_slide(layout)

        # Surface background
        bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, sw, sh)
        bg.fill.solid()
        bg.fill.fore_color.rgb = _rgb(MONICA["surface"])
        bg.line.fill.background()

        # Top bar
        bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, sw, Inches(0.72))
        bar.fill.solid()
        bar.fill.fore_color.rgb = _rgb(MONICA["sidebar"])
        bar.line.fill.background()

        eye = slide.shapes.add_textbox(Inches(0.4), Inches(0.08), Inches(8), Inches(0.28))
        _set_run(eye.text_frame.paragraphs[0], eyebrow.upper(), size_pt=11, bold=True, color="7DD3DA")

        ttl = slide.shapes.add_textbox(Inches(0.4), Inches(0.30), Inches(10), Inches(0.38))
        _set_run(ttl.text_frame.paragraphs[0], title, size_pt=20, bold=True, color="FFFFFF")

        tag = slide.shapes.add_textbox(Inches(10.2), Inches(0.22), Inches(2.8), Inches(0.35))
        p = tag.text_frame.paragraphs[0]
        p.alignment = PP_ALIGN.RIGHT
        _set_run(p, "UX mockup", size_pt=11, bold=True, color="7DD3DA")

        # Fit 1440×900 screenshot into remaining area
        top = Inches(0.85)
        bottom_margin = Inches(0.42)
        side = Inches(0.35)
        max_w = sw - side * 2
        max_h = sh - top - bottom_margin
        img_ratio = 1440 / 900
        box_ratio = max_w / max_h
        if box_ratio > img_ratio:
            pic_h = max_h
            pic_w = int(pic_h * img_ratio)
        else:
            pic_w = max_w
            pic_h = int(pic_w / img_ratio)
        left = int((sw - pic_w) / 2)
        slide.shapes.add_picture(str(path), left, top, width=pic_w, height=pic_h)

        # Footer: page ref left, logo bottom-right
        fbox = slide.shapes.add_textbox(Inches(0.4), sh - Inches(0.32), Inches(6), Inches(0.28))
        _set_run(
            fbox.text_frame.paragraphs[0],
            f"{PROJECT}  ·  {stem}.html  ·  {DATE_STR}",
            size_pt=10,
            color=MONICA["muted"],
        )
        add_brand_logo(slide, sw, sh, height=Inches(0.28))
        added += 1

    if missing:
        print(f"WARN missing screenshots: {', '.join(missing)}")
        print(f"  Run: .venv/bin/python proposal/powerpoint/capture_screenshots.py")
    return added


def set_runs_text(shape, text: str) -> bool:
    """Replace all paragraph/run text in a shape while keeping first run formatting."""
    if not shape.has_text_frame:
        return False
    tf = shape.text_frame
    # Flatten into first paragraph's first run when possible
    first_para = tf.paragraphs[0]
    if first_para.runs:
        first_para.runs[0].text = text
        for run in first_para.runs[1:]:
            run.text = ""
        for para in tf.paragraphs[1:]:
            for run in para.runs:
                run.text = ""
        return True
    # No runs — set paragraph text
    first_para.text = text
    return True


def replace_text_everywhere(shape, replacements: dict[str, str]) -> None:
    """Substring replace across all runs (preserves split-run titles when exact)."""
    if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
        for child in shape.shapes:
            replace_text_everywhere(child, replacements)
        return
    if not shape.has_text_frame:
        return
    for para in shape.text_frame.paragraphs:
        for run in para.runs:
            for old, new in replacements.items():
                if old in run.text:
                    run.text = run.text.replace(old, new)


def set_shape_text_by_name(slide, name: str, text: str) -> bool:
    for shape in slide.shapes:
        if shape.name == name:
            return set_runs_text(shape, text)
    return False


def iter_all_shapes(shapes):
    for shape in shapes:
        yield shape
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            yield from iter_all_shapes(shape.shapes)


def find_shapes_with_text(slide, contains: str):
    hits = []
    for shape in iter_all_shapes(slide.shapes):
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if contains in full:
            hits.append(shape)
    return hits


def set_full_text(shape, text: str) -> None:
    """Set shape text, preferring single-run update for titles split across runs."""
    if not shape.has_text_frame:
        return
    tf = shape.text_frame
    # If multiple runs in first para (e.g. "PRICES " + "TABLE"), put all in first run
    para = tf.paragraphs[0]
    if para.runs:
        para.runs[0].text = text
        for r in para.runs[1:]:
            r.text = ""
        for p in tf.paragraphs[1:]:
            for r in p.runs:
                r.text = ""
    else:
        para.text = text


def fill_table(table, rows: list[list[str]]) -> None:
    for r_i, row_data in enumerate(rows):
        if r_i >= len(table.rows):
            break
        for c_i, value in enumerate(row_data):
            if c_i >= len(table.columns):
                break
            cell = table.cell(r_i, c_i)
            # Keep formatting: set via text_frame
            tf = cell.text_frame
            if tf.paragraphs and tf.paragraphs[0].runs:
                tf.paragraphs[0].runs[0].text = value
                for r in tf.paragraphs[0].runs[1:]:
                    r.text = ""
                for p in tf.paragraphs[1:]:
                    for r in p.runs:
                        r.text = ""
            else:
                cell.text = value


def delete_slide(prs: Presentation, index: int) -> None:
    sld_id_lst = prs.slides._sldIdLst  # noqa: SLF001
    sld_id = sld_id_lst[index]
    rId = sld_id.get(qn("r:id"))
    prs.part.drop_rel(rId)
    sld_id_lst.remove(sld_id)


def recolor_pptx(path: Path) -> None:
    """Rewrite theme + hardcoded srgb colors inside the pptx zip."""
    tmp = path.with_suffix(".recolor.tmp.pptx")
    with ZipFile(path, "r") as zin, ZipFile(tmp, "w", compression=ZIP_DEFLATED) as zout:
        for info in zin.infolist():
            data = zin.read(info.filename)
            name = info.filename
            if name.endswith((".xml", ".rels")):
                text = data.decode("utf-8")
                # Theme scheme
                if "theme/theme1.xml" in name:
                    text = text.replace(
                        'clrScheme name="Website Proposal 01"',
                        'clrScheme name="MKDRealtor"',
                    )
                    for old, new in {
                        "63876D": MONICA["primary"],
                        "F1C988": MONICA["primary_light"],
                        "FF8D51": MONICA["primary_soft"],
                        "CB5A48": MONICA["navy"],
                        "9F4044": MONICA["ink"],
                        "632F2F": MONICA["sidebar"],
                        "181717": MONICA["ink"],
                        "954F72": MONICA["primary_light"],
                        "0563C1": MONICA["primary"],
                    }.items():
                        text = text.replace(f'srgbClr val="{old}"', f'srgbClr val="{new}"')
                        text = text.replace(f'srgbClr val="{old.lower()}"', f'srgbClr val="{new}"')
                if "theme/theme2.xml" in name:
                    for old, new in {
                        "4472C4": MONICA["primary"],
                        "ED7D31": MONICA["primary_light"],
                        "A5A5A5": MONICA["muted"],
                        "FFC000": MONICA["primary_soft"],
                        "5B9BD5": MONICA["primary_light"],
                        "70AD47": MONICA["ok"],
                        "44546A": MONICA["ink"],
                        "E7E6E6": MONICA["surface"],
                        "0563C1": MONICA["primary"],
                        "954F72": MONICA["primary_light"],
                    }.items():
                        text = text.replace(f'srgbClr val="{old}"', f'srgbClr val="{new}"')
                # Global warm → Monica on slides / masters / layouts / charts
                for old, new in COLOR_MAP.items():
                    text = re.sub(
                        rf'(?i)srgbClr val="{old}"',
                        f'srgbClr val="{new}"',
                        text,
                    )
                    # Some fills use scheme-independent hex in other attrs
                    text = re.sub(
                        rf'(?i)val="{old}"',
                        f'val="{new}"',
                        text,
                    )
                data = text.encode("utf-8")
            zout.writestr(info, data)
    tmp.replace(path)


def stamp_footer(slide, slide_width: int | None = None, slide_height: int | None = None) -> None:
    """Date bottom-left; clear bottom-right text so logo can sit there."""
    left_footer = None
    right_footer = None
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        if shape.top < Inches(6.4):
            continue
        if shape.left < Inches(5):
            left_footer = shape
        elif shape.left > Inches(8):
            right_footer = shape

    if left_footer is not None:
        set_full_text(left_footer, DATE_STR)
    if right_footer is not None:
        set_full_text(right_footer, "")

    # Also catch leftover company name anywhere in footer band
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        ).strip()
        if full in ("Company Name", COMPANY):
            set_full_text(shape, DATE_STR if shape.left < Inches(5) else "")
        elif "20 / 10 / 2020" in full:
            set_full_text(shape, DATE_STR if shape.left < Inches(5) else "")

    if slide_width is not None and slide_height is not None:
        add_brand_logo(slide, slide_width, slide_height)


def fill_title(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "WEBSITE" in full or "PROPOSAL" in full:
            # Title is often two runs/paragraphs: WEBSITE / PROPOSAL
            paras = shape.text_frame.paragraphs
            if len(paras) >= 2 and paras[0].runs and paras[1].runs:
                paras[0].runs[0].text = "COST"
                for r in paras[0].runs[1:]:
                    r.text = ""
                paras[1].runs[0].text = "PROPOSAL"
                for r in paras[1].runs[1:]:
                    r.text = ""
            else:
                set_full_text(shape, "COST PROPOSAL")
        elif "Lorem Ipsum" in full or "dummy text" in full:
            set_full_text(
                shape,
                "Custom marketing platform for Monica Dhillon — public site, "
                "realtor portal, multi-channel outreach, and social automation. "
                "Cheaper than PostcardMania, with social included.",
            )


def fill_welcome(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "Opportunities" in full:
            set_full_text(shape, "Replace PostcardMania. Own your pipeline.")
        elif "WELCOME" in full or "MESSAGE" in full:
            set_full_text(shape, "WELCOME")
        elif "Lorem ipsum" in full.lower() or "consectetuer" in full.lower():
            set_full_text(
                shape,
                "This proposal covers the one-time build-out of MKDRealtor.com and "
                "the ongoing monthly care plan. You get a branded public site, a "
                "private portal for leads and UTM analytics, email + SMS + postcard "
                "campaigns with consent/opt-out, and Facebook / Instagram / LinkedIn "
                "scheduling — deployed on our existing fleet with near-zero extra infra.",
            )


def fill_exec_summary(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "successful design projects" in full:
            set_full_text(
                shape,
                "Two cost sections: build-out (one-time) and monthly maintenance. "
                "Provider send costs (email, SMS, postcard) stay pass-through.",
            )

    items = [
        ("MKDRealtor.com platform", "Project name — realtor marketing + portal"),
        ("Single-tenant Django 6 app", "Deployed active/active behind NPM"),
        ("AI/ML Operations builds & hosts", "Same pipeline as company_site"),
        ("Django, Postgres, SMTP2GO, Lob, Meta, LinkedIn", "Stack & integrations"),
        ("LinkedIn app review + A2P 10DLC for SMS", "Client prerequisites"),
        ("Monica Dhillon / realtor brand", "Customer"),
        ("~10–14 weeks across 6 phases", "Timeline"),
        (f"{BUILD_TOTAL} build + from $149/mo care", "Investment summary"),
    ]
    # Groups are nested: outer Group 111 → Group 95..102
    numbered = []
    for shape in iter_all_shapes(slide.shapes):
        if shape.has_text_frame:
            t = "".join(
                "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
            ).strip()
            if re.fullmatch(r"0[1-8]", t):
                numbered.append(shape)

    # Find description pairs near each number by walking groups at top level
    content_groups = []
    for shape in slide.shapes:
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP and shape.name.startswith("Group"):
            # look one level deeper for the 8 item groups
            for child in shape.shapes:
                if child.shape_type == MSO_SHAPE_TYPE.GROUP:
                    content_groups.append(child)

    if len(content_groups) < 8:
        # fallback: any group that contains a 01-08 rectangle
        content_groups = []
        for shape in iter_all_shapes(slide.shapes):
            if shape.shape_type != MSO_SHAPE_TYPE.GROUP:
                continue
            texts = []
            for ch in shape.shapes:
                if ch.has_text_frame:
                    texts.append(
                        "".join(
                            "".join(r.text for r in p.runs)
                            for p in ch.text_frame.paragraphs
                        ).strip()
                    )
            if any(re.fullmatch(r"0[1-8]", t) for t in texts):
                content_groups.append(shape)

    for grp, (desc, label) in zip(content_groups[:8], items):
        textboxes = [s for s in grp.shapes if s.has_text_frame]
        # Typically: detail textbox, label textbox, number rectangle
        detail = None
        title = None
        for s in textboxes:
            t = "".join(
                "".join(r.text for r in p.runs) for p in s.text_frame.paragraphs
            ).strip()
            if re.fullmatch(r"0[1-8]", t):
                continue
            if t.startswith("-") or "Lorem" in t or len(t) > 20 or t.startswith("PHP") or "$" in t or "Project" in t or "Risk" in t or "Investor" in t or "Your" in t or "brief" in t:
                if detail is None:
                    detail = s
                elif title is None:
                    title = s
            else:
                if title is None:
                    title = s
                elif detail is None:
                    detail = s
        # Heuristic by name order
        named = {s.name: s for s in textboxes}
        # Prefer: first non-number textbox = detail (- ...), second = label
        non_num = []
        for s in textboxes:
            t = "".join(
                "".join(r.text for r in p.runs) for p in s.text_frame.paragraphs
            ).strip()
            if not re.fullmatch(r"0[1-8]", t):
                non_num.append(s)
        if len(non_num) >= 2:
            set_full_text(non_num[0], f"- {desc}")
            set_full_text(non_num[1], label)
        elif len(non_num) == 1:
            set_full_text(non_num[0], f"- {desc}")


def fill_what_we_do(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "successful design projects" in full:
            set_full_text(
                shape,
                "Everything in the UX proposal gallery — shipped as a real Django app.",
            )

    services = [
        ("Public site", "Home, about, contact with reCAPTCHA v3 lead capture and consent."),
        ("Lead portal", "Inbox, statuses, notes, and UTM attribution for every inquiry."),
        ("Mailing list", "Contacts with per-channel consent, import, and easy opt-out."),
        ("Campaigns", "Email + SMS via SMTP2GO; postcards via Lob with live 4×6 designer."),
        ("Social", "Schedule once to Facebook, Instagram, and LinkedIn — native APIs."),
    ]
    groups = [
        s
        for s in slide.shapes
        if s.shape_type == MSO_SHAPE_TYPE.GROUP and s.name.startswith("Group")
    ]
    for grp, (title, body) in zip(groups[:5], services):
        for s in grp.shapes:
            if not s.has_text_frame:
                continue
            t = "".join(
                "".join(r.text for r in p.runs) for p in s.text_frame.paragraphs
            ).strip()
            if t.startswith("Service"):
                set_full_text(s, title)
            elif "sample text" in t.lower() or "This is a" in t:
                set_full_text(s, body)


def fill_planning(slide) -> None:
    stamp_footer(slide)
    for shape in iter_all_shapes(slide.shapes):
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "successful design projects" in full:
            set_full_text(
                shape,
                "Six delivery phases from scaffold to polish. Social can start in parallel with outreach.",
            )
        elif full.strip() == "Define Target Audience":
            set_full_text(shape, "Scaffold & deploy")
        elif full.strip() == "Create a Sitemap":
            set_full_text(shape, "Public + leads + UTM")
        elif full.strip() == "SEO and Advertising":
            set_full_text(shape, "Mailing list + email/SMS")
        elif full.strip() == "Design and Developing":
            set_full_text(shape, "Postcards + social")
        elif full.strip() == "Set Goals and Objectives":
            set_full_text(shape, "Harden & handoff")
        elif "Lorem Ipsum" in full:
            # Match by parent context — replace with phase blurbs in order of groups
            pass

    blurbs = [
        "Docker, CI, Postgres, task worker on the existing fleet.",
        "Marketing pages, portal login, lead inbox, analytics.",
        "Consent, campaigns, SMTP2GO sends, unsubscribe / STOP.",
        "Lob postcards + Meta/LinkedIn scheduling.",
        "Monitoring, backups, runbook, realtor training.",
    ]
    blurb_shapes = []
    for shape in iter_all_shapes(slide.shapes):
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "Lorem Ipsum" in full and "dummy text of the printing" in full:
            blurb_shapes.append(shape)
    for shape, blurb in zip(blurb_shapes[:5], blurbs):
        set_full_text(shape, blurb)


def fill_build_expenses(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if full.strip() == "EXPENSES":
            set_full_text(shape, "BUILD-OUT")
        elif "successful design projects" in full:
            set_full_text(
                shape,
                "Section 1 — One-time investment to design, build, and launch MKDRealtor.com.",
            )
        elif full.strip() == "Expenses Title Here":
            set_full_text(shape, "Build-out line items")
        elif full.strip() == "Required Costs":
            set_full_text(shape, "Investment")

    for shape in slide.shapes:
        if shape.has_table:
            rows = [["Phase / deliverable", "Cost"]]
            for name, cost in BUILD_ITEMS:
                rows.append([name, cost])
            # Table is 9 rows: header + 7 items + total. We have 7 items — perfect.
            rows.append([f"Total build-out", BUILD_TOTAL])
            fill_table(shape.table, rows)
            break

    # Side callout groups often have "Short Text Here" / Lorem / Read More
    callouts = [
        (
            "Why this total",
            "Covers all six phases in the design doc — public site through social — plus training.",
        ),
        (
            "Not included",
            "Postcard postage, SMS/email usage, and A2P registration are billed at provider cost.",
        ),
    ]
    # Find text boxes with Short Text / Lorem / Read More
    short_titles = find_shapes_with_text(slide, "Short Text")
    lorems = [
        s
        for s in iter_all_shapes(slide.shapes)
        if s.has_text_frame
        and "Lorem Ipsum" in "".join(
            "".join(r.text for r in p.runs) for p in s.text_frame.paragraphs
        )
    ]
    reads = find_shapes_with_text(slide, "Read More")
    for i, (title, body) in enumerate(callouts):
        if i < len(short_titles):
            set_full_text(short_titles[i], title)
        if i < len(lorems):
            set_full_text(lorems[i], body)
        if i < len(reads):
            set_full_text(reads[i], "See phases →")


def fill_monthly_prices(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "PRICES" in full or "TABLE" in full:
            set_full_text(shape, "MONTHLY CARE")
        elif "successful design projects" in full:
            set_full_text(
                shape,
                "Section 2 — Ongoing maintenance so the platform stays secure and useful.",
            )
        elif "Project Budget" in full:
            set_full_text(shape, "Care plans")
        elif "All" in full and "Budget" in full:
            set_full_text(shape, "Pick a plan")

    for shape in slide.shapes:
        if shape.has_table:
            headers = MONTHLY_PLANS["headers"]
            # Table is 7x5; use 4 plan columns — leave 5th as duplicate of Recommended or blank label
            # Actually 5 columns: map Essential, Standard, Recommended, Full, and a "Pass-through" note column
            # Simpler: use 4 plans in first 4 cols; 5th col = "Notes" style - use Full Care only 4 cols
            # Template has 5 columns — use: Essential | Standard | Recommended | Full Care | Pass-through
            # Template table = 7×5 with plan names as column headers (no label column)
            rows_data = [
                ["Essential", "Standard", "Recommended", "Full Care", "Pass-through"],
                [
                    "Hosting included",
                    "Hosting included",
                    "Hosting included",
                    "Hosting included",
                    "Infra near-zero",
                ],
                [
                    "Security updates",
                    "Security updates",
                    "Security updates",
                    "Security updates",
                    "—",
                ],
                [
                    "Email support",
                    "Priority support",
                    "Priority support",
                    "Priority + Slack",
                    "—",
                ],
                [
                    "Platform only",
                    "2 hrs content/mo",
                    "4 hrs content/mo",
                    "8 hrs content/mo",
                    "—",
                ],
                [
                    "No campaign help",
                    "No campaign help",
                    "Campaign coaching",
                    "Campaign + social",
                    "Email/SMS/postcard",
                ],
                ["$149/mo", "$249/mo", "$349/mo", "$499/mo", "At cost"],
            ]
            # Table is 7 rows x 5 cols — rows_data matches
            fill_table(shape.table, rows_data)
            break


def fill_client_responsibilities(slide) -> None:
    stamp_footer(slide)
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "successful design projects" in full:
            set_full_text(
                shape,
                "A few client-side items unlock SMS, postcards, and LinkedIn posting.",
            )

    items = [
        ("Brand assets", "Logo, photos, bio copy, and preferred colors for the public site."),
        ("Domain & DNS", "Point the realtor domain to our NPM load balancer when ready."),
        ("SMTP2GO / SMS", "Approve SMTP2GO use; complete A2P 10DLC before SMS launch."),
        ("Postcard account", "Lob (or Click2Mail) API keys; artwork preferences for mailers."),
        ("Social accounts", "FB Page, IG Business, LinkedIn — OAuth connect in the portal."),
        ("LinkedIn review", "Allow 2–4 weeks for LinkedIn app posting-scope approval."),
    ]
    groups = [
        s
        for s in slide.shapes
        if s.shape_type == MSO_SHAPE_TYPE.GROUP and s.name.startswith("Group")
    ]
    # Prefer the numbered item groups (have Oval with 01 etc.)
    item_groups = []
    for g in groups:
        texts = []
        for ch in iter_all_shapes(g.shapes):
            if ch.has_text_frame:
                texts.append(
                    "".join(
                        "".join(r.text for r in p.runs)
                        for p in ch.text_frame.paragraphs
                    ).strip()
                )
        if any(re.fullmatch(r"0[1-6]", t) for t in texts):
            item_groups.append(g)

    for grp, (title, body) in zip(item_groups[:6], items):
        for s in iter_all_shapes(grp.shapes):
            if not s.has_text_frame:
                continue
            t = "".join(
                "".join(r.text for r in p.runs) for p in s.text_frame.paragraphs
            ).strip()
            if re.fullmatch(r"0[1-6]", t):
                continue
            if t == "Your Title Here" or "Title Here" in t:
                set_full_text(s, title)
            elif "sample text" in t.lower() or "This is a" in t:
                set_full_text(s, body)


def fill_contact(slide) -> None:
    stamp_footer(slide)
    replacements = {
        "35 W Wacker Dr,": "AI/ML Operations",
        "606011 Chicago, US": "Hosted on adama + roslin",
        "contact@pp.com": "hello@aimloperations.com",
        "contact@wp.com": "portal support via ticket",
        "support@wp.com": "deploy via server-infra CI",
        "+1 312-220-0088": "Schedule a kickoff call",
        "1800 - 2800": "Build-out: " + BUILD_TOTAL,
        "1800 - 3318": "Care: from $149/mo",
    }
    for shape in iter_all_shapes(slide.shapes):
        if not shape.has_text_frame:
            continue
        full = "".join(
            "".join(r.text for r in p.runs) for p in shape.text_frame.paragraphs
        )
        if "successful design projects" in full:
            set_full_text(
                shape,
                "Ready when you are — we start with Phase 0 scaffold and a staging URL.",
            )
            continue
        for old, new in replacements.items():
            if old in full:
                # replace within runs
                for para in shape.text_frame.paragraphs:
                    for run in para.runs:
                        if old in run.text:
                            run.text = run.text.replace(old, new)


def add_pass_through_note_slide(prs: Presentation, after_index: int) -> None:
    """Duplicate monthly slide is hard; instead annotate via exec — skip extra slide.

    Pass-through details live in build callouts + monthly *Usage column.
    """
    return


def main() -> None:
    if OUTPUT.exists():
        OUTPUT.unlink()
    shutil.copy2(TEMPLATE, OUTPUT)

    # Recolor before python-pptx edits (zip rewrite)
    recolor_pptx(OUTPUT)

    prs = Presentation(str(OUTPUT))

    # Keep only: title, welcome, exec, planning, prices, expenses, client, what-we-do, contact
    keep = {0, 1, 2, 4, 34, 35, 36, 38, 39}
    for idx in range(len(prs.slides) - 1, -1, -1):
        if idx not in keep:
            delete_slide(prs, idx)

    # current: [title, welcome, exec, planning, prices, expenses, client, whatwedo, contact]
    # target core: [title, welcome, exec, whatwedo, planning, expenses, prices, client, contact]
    sldIdLst = prs.slides._sldIdLst  # noqa: SLF001
    ids = list(sldIdLst)
    order = [0, 1, 2, 7, 3, 5, 4, 6, 8]
    new_ids = [ids[i] for i in order]
    for child in list(sldIdLst):
        sldIdLst.remove(child)
    for child in new_ids:
        sldIdLst.append(child)

    slides = list(prs.slides)
    fill_title(slides[0])
    fill_welcome(slides[1])
    fill_exec_summary(slides[2])
    fill_what_we_do(slides[3])
    fill_planning(slides[4])
    fill_build_expenses(slides[5])
    fill_monthly_prices(slides[6])
    fill_client_responsibilities(slides[7])
    fill_contact(slides[8])

    # UX gallery appended, then moved after "What we do"
    n_before = len(list(prs.slides))  # 9
    n_ux = add_ux_gallery(prs)
    sldIdLst = prs.slides._sldIdLst  # noqa: SLF001
    ids = list(sldIdLst)
    head = ids[:4]  # title … what we do
    mid = ids[4:n_before]  # planning … contact
    ux = ids[n_before : n_before + n_ux]
    for child in list(sldIdLst):
        sldIdLst.remove(child)
    for child in head + ux + mid:
        sldIdLst.append(child)

    # Final footer + logo on template slides (UX slides already branded)
    sw, sh = prs.slide_width, prs.slide_height
    for slide in list(prs.slides)[:4] + list(prs.slides)[4 + n_ux :]:
        stamp_footer(slide, sw, sh)
        for shape in iter_all_shapes(slide.shapes):
            if not shape.has_text_frame:
                continue
            for para in shape.text_frame.paragraphs:
                for run in para.runs:
                    if run.text.strip() in ("Company Name", COMPANY):
                        run.text = ""
                    if "20 / 10 / 2020" in run.text:
                        run.text = run.text.replace("20 / 10 / 2020", DATE_STR)

    prs.save(str(OUTPUT))
    print(f"Wrote {OUTPUT}")
    print(f"Slides: {len(list(prs.slides))} (incl. {n_ux} UX mockup slides)")
    print(f"Logo: {LOGO.name}")
    print(f"Build-out total: {BUILD_TOTAL}")
    print("Monthly: $149 / $249 / $349 / $499")


if __name__ == "__main__":
    main()
