#!/usr/bin/env python3
"""Render resume.yaml -> a .odt file.

Two output flavors:
  --level=short  (default) -> condensed single-page resume.
  --level=full             -> detailed resume; concatenates each list with its
                              `_full` sibling (e.g. bullets + bullets_full).

Builds the ODT zip from scratch (mimetype, manifest, meta, styles, content).
The ODT styling lives inline as STYLES below; content comes from resume.yaml.

DOCX is produced by piping this output through LibreOffice:
    soffice --headless --convert-to docx <file>.odt
"""
import argparse
import os
import yaml
import zipfile

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "resume.yaml")
DEFAULT_OUT = {
    "short": os.path.join(HERE, "MikeEberlein_Resume.odt"),
    "full":  os.path.join(HERE, "MikeEberlein_Resume_Detailed.odt"),
}

MIMETYPE = "application/vnd.oasis.opendocument.text"

# Roboto faces to embed in the zip, sourced from fonts/static/.
# Each tuple: (filename in Fonts/, weight attr, style attr).
EMBEDDED_FONTS = [
    ("Roboto-Regular.ttf",     "normal",  "normal"),
    ("Roboto-Italic.ttf",      "normal",  "italic"),
    ("Roboto-Bold.ttf",        "bold",    "normal"),
    ("Roboto-BoldItalic.ttf",  "bold",    "italic"),
    ("Roboto-Light.ttf",       "300",     "normal"),
    ("Roboto-LightItalic.ttf", "300",     "italic"),
    ("Roboto-Medium.ttf",      "500",     "normal"),
]
FONTS_SRC_DIR = os.path.join(HERE, "fonts", "static")

def _font_manifest_entries():
    rows = []
    for name, _w, _s in EMBEDDED_FONTS:
        rows.append(f' <manifest:file-entry manifest:full-path="Fonts/{name}" manifest:media-type="application/x-font-ttf"/>')
    return "\n".join(rows)

MANIFEST = f'''<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.3">
 <manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="application/vnd.oasis.opendocument.text"/>
 <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
 <manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
 <manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
{_font_manifest_entries()}
</manifest:manifest>
'''

META = '''<?xml version="1.0" encoding="UTF-8"?>
<office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" office:version="1.3">
<office:meta><dc:title>Mike Eberlein Resume</dc:title><meta:generator>render_odt.py</meta:generator></office:meta>
</office:document-meta>
'''

def _font_face_uris(filter_family):
    """Emit <svg:font-face-uri> children for one logical family ('roboto' | 'light')."""
    rows = []
    for name, weight, style in EMBEDDED_FONTS:
        # The 'Roboto Light' family only carries the Light faces; the main
        # 'Roboto' family carries everything else (including Medium).
        is_light = name.startswith("Roboto-Light")
        if filter_family == "light" and not is_light:
            continue
        if filter_family == "roboto" and is_light:
            continue
        rows.append(
            f'   <svg:font-face-uri xlink:href="Fonts/{name}" xlink:type="simple" '
            f'loext:font-style="{style}" loext:font-weight="{weight}">'
            f'<svg:font-face-format svg:string="application/x-font-ttf"/>'
            f'</svg:font-face-uri>'
        )
    return "\n".join(rows)

FONT_FACE_DECLS = f'''<office:font-face-decls>
 <style:font-face style:name="Roboto" svg:font-family="Roboto" style:font-family-generic="swiss" style:font-pitch="variable">
  <svg:font-face-src>
{_font_face_uris("roboto")}
  </svg:font-face-src>
 </style:font-face>
 <style:font-face style:name="Roboto Light" svg:font-family="&apos;Roboto Light&apos;" style:font-family-generic="swiss" style:font-pitch="variable">
  <svg:font-face-src>
{_font_face_uris("light")}
  </svg:font-face-src>
 </style:font-face>
</office:font-face-decls>'''

