#!/usr/bin/env python3
"""
Build the Messaging Audit report as a premium, white-label .docx from one audit.json.

Usage:
    python build_report.py <audit.json> [output_dir] [--pdf]

The report carries the practitioner's own name, optional logo, and optional
accent color (from the author profile), so the buyer's client sees the buyer's
brand, not anyone else's. With no profile it renders a clean, neutral report.

Design:
  - A title page (logo, eyebrow, title, one-line descriptor, a verdict tally,
    prepared-by), then the audit from page 2.
  - Running header and footer with page numbers on the content pages.
  - Neutral base (ink + greys) plus ONE accent color for section labels, the
    title accent, accent rules, priority numbers, the matrix header, and the
    Your Strategic Take outline. Accent defaults to a professional slate and is
    set per user via audit.json "accent".
  - Rating colors are fixed status colors (green / amber / red / grey), because
    they signal meaning, not brand.

See references/audit-schema.md. Needs python-docx (pip install python-docx).
Add --pdf to also convert via LibreOffice.
"""
import json, sys, os, subprocess, shutil, re
from collections import Counter
try:
    from docx import Document
    from docx.shared import Pt, RGBColor, Inches
    from docx.enum.table import WD_TABLE_ALIGNMENT
    from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement
except ImportError:
    print("This script needs python-docx. Install it with:  pip install python-docx\n"
          "Then run the command again.")
    sys.exit(1)

# ---- Neutral base tokens ----
INK="1B1C1B"; MUTED="5F635F"; LINE="E4E7E5"; PANEL="EEF1F4"; WHITE="FFFFFF"; FAINT="9AA0A6"
# Light, airy palette (like a clean UN-style progress report): one clear cyan
# for structure, titles, cover bar, and table header, plus a lighter cyan for
# rules, zebra rows, and tints. Both swappable per practitioner via audit.json
# "accent" and "accent2".
DEFAULT_ACCENT="B0176A"      # Firebrand pink primary
DEFAULT_ACCENT2="2B9AA8"     # Firebrand teal secondary
ENGINE_URL="https://www.fbdigital.co/firebrand-intelligence-engine-founding-access"  # founding early-access page
# Executive, Office-native fonts so the .docx renders natively in Word (where
# .docx files open). SERIF (Cambria) drives headings + the title; SANS (Calibri)
# drives body; MONO (Calibri) drives the small letter-spaced labels.
SERIF="Cambria"; SANS="Calibri"; MONO="Calibri"
CONTENT_W=6.8  # inches of text column (letter, 0.85 margins)

RATING={
 "aligned":      {"fill":"E6F4EC", "text":"1E7A46", "label":"Aligned"},
 "drifting":     {"fill":"FBEED6", "text":"B87514", "label":"Drifting"},
 "contradictory":{"fill":"FBE9E7", "text":"C0392B", "label":"Contradictory"},
 "absent":       {"fill":"EDEEF0", "text":"6B7280", "label":"Absent"},
}
def rk(s): return (s or "").strip().lower()

ACCENT=DEFAULT_ACCENT; ACCENT_SOFT="EEF1F4"
ACCENT2=DEFAULT_ACCENT2; ACCENT2_SOFT="F0EBF7"
def norm_hex(h, default=DEFAULT_ACCENT):
    h=(h or "").strip().lstrip("#")
    return h if re.fullmatch(r"[0-9a-fA-F]{6}", h or "") else default
def lighten(hexstr, f=0.90):
    r,g,b=int(hexstr[0:2],16),int(hexstr[2:4],16),int(hexstr[4:6],16)
    return f"{int(r+(255-r)*f):02X}{int(g+(255-g)*f):02X}{int(b+(255-b)*f):02X}"

