package main

import (
	"archive/zip"
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"path"
	"sort"
	"strings"
	"time"
)

// ---------------------------------------------------------------------------
// DOCX writer
//
// The package this produces is a minimal but genuinely valid WordprocessingML
// document. Every XML part is hand-written with proper escaping; nothing here is
// templated from a pre-built .docx and no third-party library is involved.
//
// Parts written:
//   [Content_Types].xml
//   _rels/.rels
//   word/document.xml
//   word/_rels/document.xml.rels
//   word/media/imageN.png
// ---------------------------------------------------------------------------

const (
	nsRel      = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
	nsCT       = "http://schemas.openxmlformats.org/package/2006/content-types"
	nsPkgRel   = "http://schemas.openxmlformats.org/package/2006/relationships"
	relImage   = nsRel + "/image"
	relOffDoc  = nsRel + "/officeDocument"
	ctDocument = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"

	emuPerPixel = 9525    // at 96 dpi: 914400 EMU per inch / 96
	contentEMU  = 5943600 // 6.5 inch content width (Letter, 1 inch margins)
)

// zipEpoch is the timestamp stamped on every part, for reproducible packages.
// 1980-01-01 is the earliest date the MS-DOS timestamp in a zip can express.
var zipEpoch = time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC)

// docxImage is one PNG to embed.
type docxImage struct {
	Name  string // media file name, e.g. "image1.png"
	RelID string // relationship id, e.g. "rId4"
	Data  []byte
	W, H  int // pixels
	Descr string
}

// docxDoc is the assembled document body plus its media.
type docxDoc struct {
	body   bytes.Buffer
	images []docxImage
	nextID int
	docPr  int
}

func newDocxDoc() *docxDoc {
	// rId1 is reserved for nothing in document.xml.rels; start images at rId1
	// within the document part's own relationship namespace.
	return &docxDoc{nextID: 1, docPr: 1}
}

// xmlEsc escapes text for XML character data and attribute values.
func xmlEsc(s string) string {
	var b bytes.Buffer
	if err := xml.EscapeText(&b, []byte(sanitizeXMLText(s))); err != nil {
		// EscapeText only fails when the writer fails; bytes.Buffer never does.
		return ""
	}
	return b.String()
}

// sanitizeXMLText drops code points that are illegal in XML 1.0 (control
// characters other than tab, newline and carriage return).
func sanitizeXMLText(s string) string {
	return strings.Map(func(r rune) rune {
		switch {
		case r == '\t' || r == '\n' || r == '\r':
			return r
		case r < 0x20:
			return -1
		case r >= 0xD800 && r <= 0xDFFF:
			return -1
		case r == 0xFFFE || r == 0xFFFF:
			return -1
		}
		return r
	}, s)
}

// para writes a paragraph of plain text. Line breaks become <w:br/>.
func (d *docxDoc) para(text string, bold bool, sizeHalfPt int, color string) {
	d.body.WriteString("<w:p><w:pPr><w:spacing w:before=\"60\" w:after=\"120\"/></w:pPr>")
	d.run(text, bold, sizeHalfPt, color)
	d.body.WriteString("</w:p>")
}

func (d *docxDoc) run(text string, bold bool, sizeHalfPt int, color string) {
	d.body.WriteString("<w:r><w:rPr>")
	if bold {
		d.body.WriteString("<w:b/>")
	}
	if color != "" {
		fmt.Fprintf(&d.body, "<w:color w:val=\"%s\"/>", xmlEsc(color))
	}
	if sizeHalfPt > 0 {
		fmt.Fprintf(&d.body, "<w:sz w:val=\"%d\"/><w:szCs w:val=\"%d\"/>", sizeHalfPt, sizeHalfPt)
	}
	d.body.WriteString("</w:rPr>")
	for i, line := range strings.Split(text, "\n") {
		if i > 0 {
			d.body.WriteString("<w:br/>")
		}
		fmt.Fprintf(&d.body, "<w:t xml:space=\"preserve\">%s</w:t>", xmlEsc(line))
	}
	d.body.WriteString("</w:r>")
}

// rule writes an empty paragraph carrying a bottom border, used as a separator.
func (d *docxDoc) rule() {
	d.body.WriteString(`<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="C9CDD3"/></w:pBdr></w:pPr></w:p>`)
}

