package export

import (
	"fmt"
	"html"
	"strings"

	"pinscrape-allgo/internal/util"
)

// randIntN is indirected so tests can make H2 picks deterministic.
var randIntN = util.FastRand

type bodyBuilder struct {
	sb      strings.Builder
	tpl     string
	ads     string
	related []string
	usedRel map[int]bool

	ai   []string
	desc []string
	imgs []Image
}

// BuildBody renders the article body according to the row contents:
//
//	Case A: images + description + ai_content
//	Case B: images + description only
//	Case C: images + ai_content only
//
// Slot rules: a slot of N items draws from the head of the queue; when fewer
// remain the leftovers are used and later slots get nothing. "rest" drains the
// queue. H2 rows are skipped when related_kw is empty. Ads rows are skipped
// when the ads inject is empty.
func BuildBody(row Article, templateType string, ads string) string {
	b := &bodyBuilder{
		tpl:     templateType,
		ads:     strings.TrimSpace(ads),
		related: row.Snippet.RelatedKW,
		usedRel: make(map[int]bool),
		ai:      splitAIBlocks(row.AIContent),
		desc:    nonEmptyDescriptions(row.Snippet.Description),
		imgs:    row.Images,
	}

	hasAI := len(b.ai) > 0
	hasDesc := len(b.desc) > 0

	switch {
	case hasAI && hasDesc:
		b.caseA()
	case hasDesc:
		b.caseB()
	case hasAI:
		b.caseC()
	}
	return b.sb.String()
}

// Case A: images + snippet description + ai_content.
func (b *bodyBuilder) caseA() {
	b.writeAds()
	b.writeAI(3)
	b.writeAds()
	b.writeImages(1)
	b.writeAI(3)
	b.writeAds()
	b.writeImages(2)
	b.writeAI(3)
	b.writeImages(1)
	b.writeAds()
	b.writeAI(4)
	b.writeAds()
	b.writeAI(2)
	b.writeImages(2)
	b.writeAIRest()

	b.writeH2()
	b.writeDesc(2)
	b.writeImages(1)
	b.writeH2()
	b.writeDesc(4)
	b.writeAds()
	b.writeImagesRest()
	b.writeH2()
	b.writeDescRest()
	b.writeAds()
}

// Case B: images + snippet description only.
func (b *bodyBuilder) caseB() {
	b.writeAds()
	b.writeDesc(3)
	b.writeAds()
	b.writeImages(1)
	b.writeH2()
	b.writeDesc(3)
	b.writeAds()
	b.writeImages(2)
	b.writeH2()
	b.writeDesc(3)
	b.writeImages(1)
	b.writeAds()
	b.writeH2()
	b.writeDesc(4)
	b.writeAds()
	b.writeDesc(2)
	b.writeImagesRest()
	b.writeH2()
	b.writeDescRest()
	b.writeAds()
}

// Case C: images + ai_content only.
func (b *bodyBuilder) caseC() {
	b.writeAds()
	b.writeAI(3)
	b.writeAds()
	b.writeImages(1)
	b.writeAI(3)
	b.writeAds()
	b.writeImages(2)
	b.writeAI(3)
	b.writeImages(1)
	b.writeAds()
	b.writeAI(4)
	b.writeAds()
	b.writeAI(2)
	b.writeImagesRest()
	b.writeAIRest()
	b.writeAds()
}

func (b *bodyBuilder) writeAds() {
	if b.ads != "" {
		b.sb.WriteString(b.ads)
	}
}

func (b *bodyBuilder) writeH2() {
	kw := PickRandomRelated(b.related, b.usedRel)
	if kw == "" {
		return
	}
	b.sb.WriteString(fmt.Sprintf("<h2>%s</h2>", html.EscapeString(kw)))
}

func (b *bodyBuilder) writeAI(n int) {
	for i := 0; i < n && len(b.ai) > 0; i++ {
		block := b.ai[0]
		b.ai = b.ai[1:]
		b.sb.WriteString(RenderParagraphHTML(`<div class="mb-4">`+block+`</div>`, b.tpl))
	}
}

func (b *bodyBuilder) writeAIRest() {
	for len(b.ai) > 0 {
		b.writeAI(1)
	}
}

func (b *bodyBuilder) writeDesc(n int) {
	for i := 0; i < n && len(b.desc) > 0; i++ {
		text := b.desc[0]
		b.desc = b.desc[1:]
		b.sb.WriteString(RenderParagraphHTML("<p>"+html.EscapeString(text)+"</p>", b.tpl))
	}
}

func (b *bodyBuilder) writeDescRest() {
	for len(b.desc) > 0 {
		b.writeDesc(1)
	}
}

func (b *bodyBuilder) writeImages(n int) {
	for i := 0; i < n && len(b.imgs) > 0; i++ {
		img := b.imgs[0]
		b.imgs = b.imgs[1:]
		b.sb.WriteString(RenderImageHTML(img, b.tpl))
	}
}

func (b *bodyBuilder) writeImagesRest() {
	for len(b.imgs) > 0 {
		b.writeImages(1)
	}
}

// splitAIBlocks splits raw AI content into block units on closing block tags,
// the same mechanism the previous renderer used.
func splitAIBlocks(aiContent string) []string {
	if strings.TrimSpace(aiContent) == "" {
		return nil
	}
	var blocks []string
	parts := tagSplitRe.Split(aiContent, -1)
	matches := tagSplitRe.FindAllString(aiContent, -1)

	current := ""
	for i, part := range parts {
		current += part
		if i < len(matches) {
			current += matches[i]
			trimmed := strings.TrimSpace(current)
			if trimmed != "" && strings.TrimSpace(StripTags(trimmed)) != "" {
				blocks = append(blocks, trimmed)
			}
			current = ""
		}
	}
	if trimmed := strings.TrimSpace(current); trimmed != "" && strings.TrimSpace(StripTags(trimmed)) != "" {
		blocks = append(blocks, trimmed)
	}
	return blocks
}

func nonEmptyDescriptions(descriptions []string) []string {
	out := make([]string, 0, len(descriptions))
	for _, d := range descriptions {
		if t := strings.TrimSpace(d); t != "" {
			out = append(out, t)
		}
	}
	return out
}
