// Universal-skeleton submission template generator (t-paliad-259).
//
// One-shot authoring tool that emits a minimal but Word-compatible
// .docx file exercising every placeholder SubmissionVarsService
// resolves — without baking in any submission_code-specific prose.
//
// Drop the output into m/mWorkRepo at
//
//	6 - material/Templates/Word/Paliad/HLC/_skeleton.docx
//
// so paliad's submission generator picks it up via the fallback chain
// slotted between the per-submission_code template and the bare
// universal HL Patents Style .dotm. Any submission_code that has no
// per-firm template still gets a draft populated with variables
// instead of the macro-only letterhead.
//
// Why a separate file from de.inf.lg.erwidg.docx: that one is a
// Klageerwiderung skeleton (DE LG, "I. Anträge / II. Sachverhalt /
// III. Rechtsausführungen"). For a UPC SoC, an EPO opposition, a DPMA
// appeal, that body structure is wrong. The universal skeleton drops
// the structure and leaves a single neutral body block the lawyer
// replaces — every variable still resolves regardless of code.
//
// Run:
//
//	go run ./scripts/gen-skeleton-submission-template -out /tmp/_skeleton.docx
//
// Output is byte-reproducible (zip mtimes pinned to a fixed UTC
// timestamp).
package main

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

// anchorsOnly switches the body emitter from the legacy variable-bag
// banner template to the Composer Slice B anchor-only body. Toggled
// via the -anchors flag; default true so the Slice B regen produces
// the composer-ready file.
var anchorsOnly = true

func main() {
	out := flag.String("out", "_skeleton.docx", "output .docx path")
	anchors := flag.Bool("anchors", true, "emit Composer-mode body with section anchors only (t-paliad-313 Slice B); false = legacy variable-bag banner body")
	flag.Parse()
	anchorsOnly = *anchors

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

var fixedTime = time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC)

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>`

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 code-agnostic Schriftsatz skeleton: firm letterhead +
// case caption + parties + submission heading + a single neutral body
// block. Mirrors the variable bag from SubmissionVarsService (firm.* /
// today.* / user.* / project.* / parties.* / rule.*) without baking in
// DE-LG-Klageerwiderung-specific structure. A lawyer customising this
// template for a UPC SoC, EPO opposition, or DPMA appeal replaces the
// [Schriftsatztext] block and renames the party labels — every
// placeholder still resolves regardless of the submission_code chosen.
//
// The {{deadline.*}} placeholders are deliberately NOT rendered by the
// default skeleton (t-paliad-287). The deadline is internal context for
// the lawyer, not text that belongs in a court-bound submission. The
// keys stay resolvable in the bag so a custom template can still
// reference them where it actually wants them.
//
// Every placeholder occupies its own <w:r> run so the renderer's pass-1
// (format-preserving, single-run) substitution catches it. The
// DEMO/SKELETON banner makes it obvious this is a starter template and
// not approved firm content.
func buildDocumentXML() string {
	if anchorsOnly {
		return buildAnchoredDocumentXML()
	}
	return buildLegacyDocumentXML()
}

// buildAnchoredDocumentXML emits the Composer-mode body: just section
// anchors. The composer pipeline (services/submission_compose.go)
// replaces each {{#section:KEY}}...{{/section:KEY}} paragraph pair
// with the rendered section content from submission_sections.
// Pre-Composer drafts continue to use the legacy body (run with
// -anchors=false).
//
// Order matches the default section spec in mig 146:
// letterhead, caption, introduction, requests, facts,
// legal_argument, evidence, exhibits, closing, signature.
func buildAnchoredDocumentXML() 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>`)

	anchorPair := func(key string) {
		plain(&b, "{{#section:"+key+"}}")
		plain(&b, "{{/section:"+key+"}}")
	}
	for _, key := range []string{
		"letterhead", "caption", "introduction", "requests",
		"facts", "legal_argument", "evidence", "exhibits",
		"closing", "signature",
	} {
		anchorPair(key)
	}

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

func buildLegacyDocumentXML() 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>`)

	skeletonBanner(&b)

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

	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 Sache")
	plain(&b, "{{parties.claimant.name}}")
	plain(&b, "vertreten durch {{parties.claimant.representative}}")
	bold(&b, "— Klägerin / Patentinhaberin / Anmelderin —")
	plain(&b, "")
	plain(&b, "gegen")
	plain(&b, "")
	plain(&b, "{{parties.defendant.name}}")
	plain(&b, "vertreten durch {{parties.defendant.representative}}")
	bold(&b, "— Beklagte / Einsprechende / Beschwerdegegnerin —")
	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}}")

	// t-paliad-287 — the dedicated Frist block was removed in 2026-05.
	// {{deadline.*}} placeholders stay resolvable in the variable bag
	// (lawyer can still drop them into a custom paragraph) but the
	// default skeleton no longer renders them in the submission body:
	// the deadline is internal/admin context and has no place in a
	// document going out to court.

	heading2(&b, "Schriftsatztext")
	plain(&b, "[Hier folgt der eigentliche Schriftsatztext. Diese Skelett-Vorlage enthält keine vorgefertigte Struktur — bitte gemäß Schriftsatz-Typ ({{rule.name}}) ergänzen.]")
	plain(&b, "")
	plain(&b, "[Body of the submission goes here. This skeleton template carries no pre-baked structure — fill in according to submission type ({{rule.name_en}}).]")

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

	// Locale-aware verification block — exercises every EN/DE alias the
	// variable bag carries (today.long_en, deadline.due_date_long_en,
	// project.our_side_en, project.proceeding.name_en, rule.name_en) and
	// the bare {{today}} alias. A lawyer customising the template can
	// delete this block; the renderer round-trips it cleanly today.
	heading2(&b, "Locale-aware variants (SKELETON)")
	plain(&b, "EN long date: {{today.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()
}

func skeletonBanner(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">SKELETON — universelle Vorlage (Schriftsatz-Typ-unabhängig, nicht freigegeben)</w:t></w:r></w:p>`)
}

func heading1(b *strings.Builder, text string) { paragraph(b, "Heading1", text, false) }

func heading2(b *strings.Builder, text string) { paragraph(b, "Heading2", text, false) }

func plain(b *strings.Builder, text string) { paragraph(b, "", text, false) }

func plainOptional(b *strings.Builder, text string) { paragraph(b, "", text, true) }

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>`)
}

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>`)
}

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
}