// image appends a PNG as an inline drawing sized in EMUs, scaled down to the
// content width when it is wider than the page allows.
func (d *docxDoc) image(data []byte, w, h int, descr string) {
	id := d.nextID
	d.nextID++
	img := docxImage{
		Name:  fmt.Sprintf("image%d.png", id),
		RelID: fmt.Sprintf("rId%d", id),
		Data:  data, W: w, H: h, Descr: descr,
	}
	d.images = append(d.images, img)

	cx := int64(w) * emuPerPixel
	cy := int64(h) * emuPerPixel
	if cx > contentEMU && cx > 0 {
		cy = cy * contentEMU / cx
		cx = contentEMU
	}
	if cx <= 0 {
		cx = 1
	}
	if cy <= 0 {
		cy = 1
	}
	pid := d.docPr
	d.docPr++

	fmt.Fprintf(&d.body,
		`<w:p><w:pPr><w:spacing w:before="60" w:after="160"/></w:pPr><w:r><w:drawing>`+
			`<wp:inline distT="0" distB="0" distL="0" distR="0">`+
			`<wp:extent cx="%d" cy="%d"/>`+
			`<wp:effectExtent l="0" t="0" r="0" b="0"/>`+
			`<wp:docPr id="%d" name="Picture %d" descr="%s"/>`+
			`<wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>`+
			`<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">`+
			`<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">`+
			`<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">`+
			`<pic:nvPicPr><pic:cNvPr id="%d" name="%s" descr="%s"/><pic:cNvPicPr/></pic:nvPicPr>`+
			`<pic:blipFill><a:blip r:embed="%s"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>`+
			`<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="%d" cy="%d"/></a:xfrm>`+
			`<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>`+
			`</pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>`,
		cx, cy, pid, pid, xmlEsc(descr), pid, xmlEsc(img.Name), xmlEsc(descr), img.RelID, cx, cy)
}

func (d *docxDoc) documentXML() []byte {
	var b bytes.Buffer
	b.WriteString(xml.Header)
	b.WriteString(`<w:document ` +
		`xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ` +
		`xmlns:r="` + nsRel + `" ` +
		`xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ` +
		`xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ` +
		`xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">` +
		`<w:body>`)
	b.Write(d.body.Bytes())
	// Letter portrait, 1 inch margins.
	b.WriteString(`<w:sectPr><w:pgSz w:w="12240" w:h="15840"/>` +
		`<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>` +
		`</w:sectPr></w:body></w:document>`)
	return b.Bytes()
}

func (d *docxDoc) documentRelsXML() []byte {
	var b bytes.Buffer
	b.WriteString(xml.Header)
	b.WriteString(`<Relationships xmlns="` + nsPkgRel + `">`)
	for _, img := range d.images {
		fmt.Fprintf(&b, `<Relationship Id="%s" Type="%s" Target="media/%s"/>`,
			xmlEsc(img.RelID), relImage, xmlEsc(img.Name))
	}
	b.WriteString(`</Relationships>`)
	return b.Bytes()
}

