// HL-firm skeleton submission template generator (t-paliad-275).
//
// Reads HLC's "HL Patents Style" .dotm letterhead, strips its VBA
// macros and template-only artifacts, then emits a clean .docx that:
//
//  1. Preserves every HL paragraph + character style (HLpat-Heading-H1,
//     HLpat-Body-B0, HLpat-Signature, HLpat-Table-Recitals-*, …) by
//     keeping word/styles.xml, word/theme/*, word/numbering.xml,
//     word/fontTable.xml, settings.xml, footnotes/endnotes from the
//     source .dotm untouched.
//  2. Preserves the firm letterhead (logo header + firm-address footer)
//     by keeping word/header[12].xml + word/footer[12].xml and the
//     sectPr that references them.
//  3. Replaces word/document.xml with a Schriftsatz-shaped body that
//     exercises every SubmissionVarsService placeholder (firm.*,
//     today.*, user.*, project.*, parties.*, procedural_event.*, rule.*,
//     deadline.*) — applying HL paragraph/character styles to each
//     section so the rendered output reads as a real HL submission with
//     variables substituted.
//
// Drop the output into HL/mWorkRepo at
//
//	6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx
//
// so paliad's submission generator picks it up via the fallback chain.
// Lookup order after this CL: per-firm per-code → _firm-skeleton.docx
// (THIS file — HL formatting + placeholders) → universal _skeleton.docx
// (generic skeleton from t-paliad-259) → bare HL Patents Style .dotm
// (no placeholders). See internal/handlers/submission_drafts.go
// resolveSubmissionTemplate.
//
// Why this is firm-specific: the .dotm carries HL-licensed fonts,
// HL-branded logo media, and HLpat-prefixed style IDs. The output lives
// under the firm-namespaced directory in mWorkRepo so a future firm gets
// its own equivalent file generated against its own .dotm.
//
// Run:
//
//	go run ./scripts/gen-hl-skeleton-template \
//	  -in /tmp/hl-patents-style.dotm \
//	  -out /tmp/_firm-skeleton.docx
//
// Output is byte-stable across runs for a given input (zip mtimes
// pinned).
package main

import (
	"archive/zip"
	"bytes"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
	"time"
)

func main() {
	in := flag.String("in", "", "path to source HL Patents Style .dotm (required)")
	out := flag.String("out", "_firm-skeleton.docx", "output .docx path")
	flag.Parse()

	if *in == "" {
		fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: -in is required (path to HL Patents Style .dotm)")
		os.Exit(2)
	}

	srcBytes, err := os.ReadFile(*in)
	if err != nil {
		fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: read source:", err)
		os.Exit(1)
	}

	docx, err := buildDocx(srcBytes)
	if err != nil {
		fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template:", err)
		os.Exit(1)
	}
	if err := os.WriteFile(*out, docx, 0o644); err != nil {
		fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: write:", err)
		os.Exit(1)
	}
	fmt.Printf("wrote %s (%d bytes)\n", *out, len(docx))
}

// fixedTime pins every zip entry's mtime so successive runs over the
// same .dotm produce byte-stable output. Useful for diffing the
// generated file in PR review.
var fixedTime = time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC)

// dropPaths lists zip entries removed during the .dotm → .docx
// conversion. VBA macros + their keymap binding + the template-only
// glossary parts and ribbon customizations are all dead weight (and
// some actively trigger Word's macro-security warning) — none of them
// add anything to a placeholder-rich Schriftsatz starter.
var dropPaths = map[string]bool{
	"word/vbaProject.bin":              true,
	"word/vbaData.xml":                 true,
	"word/customizations.xml":          true,
	"userCustomization/customUI.xml":   true,
	"customUI/customUI14.xml":          true,
	"word/glossary/document.xml":       true,
	"word/glossary/_rels/document.xml.rels": true,
	"word/glossary/fontTable.xml":      true,
	"word/glossary/numbering.xml":      true,
	"word/glossary/settings.xml":       true,
	"word/glossary/styles.xml":         true,
	"word/glossary/webSettings.xml":    true,
}

