package richtext

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

const schemaVersion = 1

// UltiDoc is the canonical on-disk format for TipTap documents.
type UltiDoc struct {
	SchemaVersion int               `json:"schemaVersion"`
	Editor        string            `json:"editor"`
	Source        *UltiDocSource    `json:"source,omitempty"`
	Content       json.RawMessage   `json:"content"`
	PageSetup       *UltiDocPageSetup       `json:"pageSetup,omitempty"`
	ParagraphStyles *UltiDocParagraphStyles `json:"paragraphStyles,omitempty"`
	YjsState        string                  `json:"yjsState,omitempty"`
	UpdatedAt     string            `json:"updatedAt,omitempty"`
}

type UltiDocSource struct {
	Path       string `json:"path"`
	Mime       string `json:"mime,omitempty"`
	ImportedAt string `json:"importedAt,omitempty"`
}

type UltiDocPageMargins struct {
	Top    float64 `json:"top"`
	Right  float64 `json:"right"`
	Bottom float64 `json:"bottom"`
	Left   float64 `json:"left"`
}

type UltiDocPageFillImage struct {
	Src  string `json:"src"`
	Mode string `json:"mode"`
}

type UltiDocPageWatermark struct {
	Kind        string  `json:"kind"`
	Text        string  `json:"text,omitempty"`
	Src         string  `json:"src,omitempty"`
	Color       string  `json:"color,omitempty"`
	Opacity     float64 `json:"opacity,omitempty"`
	RotationDeg float64 `json:"rotationDeg,omitempty"`
}

type UltiDocPageBackground struct {
	FillImage   *UltiDocPageFillImage   `json:"fillImage,omitempty"`
	Watermark   *UltiDocPageWatermark   `json:"watermark,omitempty"`
	GradientCss string                  `json:"gradientCss,omitempty"`
}

type UltiDocPageHeaderFooter struct {
	Content  json.RawMessage `json:"content"`
	HeightMm float64         `json:"heightMm,omitempty"`
}

type UltiDocPageNumberSettings struct {
	Enabled         bool   `json:"enabled"`
	Placement       string `json:"placement"`
	Align           string `json:"align"`
	StartAt         int    `json:"startAt"`
	ShowOnFirstPage bool   `json:"showOnFirstPage"`
}

type UltiDocPageSetup struct {
	WidthMm                        float64                  `json:"widthMm"`
	HeightMm                       float64                  `json:"heightMm"`
	MarginsMm                      UltiDocPageMargins       `json:"marginsMm"`
	HeaderMarginMm                 float64                  `json:"headerMarginMm,omitempty"`
	FooterMarginMm                 float64                  `json:"footerMarginMm,omitempty"`
	FormatID                       string                   `json:"formatId,omitempty"`
	Orientation                    string                   `json:"orientation,omitempty"`
	PageColor                      string                   `json:"pageColor,omitempty"`
	PageBackground                 *UltiDocPageBackground   `json:"pageBackground,omitempty"`
	Borders                        *UltiDocPageBorders      `json:"borders,omitempty"`
	Header                         *UltiDocPageHeaderFooter `json:"header,omitempty"`
	Footer                         *UltiDocPageHeaderFooter `json:"footer,omitempty"`
	HeaderFirstPage                *UltiDocPageHeaderFooter `json:"headerFirstPage,omitempty"`
	FooterFirstPage                *UltiDocPageHeaderFooter `json:"footerFirstPage,omitempty"`
	HeaderFooterDifferentFirstPage bool                     `json:"headerFooterDifferentFirstPage,omitempty"`
	HeaderFooterDifferentOddEven   bool                     `json:"headerFooterDifferentOddEven,omitempty"`
	PageNumbers                    *UltiDocPageNumberSettings `json:"pageNumbers,omitempty"`
}

type UltiDocPageBorderSide struct {
	Style   string  `json:"style"`
	WidthPx float64 `json:"widthPx"`
	Color   string  `json:"color"`
}

type UltiDocPageBorders struct {
	Top        UltiDocPageBorderSide `json:"top"`
	Right      UltiDocPageBorderSide `json:"right"`
	Bottom     UltiDocPageBorderSide `json:"bottom"`
	Left       UltiDocPageBorderSide `json:"left"`
	OffsetFrom string                `json:"offsetFrom,omitempty"`
}

func emptyDocContent() json.RawMessage {
	return json.RawMessage(`{"type":"doc","content":[{"type":"paragraph"}]}`)
}

func isEmptyDocContent(content json.RawMessage) bool {
	return !docContentHasText(content)
}

func docContentHasText(content json.RawMessage) bool {
	if len(content) == 0 {
		return false
	}
	var root any
	if err := json.Unmarshal(content, &root); err != nil {
		return false
	}
	return tipTapNodeHasText(root)
}

func tipTapNodeHasText(v any) bool {
	switch node := v.(type) {
	case map[string]any:
		if text, ok := node["text"].(string); ok && strings.TrimSpace(text) != "" {
			return true
		}
		if children, ok := node["content"]; ok {
			return tipTapNodeHasText(children)
		}
	case []any:
		for _, child := range node {
			if tipTapNodeHasText(child) {
				return true
			}
		}
	}
	return false
}

func NewUltiDoc(content json.RawMessage, source *UltiDocSource) UltiDoc {
	if len(content) == 0 {
		content = emptyDocContent()
	}
	return UltiDoc{
		SchemaVersion: schemaVersion,
		Editor:        "tiptap",
		Source:        source,
		Content:       content,
		UpdatedAt:     time.Now().UTC().Format(time.RFC3339),
	}
}