func contentTypesXML() []byte {
	var b bytes.Buffer
	b.WriteString(xml.Header)
	b.WriteString(`<Types xmlns="` + nsCT + `">` +
		`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
		`<Default Extension="xml" ContentType="application/xml"/>` +
		`<Default Extension="png" ContentType="image/png"/>` +
		`<Override PartName="/word/document.xml" ContentType="` + ctDocument + `"/>` +
		`</Types>`)
	return b.Bytes()
}

func rootRelsXML() []byte {
	var b bytes.Buffer
	b.WriteString(xml.Header)
	fmt.Fprintf(&b, `<Relationships xmlns="%s"><Relationship Id="rId1" Type="%s" Target="word/document.xml"/></Relationships>`,
		nsPkgRel, relOffDoc)
	return b.Bytes()
}

// zipBytes packages the document into a .docx byte slice.
func (d *docxDoc) zipBytes() ([]byte, error) {
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	add := func(name string, data []byte) error {
		// A fixed modification time keeps the package byte-for-byte
		// reproducible, so the ledger checksum of a rebuild from unchanged
		// inputs matches the previous one.
		w, err := zw.CreateHeader(&zip.FileHeader{
			Name:     name,
			Method:   zip.Deflate,
			Modified: zipEpoch,
		})
		if err != nil {
			return err
		}
		_, err = w.Write(data)
		return err
	}
	// [Content_Types].xml must be the first part in the package.
	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/document.xml", d.documentXML()); err != nil {
		return nil, err
	}
	if err := add("word/_rels/document.xml.rels", d.documentRelsXML()); err != nil {
		return nil, err
	}
	for _, img := range d.images {
		if err := add("word/media/"+img.Name, img.Data); err != nil {
			return nil, err
		}
	}
	if err := zw.Close(); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// ---------------------------------------------------------------------------
// DOCX verification
//
// Every build re-opens its own output and checks it, so a broken package is
// caught here and not in Word.
// ---------------------------------------------------------------------------

// DocxReport is the result of verifying a .docx package.
type DocxReport struct {
	Path          string   `json:"path"`
	Bytes         int64    `json:"bytes"`
	Parts         []string `json:"parts"`
	XMLParts      int      `json:"xml_parts_parsed"`
	MediaParts    int      `json:"media_parts"`
	Relationships int      `json:"relationships"`
	References    int      `json:"relationship_references"`
	OK            bool     `json:"ok"`
	Problems      []string `json:"problems"`
}

var requiredParts = []string{
	"[Content_Types].xml",
	"_rels/.rels",
	"word/document.xml",
	"word/_rels/document.xml.rels",
}

// verifyDocxBytes opens data as a zip and checks the structure end to end.
func verifyDocxBytes(name string, data []byte) DocxReport {
	rep := DocxReport{Path: name, Bytes: int64(len(data))}
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		rep.Problems = append(rep.Problems, fmt.Sprintf("not a readable zip archive: %v", err))
		return rep
	}
	contents := map[string][]byte{}
	for _, f := range zr.File {
		rep.Parts = append(rep.Parts, f.Name)
		rc, err := f.Open()
		if err != nil {
			rep.Problems = append(rep.Problems, fmt.Sprintf("part %s cannot be opened: %v", f.Name, err))
			continue
		}
		b, err := io.ReadAll(rc)
		rc.Close()
		if err != nil {
			rep.Problems = append(rep.Problems, fmt.Sprintf("part %s cannot be read: %v", f.Name, err))
			continue
		}
		contents[f.Name] = b
	}
	sort.Strings(rep.Parts)

	for _, want := range requiredParts {
		if _, ok := contents[want]; !ok {
			rep.Problems = append(rep.Problems, fmt.Sprintf("required part %s is missing", want))
		}
	}

	// Every XML part must parse.
	for _, name := range rep.Parts {
		if !strings.HasSuffix(name, ".xml") && !strings.HasSuffix(name, ".rels") {
			continue
		}
		rep.XMLParts++
		if err := parseXMLPart(contents[name]); err != nil {
			rep.Problems = append(rep.Problems, fmt.Sprintf("part %s is not well-formed XML: %v", name, err))
		}
	}
	for _, name := range rep.Parts {
		if strings.HasPrefix(name, "word/media/") {
			rep.MediaParts++
			if len(contents[name]) < 8 || !bytes.Equal(contents[name][:8], []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}) {
				rep.Problems = append(rep.Problems, fmt.Sprintf("media part %s is not a PNG", name))
			}
		}
	}

	// Content types must cover what the package holds.
	if ct, ok := contents["[Content_Types].xml"]; ok {
		s := string(ct)
		if !strings.Contains(s, `PartName="/word/document.xml"`) {
			rep.Problems = append(rep.Problems, "[Content_Types].xml has no Override for /word/document.xml")
		}
		if rep.MediaParts > 0 && !strings.Contains(s, `Extension="png"`) {
			rep.Problems = append(rep.Problems, "[Content_Types].xml has no Default for the png extension")
		}
	}

	// Root relationships must point at the main document part.
	if rels, ok := contents["_rels/.rels"]; ok {
		list, err := parseRelationships(rels)
		if err != nil {
			rep.Problems = append(rep.Problems, fmt.Sprintf("_rels/.rels: %v", err))
		}
		found := false
		for _, r := range list {
			if r.Type == relOffDoc {
				found = true
				if _, ok := contents[path.Clean(r.Target)]; !ok {
					rep.Problems = append(rep.Problems, fmt.Sprintf("_rels/.rels points at missing part %s", r.Target))
				}
			}
		}
		if !found {
			rep.Problems = append(rep.Problems, "_rels/.rels has no officeDocument relationship")
		}
	}

	// Document relationships: every id used in document.xml must resolve, and
	// every target must exist in the package.
	rels, err := parseRelationships(contents["word/_rels/document.xml.rels"])
	if err != nil {
		rep.Problems = append(rep.Problems, fmt.Sprintf("word/_rels/document.xml.rels: %v", err))
	}
	byID := map[string]relationship{}
	for _, r := range rels {
		if _, dup := byID[r.ID]; dup {
			rep.Problems = append(rep.Problems, fmt.Sprintf("relationship id %s is declared twice", r.ID))
		}
		byID[r.ID] = r
		target := path.Clean(path.Join("word", r.Target))
		if _, ok := contents[target]; !ok {
			rep.Problems = append(rep.Problems, fmt.Sprintf("relationship %s targets missing part %s", r.ID, target))
		}
	}
	rep.Relationships = len(rels)

	refs, err := relationshipRefs(contents["word/document.xml"])
	if err != nil {
		rep.Problems = append(rep.Problems, fmt.Sprintf("word/document.xml: %v", err))
	}
	rep.References = len(refs)
	for _, id := range refs {
		if _, ok := byID[id]; !ok {
			rep.Problems = append(rep.Problems,
				fmt.Sprintf("word/document.xml references relationship id %q, which is not declared in word/_rels/document.xml.rels", id))
		}
	}
	for _, r := range rels {
		if r.Type == relImage && !containsString(refs, r.ID) {
			rep.Problems = append(rep.Problems,
				fmt.Sprintf("relationship %s (%s) is declared but never referenced by word/document.xml", r.ID, r.Target))
		}
	}

	rep.OK = len(rep.Problems) == 0
	return rep
}

func containsString(ss []string, s string) bool {
	for _, v := range ss {
		if v == s {
			return true
		}
	}
	return false
}

type relationship struct {
	ID     string
	Type   string
	Target string
}

func parseRelationships(data []byte) ([]relationship, error) {
	if len(data) == 0 {
		return nil, fmt.Errorf("part is empty or missing")
	}
	dec := xml.NewDecoder(bytes.NewReader(data))
	var out []relationship
	for {
		tok, err := dec.Token()
		if err == io.EOF {
			return out, nil
		}
		if err != nil {
			return out, err
		}
		se, ok := tok.(xml.StartElement)
		if !ok || se.Name.Local != "Relationship" {
			continue
		}
		var r relationship
		for _, a := range se.Attr {
			switch a.Name.Local {
			case "Id":
				r.ID = a.Value
			case "Type":
				r.Type = a.Value
			case "Target":
				r.Target = a.Value
			}
		}
		if r.ID == "" {
			return out, fmt.Errorf("a Relationship element has no Id attribute")
		}
		out = append(out, r)
	}
}

// relationshipRefs returns every relationship id referenced from the given
// WordprocessingML part (r:embed, r:id and r:link attributes).
func relationshipRefs(data []byte) ([]string, error) {
	if len(data) == 0 {
		return nil, fmt.Errorf("part is empty or missing")
	}
	dec := xml.NewDecoder(bytes.NewReader(data))
	var out []string
	for {
		tok, err := dec.Token()
		if err == io.EOF {
			return out, nil
		}
		if err != nil {
			return out, err
		}
		se, ok := tok.(xml.StartElement)
		if !ok {
			continue
		}
		for _, a := range se.Attr {
			if a.Name.Space != nsRel {
				continue
			}
			switch a.Name.Local {
			case "embed", "id", "link":
				if a.Value != "" {
					out = append(out, a.Value)
				}
			}
		}
	}
}

func parseXMLPart(data []byte) error {
	dec := xml.NewDecoder(bytes.NewReader(data))
	for {
		_, err := dec.Token()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
	}
}