// rIdsToDrop names the document-rel ids whose targets are stripped
// from the package (vbaProject, customizations.xml, glossary). They
// must vanish from word/_rels/document.xml.rels so Word doesn't choke
// on a dangling reference.
var rIdsToDrop = map[string]bool{
	"rId1":  true, // vbaProject.bin
	"rId2":  true, // customizations.xml (keymap to VBA)
	"rId21": true, // glossary/document.xml
}

func buildDocx(src []byte) ([]byte, error) {
	zr, err := zip.NewReader(bytes.NewReader(src), int64(len(src)))
	if err != nil {
		return nil, fmt.Errorf("open source zip: %w", err)
	}

	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)

	for _, f := range zr.File {
		name := f.Name
		if dropPaths[name] {
			continue
		}

		body, err := readZipEntry(f)
		if err != nil {
			return nil, fmt.Errorf("read %s: %w", name, err)
		}

		switch name {
		case "[Content_Types].xml":
			body = []byte(patchContentTypes(string(body)))
		case "_rels/.rels":
			body = []byte(patchRootRels(string(body)))
		case "word/_rels/document.xml.rels":
			body = []byte(patchDocumentRels(string(body)))
		case "word/document.xml":
			body = []byte(buildDocumentXML())
		}

		hdr := &zip.FileHeader{
			Name:     name,
			Method:   zip.Deflate,
			Modified: fixedTime,
		}
		w, err := zw.CreateHeader(hdr)
		if err != nil {
			return nil, fmt.Errorf("create %s: %w", name, err)
		}
		if _, err := w.Write(body); err != nil {
			return nil, fmt.Errorf("write %s: %w", name, err)
		}
	}

	if err := zw.Close(); err != nil {
		return nil, fmt.Errorf("finalise zip: %w", err)
	}
	return buf.Bytes(), nil
}

func readZipEntry(f *zip.File) ([]byte, error) {
	rc, err := f.Open()
	if err != nil {
		return nil, err
	}
	defer rc.Close()
	return io.ReadAll(rc)
}

// patchContentTypes rewrites the macroEnabledTemplate part type to the
// regular wordprocessingml.document type (a .dotm carries the macro
// part type even on the body part), and removes Default/Override
// entries that target now-deleted parts (vba binary, customizations,
// glossary).
func patchContentTypes(in string) string {
	out := in
	out = strings.ReplaceAll(out,
		`<Override PartName="/word/document.xml" ContentType="application/vnd.ms-word.template.macroEnabledTemplate.main+xml"/>`,
		`<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>`)

	removals := []string{
		`<Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/>`,
		`<Override PartName="/word/vbaData.xml" ContentType="application/vnd.ms-word.vbaData+xml"/>`,
		`<Override PartName="/word/customizations.xml" ContentType="application/vnd.ms-word.keyMapCustomizations+xml"/>`,
		`<Override PartName="/word/glossary/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"/>`,
		`<Override PartName="/word/glossary/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>`,
		`<Override PartName="/word/glossary/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>`,
		`<Override PartName="/word/glossary/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>`,
		`<Override PartName="/word/glossary/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>`,
		`<Override PartName="/word/glossary/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>`,
	}
	for _, r := range removals {
		out = strings.ReplaceAll(out, r, "")
	}
	return out
}

// patchRootRels drops the userCustomization (ribbon mini-tab) and the
// customUI14 extensibility relationships — both reference VBA-backed
// UI we don't ship.
func patchRootRels(in string) string {
	out := in
	out = stripRelByPrefix(out, `<Relationship Id="rId2" Type="http://schemas.microsoft.com/office/2006/relationships/ui/userCustomization"`)
	out = stripRelByPrefix(out, `<Relationship Id="Rf8f70ab1afd0469a" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility"`)
	return out
}

// patchDocumentRels drops the document-level rels whose targets we
// stripped (vbaProject, customizations.xml, glossaryDocument).
func patchDocumentRels(in string) string {
	out := in
	for rid := range rIdsToDrop {
		needle := `<Relationship Id="` + rid + `" `
		out = stripRelByPrefix(out, needle)
	}
	return out
}