STYLES = f'''<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
 xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
 xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
 xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
 xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
 xmlns:xlink="http://www.w3.org/1999/xlink"
 xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
 xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
 office:version="1.3">
{FONT_FACE_DECLS}
<office:styles>
 <style:default-style style:family="paragraph">
  <style:paragraph-properties fo:hyphenate="false"/>
  <style:text-properties style:font-name="Roboto" fo:font-size="9pt" fo:color="#222222"/>
 </style:default-style>
 <style:style style:name="Standard" style:family="paragraph" style:class="text">
  <style:paragraph-properties fo:margin-top="0in" fo:margin-bottom="0in" fo:line-height="105%"/>
  <style:text-properties style:font-name="Roboto" fo:font-size="9pt"/>
 </style:style>
 <style:style style:name="Name" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:text-align="center" fo:margin-bottom="0.04in"/>
  <style:text-properties style:font-name="Roboto Light" fo:font-size="20pt" fo:color="#1F3864" fo:letter-spacing="0.04in"/>
 </style:style>
 <style:style style:name="Contact" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:text-align="center" fo:margin-bottom="0.06in"/>
  <style:text-properties fo:font-size="9pt" fo:color="#555555"/>
 </style:style>
 <style:style style:name="Summary" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:text-align="justify" fo:margin-bottom="0.04in"/>
  <style:text-properties fo:font-size="9pt"/>
 </style:style>
 <style:style style:name="SectionHead" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:margin-top="0.03in" fo:margin-bottom="0.01in" fo:border-bottom="0.5pt solid #1F3864" fo:padding-bottom="0.005in"/>
  <style:text-properties style:font-name="Roboto" fo:font-size="11pt" fo:color="#1F3864" fo:font-weight="bold" fo:letter-spacing="0.02in"/>
 </style:style>
 <style:style style:name="JobHead" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:margin-top="0.03in"/>
  <style:text-properties fo:font-weight="bold" fo:font-size="10pt"/>
 </style:style>
 <style:style style:name="RoleLine" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:margin-top="0.015in"/>
  <style:text-properties fo:font-style="italic" fo:font-size="9pt" fo:color="#555555"/>
 </style:style>
 <style:style style:name="SubRole" style:family="paragraph" style:parent-style-name="Standard">
  <style:paragraph-properties fo:margin-top="0.012in" fo:margin-left="0.22in"/>
  <style:text-properties fo:font-size="9pt" fo:color="#222222"/>
 </style:style>
 <style:style style:name="Bullet" style:family="paragraph" style:parent-style-name="Standard" style:list-style-name="L1">
  <style:paragraph-properties fo:margin-left="0.18in" fo:text-indent="-0.13in"/>
 </style:style>
 <style:style style:name="SubBullet" style:family="paragraph" style:parent-style-name="Standard" style:list-style-name="L2">
  <style:paragraph-properties fo:margin-left="0.4in" fo:text-indent="-0.13in"/>
  <style:text-properties fo:font-size="9pt" fo:color="#555555"/>
 </style:style>
 <style:style style:name="Bold" style:family="text"><style:text-properties fo:font-weight="bold"/></style:style>
 <style:style style:name="Italic" style:family="text"><style:text-properties fo:font-style="italic" fo:color="#555555"/></style:style>

 <text:list-style style:name="L1">
  <text:list-level-style-bullet text:level="1" text:bullet-char="•">
   <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
    <style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.18in" fo:text-indent="-0.13in" fo:margin-left="0.18in"/>
   </style:list-level-properties>
  </text:list-level-style-bullet>
 </text:list-style>
 <text:list-style style:name="L2">
  <text:list-level-style-bullet text:level="1" text:bullet-char="•">
   <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
    <style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.4in" fo:text-indent="-0.13in" fo:margin-left="0.4in"/>
   </style:list-level-properties>
  </text:list-level-style-bullet>
 </text:list-style>
</office:styles>
<office:automatic-styles>
 <style:page-layout style:name="PL1">
  <style:page-layout-properties fo:page-width="8.5in" fo:page-height="11in"
    fo:margin-top="0.25in" fo:margin-bottom="0.2in" fo:margin-left="0.5in" fo:margin-right="0.5in"
    style:print-orientation="portrait"/>
 </style:page-layout>
</office:automatic-styles>
<office:master-styles>
 <style:master-page style:name="Standard" style:page-layout-name="PL1"/>
</office:master-styles>
</office:document-styles>
'''

CONTENT_HEAD = '''<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
 xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
 xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
 xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
 xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
 xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
 office:version="1.3">
<office:automatic-styles>
 <style:style style:name="JobTab" style:family="paragraph" style:parent-style-name="JobHead">
  <style:paragraph-properties>
   <style:tab-stops><style:tab-stop style:position="7.5in" style:type="right"/></style:tab-stops>
  </style:paragraph-properties>
 </style:style>
 <style:style style:name="RoleTab" style:family="paragraph" style:parent-style-name="RoleLine">
  <style:paragraph-properties>
   <style:tab-stops><style:tab-stop style:position="7.5in" style:type="right"/></style:tab-stops>
  </style:paragraph-properties>
 </style:style>
 <style:style style:name="SubRoleTab" style:family="paragraph" style:parent-style-name="SubRole">
  <style:paragraph-properties>
   <style:tab-stops><style:tab-stop style:position="7.5in" style:type="right"/></style:tab-stops>
  </style:paragraph-properties>
 </style:style>
</office:automatic-styles>
<office:body><office:text>'''

