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

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

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