func preserveUltiDocMetadata(dst *UltiDoc, existing UltiDoc) {
	if dst.Source == nil && existing.Source != nil {
		dst.Source = existing.Source
	}
	if dst.PageSetup == nil && existing.PageSetup != nil {
		dst.PageSetup = existing.PageSetup
	}
	if dst.PageSetup != nil && existing.PageSetup != nil {
		if dst.PageSetup.PageBackground == nil && existing.PageSetup.PageBackground != nil {
			dst.PageSetup.PageBackground = existing.PageSetup.PageBackground
		}
	}
	if len(dst.Content) == 0 || isEmptyDocContent(dst.Content) {
		if len(existing.Content) > 0 && !isEmptyDocContent(existing.Content) {
			dst.Content = existing.Content
		}
	}
	if dst.ParagraphStyles == nil && existing.ParagraphStyles != nil {
		dst.ParagraphStyles = existing.ParagraphStyles
	}
	if dst.YjsState == "" && existing.YjsState != "" {
		dst.YjsState = existing.YjsState
	}
}

type ultiDocPatch struct {
	SchemaVersion int               `json:"schemaVersion"`
	Editor        string            `json:"editor"`
	Content       json.RawMessage   `json:"content"`
	Document      json.RawMessage   `json:"document"`
	PageSetup       *UltiDocPageSetup       `json:"pageSetup"`
	ParagraphStyles *UltiDocParagraphStyles `json:"paragraphStyles"`
	YjsState        string                  `json:"yjsState"`
}

// ApplyUltiDocPatch merges a partial JSON payload into an existing UltiDoc.
func ApplyUltiDocPatch(existing UltiDoc, raw json.RawMessage) (UltiDoc, error) {
	var patch ultiDocPatch
	if err := json.Unmarshal(raw, &patch); err != nil {
		return UltiDoc{}, err
	}

	doc := existing
	if doc.SchemaVersion == 0 {
		doc = NewUltiDoc(nil, nil)
		if len(existing.Content) > 0 {
			doc.Content = existing.Content
		}
	}

	content := patch.Content
	if len(content) == 0 && len(patch.Document) > 0 {
		content = patch.Document
	}
	if len(content) > 0 {
		doc.Content = content
	}
	if patch.PageSetup != nil {
		doc.PageSetup = patch.PageSetup
	}
	if patch.ParagraphStyles != nil {
		doc.ParagraphStyles = patch.ParagraphStyles
	}
	if patch.YjsState != "" {
		doc.YjsState = patch.YjsState
	}
	if patch.SchemaVersion > 0 {
		doc.SchemaVersion = patch.SchemaVersion
	}
	if patch.Editor != "" {
		doc.Editor = patch.Editor
	}
	preserveUltiDocMetadata(&doc, existing)
	return doc, nil
}

func ParseUltiDoc(raw []byte) (UltiDoc, error) {
	var doc UltiDoc
	if err := json.Unmarshal(raw, &doc); err != nil {
		return UltiDoc{}, err
	}
	if doc.SchemaVersion == 0 {
		doc.SchemaVersion = schemaVersion
	}
	if doc.Editor == "" {
		doc.Editor = "tiptap"
	}
	if len(doc.Content) == 0 {
		doc.Content = emptyDocContent()
	}
	return doc, nil
}

func (d UltiDoc) Marshal() ([]byte, error) {
	d.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
	if d.SchemaVersion == 0 {
		d.SchemaVersion = schemaVersion
	}
	if d.Editor == "" {
		d.Editor = "tiptap"
	}
	return json.MarshalIndent(d, "", "  ")
}

func textToDocContent(text string) json.RawMessage {
	paragraphs := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
	nodes := make([]map[string]any, 0, len(paragraphs))
	for _, para := range paragraphs {
		node := map[string]any{"type": "paragraph"}
		if strings.TrimSpace(para) != "" {
			node["content"] = []map[string]any{
				{"type": "text", "text": para},
			}
		}
		nodes = append(nodes, node)
	}
	if len(nodes) == 0 {
		nodes = append(nodes, map[string]any{"type": "paragraph"})
	}
	raw, _ := json.Marshal(map[string]any{"type": "doc", "content": nodes})
	return raw
}

func htmlToDocContent(html string) json.RawMessage {
	// Minimal bridge: strip tags naively for bootstrap; rich import uses client/docx pipeline.
	text := strings.TrimSpace(html)
	if text == "" {
		return emptyDocContent()
	}
	return textToDocContent(text)
}

func ImportBytes(name, mime string, body []byte) (json.RawMessage, error) {
	ext := strings.ToLower(name)
	if dot := strings.LastIndex(ext, "."); dot >= 0 {
		ext = ext[dot+1:]
	}
	switch ext {
	case "ultidoc", "json":
		if strings.HasSuffix(strings.ToLower(name), UltidocExtension) || strings.Contains(name, "ultidoc") {
			doc, err := ParseUltiDoc(body)
			if err != nil {
				return nil, err
			}
			return doc.Content, nil
		}
		var generic map[string]any
		if err := json.Unmarshal(body, &generic); err == nil {
			if t, _ := generic["type"].(string); t == "doc" {
				return json.RawMessage(body), nil
			}
		}
	case "txt", "log", "ini", "conf", "cfg", "env":
		return textToDocContent(string(body)), nil
	case "md", "markdown":
		return textToDocContent(string(body)), nil
	case "html", "htm":
		return htmlToDocContent(string(body)), nil
	}
	_ = mime
	return nil, fmt.Errorf("import requires client conversion for %q", name)
}