CONTENT_TAIL = '</office:text></office:body></office:document-content>'

# ---------- XML helpers ----------
XML_ESCAPES = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}

def xesc(s: str) -> str:
    return "".join(XML_ESCAPES.get(c, c) for c in s)

def p(style, content):
    return f'<text:p text:style-name="{style}">{content}</text:p>'

def b(text):
    return f'<text:span text:style-name="Bold">{xesc(text)}</text:span>'

def i(text):
    return f'<text:span text:style-name="Italic">{xesc(text)}</text:span>'

TAB = '<text:tab/>'

def bullet(text, style="Bullet"):
    list_style = "L2" if style == "SubBullet" else "L1"
    return (f'<text:list text:style-name="{list_style}"><text:list-item>'
            f'<text:p text:style-name="{style}">{xesc(text)}</text:p>'
            f'</text:list-item></text:list>')

def merged(item, key, level):
    """Return item[key] for short, or item[key] + item[key + '_full'] for full."""
    base = item.get(key) or []
    if level == "full":
        base = list(base) + list(item.get(key + "_full") or [])
    return base

def render(data, level="short") -> str:
    parts = []
    h = data["header"]
    parts.append(p("Name", xesc(h["name"])))
    contact = f'{xesc(h["tagline"])} &#8226; {xesc(h["email"])}'
    parts.append(p("Contact", contact))
    parts.append(p("Summary", xesc(data["summary"].strip())))

    parts.append(p("SectionHead", "EXPERIENCE"))
    for company in merged(data, "experience", level):
        parts.append(f'<text:p text:style-name="JobTab">{b(company["company"])}{TAB}{xesc(company["dates"])}</text:p>')
        for role in merged(company, "roles", level):
            parts.append(f'<text:p text:style-name="RoleTab">{i(role["title"])}{TAB}{i(role["dates"])}</text:p>')
            subroles = merged(role, "subroles", level)
            if subroles:
                for sr in subroles:
                    parts.append(f'<text:p text:style-name="SubRoleTab">{xesc(sr["title"])}{TAB}{i(sr["dates"])}</text:p>')
                    for blt in merged(sr, "bullets", level):
                        parts.append(bullet(blt, style="SubBullet"))
            else:
                for blt in merged(role, "bullets", level):
                    parts.append(bullet(blt, style="Bullet"))

    parts.append(p("SectionHead", "EDUCATION"))
    for ed in merged(data, "education", level):
        parts.append(f'<text:p text:style-name="JobTab">{b(ed["school"])}{TAB}{xesc(ed["dates"])}</text:p>')
        for blt in merged(ed, "bullets", level):
            parts.append(bullet(blt, style="Bullet"))

    parts.append(p("SectionHead", "CERTIFICATIONS"))
    parts.append(p("Standard", " &#8226; ".join(xesc(c) for c in merged(data, "certifications", level))))

    return CONTENT_HEAD + "".join(parts) + CONTENT_TAIL

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--level", choices=["short", "full"], default="short")
    ap.add_argument("--output", default=None,
                    help="Output .odt path (default: MikeEberlein_Resume.odt / MikeEberlein_Resume_Detailed.odt)")
    args = ap.parse_args()

    out_path = args.output or DEFAULT_OUT[args.level]
    with open(DATA, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    content = render(data, level=args.level)

    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z:
        zi = zipfile.ZipInfo("mimetype")
        zi.compress_type = zipfile.ZIP_STORED
        z.writestr(zi, MIMETYPE)
        z.writestr("META-INF/manifest.xml", MANIFEST)
        z.writestr("meta.xml", META)
        z.writestr("styles.xml", STYLES)
        z.writestr("content.xml", content)
        # Embed the Roboto faces inside the document for portability.
        for name, _w, _s in EMBEDDED_FONTS:
            src = os.path.join(FONTS_SRC_DIR, name)
            if not os.path.exists(src):
                raise SystemExit(f"font missing: {src}")
            with open(src, "rb") as f:
                z.writestr(f"Fonts/{name}", f.read())
    print(f"wrote {out_path}  (level={args.level})")

if __name__ == "__main__":
    main()