// stripRelByPrefix removes the full <Relationship .../> element whose
// open tag starts with the given prefix. Tolerates either a regular
// closing tag (</Relationship>) or the more common self-closing form.
func stripRelByPrefix(s, prefix string) string {
	for {
		start := strings.Index(s, prefix)
		if start < 0 {
			return s
		}
		// Find end of this element (next "/>"). The .dotm always uses the
		// self-closing form for Relationship elements.
		end := strings.Index(s[start:], "/>")
		if end < 0 {
			return s
		}
		s = s[:start] + s[start+end+2:]
	}
}

// buildDocumentXML emits a Schriftsatz skeleton that exercises every
// SubmissionVarsService placeholder (the canonical 48-key v1 contract
// + the procedural_event.* canonical names + their rule.* legacy
// aliases). The structure mirrors a real DE/UPC submission — title
// block → court → rubrum → patent reference → submission title →
// legal grounds → Sachverhalt/Anträge/Rechtsausführungen/Beweis →
// signature → locale-variant verification footer.
//
// Each placeholder lives in its own <w:r> run so the renderer's pass-1
// (format-preserving single-run replace) catches every key. HL
// paragraph styles (HLpat-Heading-H1, HLpat-Header-Section, etc.) are
// applied via pStyle, character styles via rStyle.
//
// The sectPr at the bottom is copied verbatim from the source .dotm
// so the firm header/footer references (rId16=header1, rId17=footer1,
// rId18=header2 first-page, rId19=footer2 first-page) keep resolving
// after we replace the body. pgSz/pgMar/cols/docGrid match the .dotm
// exactly — a lawyer printing this gets the same A4 layout the .dotm
// produces.
func buildDocumentXML() string {
	var b strings.Builder
	b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`)
	b.WriteString(`<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">`)
	b.WriteString(`<w:body>`)

	skeletonBanner(&b)

	heading(&b, "HLpat-Heading-H1", "{{firm.name}}")
	body0(&b, "Bearbeiter: {{user.display_name}}")
	body0(&b, "E-Mail: {{user.email}} · Büro: {{user.office}}")
	body0(&b, "Datum: {{today.long_de}} ({{today.iso}})")
	body0(&b, "{{firm.signature_block}}")

	headerSection(&b, "{{project.court}}")
	body0(&b, "Aktenzeichen: {{project.case_number}}")
	body0(&b, "Verfahrensart: {{project.proceeding.name}} ({{project.proceeding.code}})")
	body0(&b, "Instanz: {{project.instance_level}}")

	headerSubsection(&b, "In der Sache")

	recitalsParty(&b, "{{parties.claimant.name}}")
	recitalsPartyDetails(&b, "vertreten durch {{parties.claimant.representative}}")
	recitalsRoles(&b, "— Klägerin / Patentinhaberin / Anmelderin —")

	recitalsSequencer(&b, "gegen")

	recitalsParty(&b, "{{parties.defendant.name}}")
	recitalsPartyDetails(&b, "vertreten durch {{parties.defendant.representative}}")
	recitalsRoles(&b, "— Beklagte / Einsprechende / Beschwerdegegnerin —")

	recitalsSequencer(&b, "sowie")

	recitalsParty(&b, "{{parties.other.name}}")
	recitalsPartyDetails(&b, "vertreten durch {{parties.other.representative}}")
	recitalsRoles(&b, "— Weitere Beteiligte —")

	headerSubsection(&b, "Betreff")
	body0(&b, "Streitpatent: {{project.patent_number}} (UPC-Schreibweise: {{project.patent_number_upc}})")
	body0(&b, "Anmeldung: {{project.filing_date}} · Erteilung: {{project.grant_date}}")
	body0(&b, "Projekttitel: {{project.title}}")
	body0(&b, "Unsere Seite: {{project.our_side_de}} ({{project.our_side}})")
	body0(&b, "Mandant: {{project.client_number}} · Matter: {{project.matter_number}}")
	body0(&b, "Internes Aktenzeichen: {{project.reference}}")

	heading(&b, "HLpat-Heading-H1", "{{procedural_event.name}}")
	body0(&b, "(Schriftsatz-Code: {{procedural_event.code}})")
	body0(&b, "Rechtsgrundlage: {{procedural_event.legal_source_pretty}} ({{procedural_event.legal_source}})")
	body0(&b, "Typische Partei: {{procedural_event.primary_party}} · Schriftsatz-Typ: {{procedural_event.event_kind}}")

	// t-paliad-287 — the dedicated Frist block was removed in 2026-05.
	// {{deadline.*}} placeholders stay resolvable in the variable bag
	// for custom templates that want them, but the default HL skeleton
	// no longer renders them in the submission body: the deadline is
	// internal/admin context and has no place in a court-bound document.

	heading(&b, "HLpat-Heading-H2", "I. Sachverhalt")
	body0(&b, "[Hier folgt der Sachverhalt. Diese Vorlage ist eine Skelett-Fassung — bitte gemäß Schriftsatz-Typ ({{procedural_event.name}}) ausformulieren.]")

	heading(&b, "HLpat-Heading-H2", "II. Anträge")
	requestsIntro(&b, "Es wird beantragt:")
	requestsLevel1(&b, "[Antrag 1 — gemäß {{procedural_event.legal_source_pretty}}]")
	requestsLevel1(&b, "[Antrag 2]")

	heading(&b, "HLpat-Heading-H2", "III. Rechtsausführungen")
	body0(&b, "[Hier folgen die Rechtsausführungen.]")

	heading(&b, "HLpat-Heading-H2", "IV. Beweis")
	evidenceOffering(&b, "Beweis: [Beweismittel — z. B. Anlage K1: {{project.patent_number}}]")

	heading(&b, "HLpat-Heading-H2", "Schlussformel")
	signature(&b, "{{today.long_de}}")
	signature(&b, "")
	signature(&b, "{{user.display_name}}")
	signature(&b, "{{firm.name}}")

	// Locale-aware verification block — exercises every EN/DE alias the
	// variable bag carries plus the rule.* legacy aliases so a lawyer
	// editing the template sees that both surfaces resolve. A real
	// submission deletes this section after sanity-checking the render.
	heading(&b, "HLpat-Heading-H3", "Locale-Varianten & Legacy-Aliase (SKELETON)")
	body1(&b, "EN long date: {{today.long_en}} · Today (bare alias): {{today}}")
	body1(&b, "Project our side (EN): {{project.our_side_en}} · Proceeding (EN): {{project.proceeding.name_en}}")
	body1(&b, "Proceeding (DE): {{project.proceeding.name_de}}")
	body1(&b, "Procedural event name (DE): {{procedural_event.name_de}} · (EN): {{procedural_event.name_en}}")
	body1(&b, "Rule legacy aliases — name: {{rule.name}}, name_de: {{rule.name_de}}, name_en: {{rule.name_en}}")
	body1(&b, "Rule legacy aliases — code: {{rule.submission_code}}, legal_source: {{rule.legal_source}}, legal_source_pretty: {{rule.legal_source_pretty}}")
	body1(&b, "Rule legacy aliases — primary_party: {{rule.primary_party}}, event_type: {{rule.event_type}}")

	// sectPr — copied verbatim from the source .dotm. Keeps the firm
	// letterhead header (rId16=header1.xml, rId18=header2.xml first-page)
	// and the firm-address footer (rId17, rId19) on every printed page.
	b.WriteString(sectPrXML)

	b.WriteString(`</w:body></w:document>`)
	return b.String()
}