# ---- run / paragraph helpers ----
def set_run(r, name=SANS, size=10.5, color=INK, bold=False, italic=False, caps=False, spacing=None):
    r.font.name=name; r.font.size=Pt(size); r.font.bold=bold; r.font.italic=italic
    r.font.color.rgb=RGBColor.from_string(color)
    rpr=r._element.get_or_add_rPr()
    rf=rpr.find(qn('w:rFonts'))
    if rf is None: rf=OxmlElement('w:rFonts'); rpr.append(rf)
    for a in ('w:ascii','w:hAnsi','w:cs'): rf.set(qn(a), name)
    if caps:
        c=OxmlElement('w:caps'); c.set(qn('w:val'),'true'); rpr.append(c)
    if spacing is not None:
        s=OxmlElement('w:spacing'); s.set(qn('w:val'),str(spacing)); rpr.append(s)  # twentieths of a point

def para(doc, space_after=8, space_before=0, align=None):
    p=doc.add_paragraph(); p.paragraph_format.space_after=Pt(space_after)
    p.paragraph_format.space_before=Pt(space_before)
    if align is not None: p.alignment=align
    return p

def shade(cell, hexfill):
    tcPr=cell._tc.get_or_add_tcPr(); sh=OxmlElement('w:shd')
    sh.set(qn('w:val'),'clear'); sh.set(qn('w:color'),'auto'); sh.set(qn('w:fill'),hexfill); tcPr.append(sh)

def no_borders(table):
    tblPr=table._tbl.tblPr; borders=OxmlElement('w:tblBorders')
    for edge in ('top','left','bottom','right','insideH','insideV'):
        e=OxmlElement(f'w:{edge}'); e.set(qn('w:val'),'none'); borders.append(e)
    tblPr.append(borders)

def cell_border(cell, color, sz=12):
    tcPr=cell._tc.get_or_add_tcPr(); b=OxmlElement('w:tcBorders')
    for edge in ('top','left','bottom','right'):
        e=OxmlElement(f'w:{edge}'); e.set(qn('w:val'),'single'); e.set(qn('w:sz'),str(sz))
        e.set(qn('w:space'),'0'); e.set(qn('w:color'),color); b.append(e)
    tcPr.append(b)

def no_row_split(table):
    for r in table.rows:
        trPr=r._tr.get_or_add_trPr()
        cs=OxmlElement('w:cantSplit'); cs.set(qn('w:val'),'true'); trPr.append(cs)

def repeat_header_row(table):
    # mark the first row as a table header so it repeats at the top of every page the table spans
    trPr=table.rows[0]._tr.get_or_add_trPr()
    th=OxmlElement('w:tblHeader'); th.set(qn('w:val'),'true'); trPr.append(th)

def cell_margins(table, top=60, bottom=60, left=120, right=120):
    tblPr=table._tbl.tblPr; m=OxmlElement('w:tblCellMar')
    for edge,val in (('top',top),('bottom',bottom),('left',left),('right',right)):
        e=OxmlElement(f'w:{edge}'); e.set(qn('w:w'),str(val)); e.set(qn('w:type'),'dxa'); m.append(e)
    tblPr.append(m)

def bottom_border(par, color, sz=6, space=1):
    pPr=par._p.get_or_add_pPr(); b=OxmlElement('w:pBdr'); bot=OxmlElement('w:bottom')
    bot.set(qn('w:val'),'single'); bot.set(qn('w:sz'),str(sz)); bot.set(qn('w:space'),str(space)); bot.set(qn('w:color'),color)
    b.append(bot); pPr.append(b)

def top_border(par, color, sz=6, space=1):
    pPr=par._p.get_or_add_pPr(); b=OxmlElement('w:pBdr'); t=OxmlElement('w:top')
    t.set(qn('w:val'),'single'); t.set(qn('w:sz'),str(sz)); t.set(qn('w:space'),str(space)); t.set(qn('w:color'),color)
    b.append(t); pPr.append(b)

def rule(doc, color=LINE):
    p=para(doc, space_after=6, space_before=6); bottom_border(p, color, sz=6); return p

def accent_rule(doc, width_in=1.5, space_after=12):
    p=para(doc, space_after=space_after, space_before=2)
    p.paragraph_format.right_indent=Inches(max(0.0, CONTENT_W-width_in))
    bottom_border(p, ACCENT, sz=16, space=1); return p

def eyebrow(doc, text, color=None):
    p=para(doc, space_after=2, space_before=12)
    set_run(p.add_run(text.upper()), name=MONO, size=8.5, color=color or ACCENT, bold=True, caps=True, spacing=24)
    return p

