// Demo submission template generator (t-paliad-241).
//
// One-shot authoring tool that emits a minimal but Word-compatible
// .docx file exercising every placeholder SubmissionVarsService
// resolves. Drop the output into m/mWorkRepo at
//
//	6 - material/Templates/Word/Paliad/HLC/de.inf.lg.erwidg.docx
//
// so paliad's submission-draft editor (t-paliad-238 Slice A) can fetch
// it via the per-submission_code fallback chain wired into
// handlers/files.go. The structure is a fake Klageerwiderung skeleton
// in German — fake legal prose, real placeholder tokens.
//
// Why a generator instead of authoring in Word: the per-placeholder
// docx grammar is `{{[A-Za-z][A-Za-z0-9_.]*}}` and Word's autocorrect
// happily fragments such tokens across <w:r> runs ({{ → "{", "{",
// project.case_number, "}", "}"). A programmatic emitter writes each
// placeholder as a single run so the renderer's pass-1 substitution
// (format-preserving) catches it cleanly. The merge engine handles
// cross-run cases too (pass 2) but pass 1 is the cheaper path.
//
// Run:
//
//	go run ./scripts/gen-demo-submission-template -out /tmp/de.inf.lg.erwidg.docx
//
// Output is deterministic so re-generating to the same path produces a
// byte-identical file (modulo zip mtime — we pin those to a fixed UTC
// timestamp so the bytes are reproducible).
package main

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