// sectPrXML matches the source .dotm's section properties exactly so
// the firm header/footer refs and A4 page geometry round-trip.
const sectPrXML = `<w:sectPr><w:headerReference w:type="default" r:id="rId16"/><w:footerReference w:type="default" r:id="rId17"/><w:headerReference w:type="first" r:id="rId18"/><w:footerReference w:type="first" r:id="rId19"/><w:pgSz w:w="11906" w:h="16838" w:code="9"/><w:pgMar w:top="567" w:right="1418" w:bottom="567" w:left="1418" w:header="284" w:footer="284" w:gutter="0"/><w:cols w:space="720"/><w:titlePg/><w:docGrid w:linePitch="286"/></w:sectPr>`

func skeletonBanner(b *strings.Builder) {
	b.WriteString(`<w:p><w:pPr><w:pStyle w:val="HLpat-Heading-H1"/></w:pPr><w:r><w:rPr><w:b/><w:color w:val="C00000"/></w:rPr><w:t xml:space="preserve">SKELETON — HL Patents Style mit Platzhaltern (nicht freigegeben)</w:t></w:r></w:p>`)
}

func heading(b *strings.Builder, style, text string)         { styledPara(b, style, "", text) }
func headerSection(b *strings.Builder, text string)          { styledPara(b, "HLpat-Header-Section", "", text) }
func headerSubsection(b *strings.Builder, text string)       { styledPara(b, "HLpat-Header-Subsection", "", text) }
func body0(b *strings.Builder, text string)                  { styledPara(b, "HLpat-Body-B0", "", text) }
func body1(b *strings.Builder, text string)                  { styledPara(b, "HLpat-Body-B1", "", text) }
func recitalsParty(b *strings.Builder, text string)          { styledPara(b, "HLpat-Table-Recitals-Party", "", text) }
func recitalsPartyDetails(b *strings.Builder, text string)   { styledPara(b, "HLpat-Table-Recitals-PartyDetails", "", text) }
func recitalsRoles(b *strings.Builder, text string)          { styledPara(b, "HLpat-Table-Recitals-PartyRoles", "", text) }
func recitalsSequencer(b *strings.Builder, text string)      { styledPara(b, "HLpat-Table-Recitals-Sequencers", "", text) }
func requestsIntro(b *strings.Builder, text string)          { styledPara(b, "HLpat-Requests-Intro", "", text) }
func requestsLevel1(b *strings.Builder, text string)         { styledPara(b, "HLpat-Requests-Level1", "", text) }
func evidenceOffering(b *strings.Builder, text string)       { styledPara(b, "HLpat-EvidenceOffering", "", text) }
func signature(b *strings.Builder, text string)              { styledPara(b, "HLpat-Signature", "", text) }