def section_head(doc, num, title, pb=False):
    eb=eyebrow(doc, f"Section {num}"); eb.paragraph_format.keep_with_next=True
    if pb: eb.paragraph_format.page_break_before=True
    p=para(doc, space_after=4, space_before=2); p.paragraph_format.keep_with_next=True
    set_run(p.add_run(title), name=SERIF, size=18, color=INK, bold=True)
    ar=accent_rule(doc, width_in=1.4, space_after=12); ar.paragraph_format.keep_with_next=True

def h2_plain(doc, title):
    p=para(doc, space_after=4, space_before=2)
    set_run(p.add_run(title), name=SERIF, size=18, color=INK, bold=True)
    accent_rule(doc, width_in=1.4, space_after=12)

def badge_run(p, rating):
    info=RATING.get(rk(rating), RATING["absent"])
    set_run(p.add_run("  "+info["label"].upper()), name=MONO, size=8.5, color=info["text"], bold=True, caps=True, spacing=12)

def add_logo(doc, data, width_in):
    path=data.get("logo_path")
    if path and os.path.exists(path):
        try:
            p=doc.add_paragraph(); p.paragraph_format.space_after=Pt(4)
            p.add_run().add_picture(path, width=Inches(width_in)); return True
        except Exception: return False
    return False

def field(par, code):
    r=par.add_run();
    f1=OxmlElement('w:fldChar'); f1.set(qn('w:fldCharType'),'begin')
    it=OxmlElement('w:instrText'); it.set(qn('xml:space'),'preserve'); it.text=f' {code} '
    f2=OxmlElement('w:fldChar'); f2.set(qn('w:fldCharType'),'end')
    r._r.append(f1); r._r.append(it); r._r.append(f2); return r

def setup_running(section, brand, prepared):
    section.different_first_page_header_footer=True
    # header (pages 2+): intentionally blank (no rule)
    section.header.paragraphs[0].text=""
    # footer (pages 2+): prepared-by left, page number right
    fp=section.footer.paragraphs[0]; fp.text=""
    fp.paragraph_format.tab_stops.add_tab_stop(Inches(CONTENT_W), WD_TAB_ALIGNMENT.RIGHT)
    top_border(fp, LINE, sz=4, space=6)
    if prepared: set_run(fp.add_run(prepared), name=MONO, size=8, color=FAINT)
    set_run(fp.add_run("\t"), name=MONO, size=8, color=FAINT)
    set_run(fp.add_run("Page "), name=MONO, size=8, color=FAINT)
    field(fp, "PAGE");
    # style the PAGE field run
    for r in fp.runs:
        if r.font.size is None: set_run(r, name=MONO, size=8, color=FAINT)

def row_exact_height(table, inches):
    tr=table.rows[0]; trPr=tr._tr.get_or_add_trPr()
    h=OxmlElement('w:trHeight'); h.set(qn('w:val'), str(int(inches*1440))); h.set(qn('w:hRule'),'exact'); trPr.append(h)

def build_cover(doc, data, brand, prepared):
    # Compact masthead at the top of page 1 (no full-page wash), then the
    # executive summary flows directly below it.
    add_logo(doc, data, width_in=1.5)
    ep=para(doc, space_after=6, space_before=6)
    set_run(ep.add_run("Messaging audit"), name=MONO, size=9, color=ACCENT, bold=True, caps=True, spacing=30)
    tp=para(doc, space_after=8)
    set_run(tp.add_run(brand), name=SERIF, size=30, color=INK, bold=True)
    trr=para(doc, space_after=10, space_before=2); bottom_border(trr, ACCENT, sz=14)
    meta=[f"Prepared by {prepared}" if prepared else None, data.get("date",""),
          f'{data.get("asset_count","")} assets audited']
    set_run(para(doc, space_after=14).add_run("   ·   ".join([m for m in meta if m])), name=MONO, size=9, color=MUTED)
    # at-a-glance score box (contained stat cards, sized to content)
    counts=Counter(rk(i.get("rating")) for i in data.get("scorecard",[]))
    order=[k for k in ("contradictory","absent","drifting","aligned") if counts.get(k)]
    if order:
        set_run(para(doc, space_after=4).add_run("At a glance"), name=MONO, size=8.5, color=MUTED, caps=True, spacing=24)
        gt=doc.add_table(rows=1, cols=len(order)); cell_margins(gt, top=80, bottom=80, left=120, right=120)
        for i,k in enumerate(order):
            info=RATING[k]; c=gt.rows[0].cells[i]; shade(c, "FAFAFA"); cell_border(c, LINE, sz=6)
            np=c.paragraphs[0]; np.alignment=WD_ALIGN_PARAGRAPH.CENTER; np.paragraph_format.space_after=Pt(0)
            set_run(np.add_run(str(counts[k])), name=SERIF, size=20, color=info["text"], bold=True)
            lp=c.add_paragraph(); lp.alignment=WD_ALIGN_PARAGRAPH.CENTER; lp.paragraph_format.space_before=Pt(0)
            set_run(lp.add_run(info["label"].upper()), name=MONO, size=8, color=MUTED, caps=True, spacing=14)
        para(doc, space_after=6)