func main() {
	out := flag.String("out", "de.inf.lg.erwidg.docx", "output .docx path")
	flag.Parse()

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

// fixedTime is the zip mtime stamp baked into every entry so the output
// is byte-reproducible.
var fixedTime = time.Date(2026, 5, 23, 0, 0, 0, 0, time.UTC)

// buildDocx assembles the four-part .docx zip Word needs to open the
// file cleanly: Content_Types, root rels, document.xml, and document
// rels. Everything else (styles, theme, fonts) is optional — Word
// supplies sane defaults when absent.
func buildDocx() ([]byte, error) {
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)

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

	if err := add("[Content_Types].xml", contentTypesXML); err != nil {
		return nil, err
	}
	if err := add("_rels/.rels", rootRelsXML); err != nil {
		return nil, err
	}
	if err := add("word/_rels/document.xml.rels", documentRelsXML); err != nil {
		return nil, err
	}
	if err := add("word/styles.xml", stylesXML); err != nil {
		return nil, err
	}
	if err := add("word/document.xml", buildDocumentXML()); err != nil {
		return nil, err
	}

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

const contentTypesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>`

const rootRelsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`

const documentRelsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>`

// stylesXML provides minimal Heading1 + Heading2 paragraph styles so
// the section headings render with visual weight. Body text falls
// through to Word's Normal style.
const stylesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:style w:type="paragraph" w:styleId="Heading1">
    <w:name w:val="heading 1"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:before="360" w:after="120"/></w:pPr>
    <w:rPr><w:b/><w:sz w:val="28"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading2">
    <w:name w:val="heading 2"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:before="240" w:after="80"/></w:pPr>
    <w:rPr><w:b/><w:sz w:val="24"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
    <w:name w:val="Normal"/>
  </w:style>
</w:styles>`

// Document body — a fake Klageerwiderung skeleton with every placeholder
// SubmissionVarsService resolves embedded in natural positions. Each
// placeholder is in its own run so pass-1 substitution catches it without
// fragmentation worries. The DEMO marker in the header makes it obvious
// this is not approved firm content.
//
// Structure mirrors a real submission:
//
//	1. Firm letterhead + author block (firm.*, user.*, today.*)
//	2. Court caption (project.*, project.proceeding.*)
//	3. Parties block (parties.*)
//	4. Submission title + legal source (rule.*)
//	5. Deadline (deadline.*)
//	6. Boilerplate body + signature
//
// Order matches what a lawyer drafting a real Klageerwiderung would put
// at the top of the document, so when the lawyer customises this
// template later they don't have to restructure.
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">`)
	b.WriteString(`<w:body>`)

	demoBanner(&b)

	heading1(&b, "{{firm.name}} — Patentstreitsachen")
	plain(&b, "Bearbeiter: {{user.display_name}}")
	plain(&b, "E-Mail: {{user.email}} · Büro: {{user.office}}")
	plain(&b, "Datum: {{today.long_de}} ({{today.iso}})")

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

	heading2(&b, "In der Patentstreitsache")
	plain(&b, "{{parties.claimant.name}}")
	plain(&b, "vertreten durch {{parties.claimant.representative}}")
	bold(&b, "— Klägerin —")
	plain(&b, "")
	plain(&b, "gegen")
	plain(&b, "")
	plain(&b, "{{parties.defendant.name}}")
	plain(&b, "vertreten durch {{parties.defendant.representative}}")
	bold(&b, "— Beklagte —")
	plainOptional(&b, "Weitere Beteiligte: {{parties.other.name}}, vertreten durch {{parties.other.representative}}")

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

	heading1(&b, "{{rule.name}}")
	plain(&b, "(Schriftsatz-Code: {{rule.submission_code}})")
	plain(&b, "Rechtsgrundlage: {{rule.legal_source_pretty}} ({{rule.legal_source}})")
	plain(&b, "Typische Partei: {{rule.primary_party}} · Schriftsatz-Typ: {{rule.event_type}}")

	heading2(&b, "Frist")
	plain(&b, "Diese Frist wurde berechnet aus: {{deadline.computed_from}}")
	plain(&b, "Fälligkeit: {{deadline.due_date_long_de}} ({{deadline.due_date}})")
	plainOptional(&b, "Ursprüngliche Frist: {{deadline.original_due_date}}")
	plain(&b, "Frist-Bezeichnung: {{deadline.title}} · Quelle: {{deadline.source}}")

	heading2(&b, "I. Anträge")
	plain(&b, "Die Beklagte beantragt,")
	plain(&b, "")
	plain(&b, "1. die Klage abzuweisen;")
	plain(&b, "2. der Klägerin die Kosten des Rechtsstreits aufzuerlegen.")

	heading2(&b, "II. Sachverhalt")
	plain(&b, "[DEMO-Platzhalter] Hier folgt der Sachvortrag der Beklagten zum Streitpatent {{project.patent_number}} und zu den von der Klägerin geltend gemachten Ansprüchen.")

	heading2(&b, "III. Rechtsausführungen")
	plain(&b, "[DEMO-Platzhalter] Die Beklagte tritt der Klage aus den nachfolgenden Gründen entgegen.")

	heading2(&b, "Schlussformel")
	plain(&b, "{{today.long_de}}")
	plain(&b, "")
	plain(&b, "{{user.display_name}}")
	plain(&b, "{{firm.name}}")
	plainOptional(&b, "{{firm.signature_block}}")

	// English-locale exercise — lets the lawyer verify the EN long-form
	// date and EN proceeding name resolve correctly when the user's
	// preference is en. Also exercises the bare {{today}} alias
	// (identical to {{today.iso}}; included so every key the variable
	// bag carries appears at least once in this demo template).
	heading2(&b, "Locale-aware variants (DEMO)")
	plain(&b, "EN long date: {{today.long_en}} · Deadline EN: {{deadline.due_date_long_en}}")
	plain(&b, "Project our side (EN): {{project.our_side_en}} · Proceeding (EN): {{project.proceeding.name_en}}")
	plain(&b, "Rule name (EN): {{rule.name_en}} · Project our side (DE): {{project.our_side_de}}")
	plain(&b, "Proceeding (DE): {{project.proceeding.name_de}} · Rule name (DE): {{rule.name_de}}")
	plain(&b, "Today (bare alias): {{today}}")

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

// demoBanner writes a clearly-marked DEMO header so the file can't be
// mistaken for approved firm content (HLC branding compliance has not
// reviewed this — it's a developer-authored placeholder fixture).
func demoBanner(b *strings.Builder) {
	b.WriteString(`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:rPr><w:b/><w:color w:val="C00000"/></w:rPr><w:t xml:space="preserve">DEMO — interne Vorlage (nicht freigegeben)</w:t></w:r></w:p>`)
}

// heading1 emits a styled "Heading 1" paragraph with placeholder runs
// emitted intact (one run per placeholder so pass-1 substitution works).
func heading1(b *strings.Builder, text string) { paragraph(b, "Heading1", text, false) }

// heading2 emits a "Heading 2" paragraph.
func heading2(b *strings.Builder, text string) { paragraph(b, "Heading2", text, false) }

// plain emits a Normal-style paragraph.
func plain(b *strings.Builder, text string) { paragraph(b, "", text, false) }

// plainOptional is a Normal paragraph rendered as italic so the lawyer
// recognises rows that contain placeholders which may be empty
// (parties.other.*, deadline.original_due_date, firm.signature_block).
// Visual cue only; the merge engine still substitutes the same way.
func plainOptional(b *strings.Builder, text string) { paragraph(b, "", text, true) }

// bold emits a Normal paragraph with bold run formatting.
func bold(b *strings.Builder, text string) {
	b.WriteString(`<w:p>`)
	b.WriteString(`<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">`)
	b.WriteString(xmlEscape(text))
	b.WriteString(`</w:t></w:r></w:p>`)
}

// paragraph splits text on placeholder boundaries and emits one <w:r>
// per segment. Each placeholder occupies a dedicated run so the
// renderer's pass-1 substitution (format-preserving, single-run) hits
// the placeholder without the cross-run fallback. Italic runs are
// flagged via the italic argument.
func paragraph(b *strings.Builder, style, text string, italic bool) {
	b.WriteString(`<w:p>`)
	if style != "" {
		b.WriteString(`<w:pPr><w:pStyle w:val="`)
		b.WriteString(style)
		b.WriteString(`"/></w:pPr>`)
	}
	for _, seg := range splitOnPlaceholders(text) {
		b.WriteString(`<w:r>`)
		if italic {
			b.WriteString(`<w:rPr><w:i/></w:rPr>`)
		}
		b.WriteString(`<w:t xml:space="preserve">`)
		b.WriteString(xmlEscape(seg))
		b.WriteString(`</w:t></w:r>`)
	}
	b.WriteString(`</w:p>`)
}

// splitOnPlaceholders returns the input split into alternating text /
// placeholder segments while keeping each placeholder intact in its own
// segment. Empty input yields a single empty segment so the paragraph
// still emits a (visible) blank line.
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
		}
	}
}

// xmlEscape handles the five XML-significant characters for <w:t>
// content. Whitespace is preserved by the xml:space="preserve" attr we
// always emit on text runs.
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
}