// styledPara writes one paragraph with the given pStyle (paragraph
// style id) and optional rStyle (character style applied to every run).
// Empty style ids drop the corresponding wrapper. Placeholders inside
// `text` are split into their own runs so the renderer's pass-1
// single-run replace catches each one independently.
func styledPara(b *strings.Builder, pStyle, rStyle, text string) {
	b.WriteString(`<w:p>`)
	if pStyle != "" {
		b.WriteString(`<w:pPr><w:pStyle w:val="`)
		b.WriteString(pStyle)
		b.WriteString(`"/></w:pPr>`)
	}
	for _, seg := range splitOnPlaceholders(text) {
		b.WriteString(`<w:r>`)
		if rStyle != "" {
			b.WriteString(`<w:rPr><w:rStyle w:val="`)
			b.WriteString(rStyle)
			b.WriteString(`"/></w:rPr>`)
		}
		b.WriteString(`<w:t xml:space="preserve">`)
		b.WriteString(xmlEscape(seg))
		b.WriteString(`</w:t></w:r>`)
	}
	b.WriteString(`</w:p>`)
}

func splitOnPlaceholders(s string) []string {
	if s == "" {
		return []string{""}
	}
	var out []string
	for {
		open := strings.Index(s, "{{")
		if open < 0 {
			out = append(out, s)
			return out
		}
		close := strings.Index(s[open:], "}}")
		if close < 0 {
			out = append(out, s)
			return out
		}
		end := open + close + 2
		if open > 0 {
			out = append(out, s[:open])
		}
		out = append(out, s[open:end])
		s = s[end:]
		if s == "" {
			return out
		}
	}
}

func xmlEscape(s string) string {
	s = strings.ReplaceAll(s, "&", "&amp;")
	s = strings.ReplaceAll(s, "<", "&lt;")
	s = strings.ReplaceAll(s, ">", "&gt;")
	s = strings.ReplaceAll(s, `"`, "&quot;")
	s = strings.ReplaceAll(s, "'", "&apos;")
	return s
}