def add_hyperlink(paragraph, url, text, color_hex, size=10.5, bold=True, name=SANS):
    part=paragraph.part
    r_id=part.relate_to(url, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", is_external=True)
    hl=OxmlElement('w:hyperlink'); hl.set(qn('r:id'), r_id)
    r=OxmlElement('w:r'); rPr=OxmlElement('w:rPr')
    rf=OxmlElement('w:rFonts')
    for a in ('w:ascii','w:hAnsi','w:cs'): rf.set(qn(a), name)
    rPr.append(rf)
    if bold: rPr.append(OxmlElement('w:b'))
    c=OxmlElement('w:color'); c.set(qn('w:val'), color_hex); rPr.append(c)
    sz=OxmlElement('w:sz'); sz.set(qn('w:val'), str(int(size*2))); rPr.append(sz)
    u=OxmlElement('w:u'); u.set(qn('w:val'),'single'); rPr.append(u)
    r.append(rPr)
    t=OxmlElement('w:t'); t.set(qn('xml:space'),'preserve'); t.text=text; r.append(t)
    hl.append(r); paragraph._p.append(hl)
    return hl

def build_engine_cta(doc):
    # Firebrand lead-magnet CTA, single centered founding-list card.
    base=os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets")
    ico_cohort=os.path.join(base,"cta-icon-cohort.png")
    ico_wait=os.path.join(base,"cta-icon-waitlist.png")
    logo=os.path.join(base,"logo-black-horiz.png")
    def cell_pad(cell, top=150,bottom=150,left=170,right=170):
        tcPr=cell._tc.get_or_add_tcPr(); m=OxmlElement("w:tcMar")
        for edge,val in (("top",top),("bottom",bottom),("left",left),("right",right)):
            e=OxmlElement("w:"+edge); e.set(qn("w:w"),str(val)); e.set(qn("w:type"),"dxa"); m.append(e)
        tcPr.append(m)
    def tfix(t):
        t.autofit=False; t.allow_autofit=False
        lay=OxmlElement("w:tblLayout"); lay.set(qn("w:type"),"fixed"); t._tbl.tblPr.append(lay)
    tr=para(doc, space_after=2, space_before=6); bottom_border(tr, ACCENT, sz=18)
    tr.paragraph_format.keep_with_next=True; tr.paragraph_format.page_break_before=True
    eb=eyebrow(doc,"What's next"); eb.paragraph_format.keep_with_next=True
    t1=para(doc, space_after=0, space_before=2); t1.paragraph_format.keep_with_next=True
    set_run(t1.add_run("Close these gaps with the"), name=SERIF, size=19, color=INK, bold=True)
    t2=para(doc, space_after=4); t2.paragraph_format.keep_with_next=True
    set_run(t2.add_run("Firebrand Intelligence Engine"), name=SERIF, size=19, color=ACCENT, bold=True)
    ar=accent_rule(doc, width_in=1.2, space_after=10); ar.paragraph_format.keep_with_next=True
    bp=para(doc, space_after=14); bp.paragraph_format.keep_with_next=True
    set_run(bp.add_run("This audit found the gaps. Fixing them is the next job, and it is exactly what the Firebrand Intelligence Engine is built for: a suite of strategic marketing skills that turn a gap list like this one into corrected positioning, messaging, and voice, written in your brand voice, not generic AI output."), size=10.5, color=INK)
    eng=os.path.join(base,"cta-engine.png")
    if os.path.exists(eng):
        ep=para(doc, space_after=6); ep.alignment=WD_ALIGN_PARAGRAPH.CENTER
        ep.add_run().add_picture(eng, width=Inches(6.4))
    # small left-aligned logo sign-off (footer)
    if os.path.exists(logo):
        lg=para(doc, space_after=6, space_before=12); lg.alignment=WD_ALIGN_PARAGRAPH.LEFT
        lg.add_run().add_picture(logo, width=Inches(1.4))
# ---- report ----
def build(doc, data):
    global ACCENT, ACCENT_SOFT, ACCENT2, ACCENT2_SOFT
    # Free edition: practitioner branding is FIXED to Firebrand, regardless of audit.json.
    ACCENT=DEFAULT_ACCENT; ACCENT_SOFT=lighten(ACCENT, 0.90)
    ACCENT2=DEFAULT_ACCENT2; ACCENT2_SOFT=lighten(ACCENT2, 0.90)
    brand=data.get("brand","[Brand]"); prepared="Kara Parlin, Firebrand Digital Marketing"
    _logo=os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "logo-black-horiz.png")
    if os.path.exists(_logo): data["logo_path"]=_logo
    for s in doc.sections:
        s.top_margin=Inches(0.75); s.bottom_margin=Inches(0.8)
        s.left_margin=Inches(0.85); s.right_margin=Inches(0.85)
    setup_running(doc.sections[0], brand, prepared)

    # ---------- MASTHEAD + Executive summary on page 1 ----------
    build_cover(doc, data, brand, prepared)

    # ---------- 1. Executive summary ----------
    section_head(doc, 1, "Executive summary")
    set_run(para(doc, space_after=8).add_run(data.get("exec_summary","")), size=10.5)
    if data.get("overall_read"):
        set_run(para(doc, space_after=6).add_run(data["overall_read"]), name=SERIF, size=12, color=INK)
    sc=data.get("scorecard",[])
    if sc:
        t=doc.add_table(rows=0, cols=2); t.alignment=WD_TABLE_ALIGNMENT.LEFT; no_borders(t); cell_margins(t, top=42, bottom=42)
        for item in sc:
            row=t.add_row().cells
            r0=row[0].paragraphs[0]; r0.paragraph_format.space_after=Pt(0); r0.paragraph_format.space_before=Pt(0)
            set_run(r0.add_run(item.get("element","")), size=9.5, bold=True)
            info=RATING.get(rk(item.get("rating")), RATING["absent"])
            r1=row[1].paragraphs[0]; r1.paragraph_format.space_after=Pt(0); r1.paragraph_format.space_before=Pt(0)
            set_run(r1.add_run(info["label"]), name=MONO, size=8.5, color=info["text"], bold=True, spacing=8)
            row[0].width=Inches(3.3); row[1].width=Inches(2.2)
        no_row_split(t)
        para(doc, space_after=2)

    # ---------- 2. Scope ----------
    section_head(doc, 2, "Scope")
    sp1=para(doc, space_after=4); sp1.paragraph_format.keep_with_next=True
    set_run(sp1.add_run("Assets audited"), size=10.5, bold=True)
    sp2=para(doc, space_after=10); sp2.paragraph_format.keep_with_next=True
    set_run(sp2.add_run(data.get("assets_audited","")), size=10.5)
    sp3=para(doc, space_after=4); sp3.paragraph_format.keep_with_next=True
    set_run(sp3.add_run("Not covered or not accessible"), size=10.5, bold=True)
    set_run(para(doc, space_after=10).add_run(data.get("out_of_scope","")), size=10.5)

    # ---------- 3. Extraction matrix ----------
    section_head(doc, 3, "Extraction matrix")
    set_run(para(doc, space_after=10).add_run(
        "Every finding below traces back to this table. Cells hold the brand's actual language; a dash means the element is absent from that asset."), size=10.5)
    m=data.get("matrix",{}); cols=m.get("assets",[]); rows=m.get("rows",[])
    if cols and rows:
        t=doc.add_table(rows=1, cols=1+len(cols)); t.alignment=WD_TABLE_ALIGNMENT.LEFT
        no_borders(t); cell_margins(t, top=90, bottom=90, left=130, right=130)
        hdr=t.rows[0].cells
        set_run(hdr[0].paragraphs[0].add_run("Element"), name=MONO, size=8.5, color=INK, bold=True, caps=True, spacing=12)
        bottom_border(hdr[0].paragraphs[0], ACCENT, sz=10)
        for i,c in enumerate(cols):
            set_run(hdr[i+1].paragraphs[0].add_run(c), name=MONO, size=8.5, color=INK, bold=True, caps=True, spacing=12)
            bottom_border(hdr[i+1].paragraphs[0], ACCENT, sz=10)
        for ri,rd in enumerate(rows):
            cells=t.add_row().cells
            set_run(cells[0].paragraphs[0].add_run(rd.get("element","")), size=9, bold=True)
            vals=rd.get("values",[])
            for i in range(len(cols)):
                v=vals[i] if i<len(vals) else ""
                if not v or v.strip() in ("-","--","(absent)",""):
                    set_run(cells[i+1].paragraphs[0].add_run("(absent)"), size=9, color=MUTED, italic=True)
                else:
                    set_run(cells[i+1].paragraphs[0].add_run(v), size=9)
            for cc in cells:
                bottom_border(cc.paragraphs[0], LINE, sz=4)
        no_row_split(t); repeat_header_row(t)
        para(doc, space_after=6)

    # ---------- 4. Consistency findings ----------
    section_head(doc, 4, "Consistency findings")
    for f in data.get("findings",[]):
        info=RATING.get(rk(f.get("rating")), RATING["absent"])
        p=para(doc, space_after=2, space_before=8); p.paragraph_format.keep_with_next=True
        set_run(p.add_run(f.get("element","")), name=SERIF, size=12.5, color=INK, bold=True)
        badge_run(p, f.get("rating"))
        ev=f.get("evidence",[])
        if ev:
            bt=doc.add_table(rows=1, cols=1); cell_margins(bt, top=90, bottom=90, left=150, right=150); no_borders(bt)
            cell=bt.rows[0].cells[0]; shade(cell, "F6F7F8")
            for j,q in enumerate(ev):
                ep=cell.paragraphs[0] if j==0 else cell.add_paragraph(); ep.paragraph_format.space_after=Pt(2)
                set_run(ep.add_run(q), size=9.5, color=INK)
            no_row_split(bt)
        if f.get("so_what"):
            set_run(para(doc, space_after=10, space_before=3).add_run(f["so_what"]), size=9.5, color=MUTED)

    # ---------- 5. Gaps ----------
    section_head(doc, 5, "Gaps")
    gaps=data.get("gaps",[])
    for idx,g in enumerate(gaps):
        p=para(doc, space_after=7, space_before=2)
        if idx < len(gaps)-1: p.paragraph_format.keep_with_next=True
        set_run(p.add_run(g.get("type","").upper()+"   "), name=MONO, size=8.5, color=ACCENT, bold=True, caps=True, spacing=12)
        set_run(p.add_run(g.get("text","")), size=10)

    # ---------- 6. Priority fixes ----------
    section_head(doc, 6, "What to fix first")
    for i,fx in enumerate(data.get("fixes",[]), start=1):
        hp=para(doc, space_after=3, space_before=12); hp.paragraph_format.keep_with_next=True
        set_run(hp.add_run(f"{i:02d}  "), name=SERIF, size=16, color=ACCENT, bold=True)
        set_run(hp.add_run(fx.get("title","")), name=SERIF, size=13, color=INK, bold=True)
        wp=para(doc, space_after=3); wp.paragraph_format.keep_with_next=True
        set_run(wp.add_run(fx.get("why","")), size=10)
        meta=f'Rating: {fx.get("rating","")}    Effort: {fx.get("effort","")}    Next: {fx.get("next","")}'
        set_run(para(doc, space_after=6).add_run(meta.upper()), name=MONO, size=8, color=MUTED, caps=True, spacing=10)

    # ---------- 7. Your Strategic Take ----------
    eb=eyebrow(doc,"Your strategic take"); eb.paragraph_format.keep_with_next=True
    p=para(doc, space_after=4, space_before=2); p.paragraph_format.keep_with_next=True
    set_run(p.add_run("What only you can decide"), name=SERIF, size=18, color=INK, bold=True)
    ar=accent_rule(doc, width_in=1.4, space_after=12); ar.paragraph_format.keep_with_next=True
    bt=doc.add_table(rows=1, cols=1); cell_margins(bt, top=170,bottom=170,left=210,right=210)
    cell=bt.rows[0].cells[0]; cell_border(cell, ACCENT, sz=12)
    ip=cell.paragraphs[0]; ip.paragraph_format.space_after=Pt(8)
    set_run(ip.add_run("This is where the audit stops and you decide. Everything above comes from what the copy actually says, which the audit can read. The calls below can't be made from the copy alone. They need your read of the business, not the AI's."),
            size=10, color=MUTED, italic=True)
    for note in data.get("strategic_take",[]):
        np=cell.add_paragraph(); np.paragraph_format.space_after=Pt(6)
        set_run(np.add_run(note.get("q","")+"  "), size=10, color=ACCENT, bold=True)
        set_run(np.add_run(note.get("note","")), size=10, color=INK)
    para(doc, space_after=8)

    # ---------- 8. Next step ----------
    section_head(doc, 8, "Where this goes next")
    nt=doc.add_table(rows=1, cols=1); no_borders(nt); cell_margins(nt, top=140,bottom=140,left=180,right=180)
    ncell=nt.rows[0].cells[0]; shade(ncell, "F6F7F8")
    set_run(ncell.paragraphs[0].add_run(data.get("next_step","")), size=10.5, color=INK)
    para(doc, space_after=10)

    # ---------- Engine CTA (free-edition closer) ----------
    build_engine_cta(doc)

    # footer disclaimer (end of content)
    dp=para(doc, space_after=2, space_before=6); top_border(dp, LINE, sz=4, space=6)
    set_run(dp.add_run(
        "This audit inventories and diagnoses existing messaging. It does not create new messaging, and nothing in it has been published or changed on the brand's live assets."),
        size=8.5, color=MUTED, italic=True)

def slug(s): return re.sub(r'[^a-z0-9]+','-', (s or "brand").lower()).strip('-') or "brand"

def to_pdf(docx_path, out_dir):
    soffice=shutil.which("soffice") or shutil.which("libreoffice")
    if not soffice:
        print("[note] LibreOffice not found, PDF skipped. The .docx is ready; open it and Save As PDF."); return None
    try:
        subprocess.run([soffice,"--headless","--convert-to","pdf","--outdir",out_dir,docx_path],
                       check=True, capture_output=True, timeout=120)
        pdf=os.path.splitext(docx_path)[0]+".pdf"; return pdf if os.path.exists(pdf) else None
    except Exception as e:
        print(f"[note] PDF conversion failed ({e}). The .docx is ready; open it and Save As PDF."); return None

def main():
    args=[a for a in sys.argv[1:] if a!="--pdf"]; want_pdf="--pdf" in sys.argv[1:]
    if not args:
        print("Usage: python build_report.py <audit.json> [output_dir] [--pdf]"); sys.exit(1)
    data=json.loads(open(args[0]).read())
    out_dir=args[1] if len(args)>1 else os.path.dirname(os.path.abspath(args[0]))
    os.makedirs(out_dir, exist_ok=True)
    docx_path=os.path.join(out_dir, slug(data.get("brand"))+"-messaging-audit.docx")
    doc=Document(); build(doc, data); doc.save(docx_path)
    print(f"[ok] docx: {docx_path}")
    if want_pdf:
        pdf=to_pdf(docx_path, out_dir)
        if pdf: print(f"[ok] pdf:  {pdf}")

if __name__=="__main__":
    main()
