package main

import (
	"bufio"
	"fmt"
	"html"
	"math/rand"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

type Exporter struct {
	config           Config
	bucket           string
	foldering        string
	authorName       string
	baseURL          string
	siteName         string
	exportPath       string
	engine           *TemplateEngine
	injectFiles      map[string]string
	relatedBuffer    []ArticleMeta
	relatedBufferMax int
	firstArticles    []ArticleMeta
	articleCount     int
	mu               sync.Mutex
}

func NewExporter(config Config, bucket, foldering, authorName string) *Exporter {
	baseURL := fmt.Sprintf("https://storage.googleapis.com/%s", bucket)
	exportPath := projectPath(config.ExportFolder, foldering)

	if err := os.MkdirAll(exportPath, 0755); err != nil {
		fmt.Printf("Warning: could not create export path: %v\n", err)
	}

	templateDir := projectPath("templates")
	engine, err := NewTemplateEngine(templateDir)
	if err != nil {
		fmt.Printf("Warning: template engine error: %v\n", err)
	}

	injectFiles := loadInjectFiles(config.InjectFolder)

	return &Exporter{
		config:           config,
		bucket:           bucket,
		foldering:        foldering,
		authorName:       authorName,
		baseURL:          strings.TrimRight(baseURL, "/"),
		siteName:         fmt.Sprintf("%s Ideas", authorName),
		exportPath:       exportPath,
		engine:           engine,
		injectFiles:      injectFiles,
		relatedBufferMax: 100,
	}
}

func loadInjectFiles(injectFolder string) map[string]string {
	result := make(map[string]string)
	injectPath := projectPath(injectFolder)

	files := []string{"footer.php", "adsinarticle.php"}
	for _, file := range files {
		filePath := filepath.Join(injectPath, file)
		data, err := os.ReadFile(filePath)
		if err == nil {
			name := strings.TrimSuffix(file, filepath.Ext(file))
			result[name] = string(data)
		}
	}
	return result
}

func (e *Exporter) generateHeaderInjection() string {
	return fmt.Sprintf(`<script src="https://storage.googleapis.com/%s/ars.js"></script>
<script src="https://storage.googleapis.com/%s/head1.js"></script>
<script src="https://storage.googleapis.com/%s/head2.js"></script>`, e.bucket, e.bucket, e.bucket)
}

func (e *Exporter) copyJsFiles() {
	injectPath := projectPath(e.config.InjectFolder)
	jsFiles := []string{"ars.js", "head1.js", "head2.js"}
	for _, jsFile := range jsFiles {
		src := filepath.Join(injectPath, jsFile)
		dst := filepath.Join(e.exportPath, jsFile)
		if _, err := os.Stat(src); err == nil {
			if _, err := os.Stat(dst); os.IsNotExist(err) {
				data, err := os.ReadFile(src)
				if err == nil {
					os.WriteFile(dst, data, 0644)
				}
			}
		}
	}
}

func (e *Exporter) generateRobotsTxt() {
	content := fmt.Sprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", e.baseURL)
	os.WriteFile(filepath.Join(e.exportPath, "robots.txt"), []byte(content), 0644)
}

func (e *Exporter) Run(articles []SnippetArticle) {
	e.copyJsFiles()
	e.generateRobotsTxt()

	if e.config.WorkerCount > 1 && len(articles) >= e.config.WorkerCount*50 {
		e.runParallel(articles)
	} else {
		e.runSequential(articles)
	}
}

func (e *Exporter) runSequential(articles []SnippetArticle) {
	e.relatedBuffer = nil
	e.firstArticles = nil
	e.articleCount = 0

	splitSize := e.config.SplitSize
	if splitSize <= 0 {
		splitSize = 5000
	}

	currentSplitNum := 0
	var sitemapWriter *bufio.Writer
	var sitemapFile *os.File

	for _, article := range articles {
		e.articleCount++
		splitFolderNum := ((e.articleCount - 1) / splitSize) + 1

		if splitFolderNum != currentSplitNum {
			if currentSplitNum > 0 {
				e.finalizeSitemap(sitemapWriter, sitemapFile)
			}
			currentSplitNum = splitFolderNum
			e.relatedBuffer = nil
			e.firstArticles = nil
			sitemapWriter, sitemapFile = e.initSitemap()
		}

		e.processArticle(article, sitemapWriter)
	}

	if currentSplitNum > 0 {
		e.finalizeSitemap(sitemapWriter, sitemapFile)
		e.generateIndex()
	}

	fmt.Printf("Export completed for bucket: %s (%s)\n", e.bucket, e.foldering)
}

func (e *Exporter) initSitemap() (*bufio.Writer, *os.File) {
	if !e.config.Export.GenerateSitemap {
		return nil, nil
	}
	path := filepath.Join(e.exportPath, "sitemap.xml")
	f, err := os.Create(path)
	if err != nil {
		fmt.Printf("Warning: could not create sitemap: %v\n", err)
		return nil, nil
	}
	w := bufio.NewWriterSize(f, 64*1024)
	w.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
	w.WriteString("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")
	return w, f
}

func (e *Exporter) finalizeSitemap(w *bufio.Writer, f *os.File) {
	if w != nil {
		w.WriteString("</urlset>")
		w.Flush()
	}
	if f != nil {
		f.Close()
	}
}

func (e *Exporter) processArticle(row SnippetArticle, sitemapWriter *bufio.Writer) {
	images := row.Images
	if images == nil {
		images = []Image{}
	}

	title := Ucwords(row.Keyword)

	description := ""
	if len(row.Snippet.Description) > 0 {
		description = row.Snippet.Description[0]
		if len(description) > 160 {
			description = description[:160] + "..."
		}
	}
	if description == "" {
		description = fmt.Sprintf("Explore %s ideas and inspiration.", title)
	}

	articleMeta := ArticleMeta{
		Slug:    row.Slug,
		Title:   title,
		Keyword: Ucwords(row.Keyword),
	}

	if len(e.firstArticles) < 20 {
		e.firstArticles = append(e.firstArticles, articleMeta)
	}

	e.relatedBuffer = append(e.relatedBuffer, articleMeta)
	if len(e.relatedBuffer) > e.relatedBufferMax {
		e.relatedBuffer = e.relatedBuffer[1:]
	}

	relatedArticles := e.getRelatedArticles()

	headerInject := e.generateHeaderInjection()
	footerInject := e.injectFiles["footer"]
	adsInject := e.injectFiles["adsinarticle"]

	firstImageURL := ""
	if len(images) > 0 {
		firstImageURL = images[0].ImageURL
	}

	schemaJSON := fmt.Sprintf(`{"@context":"https://schema.org","@type":"Article","headline":"%s","image":"%s","datePublished":"%s","author":{"@type":"Person","name":"%s"}}`,
		escapeJSON(title), escapeJSON(firstImageURL), escapeJSON(row.CreatedAt), escapeJSON(e.authorName))

	templateName := e.config.Template
	if templateName == "" {
		templateName = "modern"
	}

	contentBody := renderSnippetContent(
		row.Snippet.Description,
		images,
		row.Snippet.RelatedKW,
		adsInject,
		templateName,
	)

	tplFile := templateName + ".gohtml"
	tplPath := projectPath("templates", tplFile)

	var htmlContent string
	var err error

	if _, statErr := os.Stat(tplPath); os.IsNotExist(statErr) {
		data := ArticleData{
			Title:            title,
			Description:      description,
			Canonical:        fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug),
			Schema:           schemaJSON,
			SiteName:         e.siteName,
			AuthorName:       e.authorName,
			Date:             parseTime(row.CreatedAt).Format("Jan 02, 2006"),
			CreatedAt:        row.CreatedAt,
			AIBlocks:         []string{contentBody},
			Images:           images,
			RelatedArticles:  relatedArticles,
			InjectHeader:     headerInject,
			InjectFooter:     footerInject,
			InjectAds:        adsInject,
			HasAIBlocks:      contentBody != "",
			HasRelated:       len(relatedArticles) > 0,
			ContentBody:      contentBody,
			HeaderInject:     headerInject,
			FooterInject:     footerInject,
		}
		if len(images) > 0 {
			data.MainImage = &images[0]
			data.AdditionalImages = images[1:]
		}
		htmlContent, err = e.engine.Render("article.gohtml", data)
	} else {
		data := ArticleData{
			Title:            title,
			Description:      description,
			Canonical:        fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug),
			Schema:           schemaJSON,
			SiteName:         e.siteName,
			AuthorName:       e.authorName,
			Date:             parseTime(row.CreatedAt).Format("Jan 02, 2006"),
			CreatedAt:        row.CreatedAt,
			AIBlocks:         []string{contentBody},
			Images:           images,
			RelatedArticles:  relatedArticles,
			InjectHeader:     headerInject,
			InjectFooter:     footerInject,
			InjectAds:        adsInject,
			HasAIBlocks:      contentBody != "",
			HasRelated:       len(relatedArticles) > 0,
			HeaderInject:     headerInject,
			FooterInject:     footerInject,
		}
		if len(images) > 0 {
			data.MainImage = &images[0]
			data.AdditionalImages = images[1:]
		}
		htmlContent, err = e.engine.Render(tplFile, data)
	}

	if err != nil {
		fmt.Printf("Warning: error rendering %s: %v\n", row.Slug, err)
		return
	}

	outputPath := filepath.Join(e.exportPath, row.Slug+".html")
	writeFileBuffered(outputPath, htmlContent)

	if sitemapWriter != nil {
		url := fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug)
		sitemapWriter.WriteString(fmt.Sprintf("  <url><loc>%s</loc></url>\n", html.EscapeString(url)))
	}
}

func (e *Exporter) getRelatedArticles() []ArticleMeta {
	candidates := e.relatedBuffer
	if len(candidates) == 0 {
		return nil
	}
	if len(candidates) <= 10 {
		return candidates
	}

	indices := rand.Perm(len(candidates))[:10]
	related := make([]ArticleMeta, 10)
	for i, idx := range indices {
		related[i] = candidates[idx]
	}
	return related
}

func (e *Exporter) generateIndex() {
	articles := e.firstArticles
	headerInject := e.generateHeaderInjection()
	footerInject := e.injectFiles["footer"]

	schemaJSON := fmt.Sprintf(`{"@context":"https://schema.org","@type":"CollectionPage","name":"%s Blog Ideas","description":"Explore our curated list of design inspirations on %s.","url":"%s/index.html"}`,
		escapeJSON(e.siteName), escapeJSON(e.siteName), escapeJSON(e.baseURL))

	data := IndexData{
		SiteName:     e.siteName,
		AuthorName:   e.authorName,
		Description:  fmt.Sprintf("Explore our curated list of design inspirations on %s.", e.siteName),
		Canonical:    fmt.Sprintf("%s/index.html", e.baseURL),
		Schema:       schemaJSON,
		Articles:     articles,
		InjectHeader: headerInject,
		InjectFooter: footerInject,
	}

	htmlContent, err := e.engine.Render("index.gohtml", data)
	if err != nil {
		fmt.Printf("Warning: error rendering index: %v\n", err)
		return
	}

	outputPath := filepath.Join(e.exportPath, "index.html")
	writeFileBuffered(outputPath, htmlContent)
}

func (e *Exporter) runParallel(articles []SnippetArticle) {
	totalArticles := len(articles)
	workerCount := e.config.WorkerCount
	if workerCount > totalArticles {
		workerCount = totalArticles
	}

	fmt.Printf("  Parallel mode: %d workers for %d articles\n", workerCount, totalArticles)

	relatedMap := e.preComputeRelated(articles)
	firstArticles := e.preComputeFirstArticles(articles)

	batchSize := (totalArticles + workerCount - 1) / workerCount
	var wg sync.WaitGroup
	sitemapChan := make(chan string, totalArticles)
	errChan := make(chan error, totalArticles)

	for i := 0; i < workerCount; i++ {
		start := i * batchSize
		end := start + batchSize
		if end > totalArticles {
			end = totalArticles
		}
		if start >= totalArticles {
			break
		}

		batch := articles[start:end]
		batchRelated := make(map[int][]ArticleMeta)
		for j := range batch {
			globalIdx := start + j
			if rel, ok := relatedMap[globalIdx]; ok {
				batchRelated[j] = rel
			}
		}

		wg.Add(1)
		go func(workerID int, batch []SnippetArticle, related map[int][]ArticleMeta) {
			defer wg.Done()
			e.processBatch(workerID, batch, related, sitemapChan, errChan)
		}(i, batch, batchRelated)
	}

	go func() {
		wg.Wait()
		close(sitemapChan)
		close(errChan)
	}()

	var allSitemapEntries []string
	var workerErrors []error

	var sitemapWg sync.WaitGroup
	sitemapWg.Add(2)

	go func() {
		defer sitemapWg.Done()
		for entry := range sitemapChan {
			allSitemapEntries = append(allSitemapEntries, entry)
		}
	}()

	go func() {
		defer sitemapWg.Done()
		for err := range errChan {
			if err != nil {
				workerErrors = append(workerErrors, err)
			}
		}
	}()

	sitemapWg.Wait()

	for _, err := range workerErrors {
		fmt.Printf("  Worker error: %v\n", err)
	}

	if len(allSitemapEntries) > 0 && e.config.Export.GenerateSitemap {
		e.writeSitemap(allSitemapEntries)
	}

	e.firstArticles = firstArticles
	e.generateIndex()

	actualWorkers := (totalArticles + batchSize - 1) / batchSize
	fmt.Printf("  Parallel export completed: %d articles with %d workers\n", totalArticles, actualWorkers)
	fmt.Printf("Export completed for bucket: %s (%s)\n", e.bucket, e.foldering)
}

func (e *Exporter) processBatch(workerID int, batch []SnippetArticle, relatedMap map[int][]ArticleMeta, sitemapChan chan<- string, errChan chan<- error) {
	templateName := e.config.Template
	if templateName == "" {
		templateName = "modern"
	}

	headerInject := e.generateHeaderInjection()
	footerInject := e.injectFiles["footer"]
	adsInject := e.injectFiles["adsinarticle"]

	for i, row := range batch {
		images := row.Images
		if images == nil {
			images = []Image{}
		}

		title := Ucwords(row.Keyword)

		description := ""
		if len(row.Snippet.Description) > 0 {
			description = row.Snippet.Description[0]
			if len(description) > 160 {
				description = description[:160] + "..."
			}
		}
		if description == "" {
			description = fmt.Sprintf("Explore %s ideas and inspiration.", title)
		}

		relatedArticles := relatedMap[i]

		firstImageURL := ""
		if len(images) > 0 {
			firstImageURL = images[0].ImageURL
		}

		schemaJSON := fmt.Sprintf(`{"@context":"https://schema.org","@type":"Article","headline":"%s","image":"%s","datePublished":"%s","author":{"@type":"Person","name":"%s"}}`,
			escapeJSON(title), escapeJSON(firstImageURL), escapeJSON(row.CreatedAt), escapeJSON(e.authorName))

		contentBody := renderSnippetContent(
			row.Snippet.Description,
			images,
			row.Snippet.RelatedKW,
			adsInject,
			templateName,
		)

		tplFile := templateName + ".gohtml"
		tplPath := projectPath("templates", tplFile)

		var htmlContent string
		var err error

		if _, statErr := os.Stat(tplPath); os.IsNotExist(statErr) {
			data := ArticleData{
				Title:            title,
				Description:      description,
				Canonical:        fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug),
				Schema:           schemaJSON,
				SiteName:         e.siteName,
				AuthorName:       e.authorName,
				Date:             parseTime(row.CreatedAt).Format("Jan 02, 2006"),
				CreatedAt:        row.CreatedAt,
				AIBlocks:         []string{contentBody},
				Images:           images,
				RelatedArticles:  relatedArticles,
				InjectHeader:     headerInject,
				InjectFooter:     footerInject,
				InjectAds:        adsInject,
				HasAIBlocks:      contentBody != "",
				HasRelated:       len(relatedArticles) > 0,
				ContentBody:      contentBody,
				HeaderInject:     headerInject,
				FooterInject:     footerInject,
			}
			if len(images) > 0 {
				data.MainImage = &images[0]
				data.AdditionalImages = images[1:]
			}
			htmlContent, err = e.engine.Render("article.gohtml", data)
		} else {
			data := ArticleData{
				Title:            title,
				Description:      description,
				Canonical:        fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug),
				Schema:           schemaJSON,
				SiteName:         e.siteName,
				AuthorName:       e.authorName,
				Date:             parseTime(row.CreatedAt).Format("Jan 02, 2006"),
				CreatedAt:        row.CreatedAt,
				AIBlocks:         []string{contentBody},
				Images:           images,
				RelatedArticles:  relatedArticles,
				InjectHeader:     headerInject,
				InjectFooter:     footerInject,
				InjectAds:        adsInject,
				HasAIBlocks:      contentBody != "",
				HasRelated:       len(relatedArticles) > 0,
				HeaderInject:     headerInject,
				FooterInject:     footerInject,
			}
			if len(images) > 0 {
				data.MainImage = &images[0]
				data.AdditionalImages = images[1:]
			}
			htmlContent, err = e.engine.Render(tplFile, data)
		}

		if err != nil {
			errChan <- fmt.Errorf("worker %d: error rendering %s: %w", workerID, row.Slug, err)
			continue
		}

		outputPath := filepath.Join(e.exportPath, row.Slug+".html")
		writeFileBuffered(outputPath, htmlContent)

		sitemapChan <- fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug)
	}
}

func (e *Exporter) preComputeRelated(articles []SnippetArticle) map[int][]ArticleMeta {
	relatedMap := make(map[int][]ArticleMeta)
	var buffer []ArticleMeta
	bufferMax := 100

	for i, row := range articles {
		title := Ucwords(row.Keyword)
		meta := ArticleMeta{
			Slug:    row.Slug,
			Title:   title,
			Keyword: Ucwords(row.Keyword),
		}
		buffer = append(buffer, meta)
		if len(buffer) > bufferMax {
			buffer = buffer[1:]
		}

		if len(buffer) <= 10 {
			relatedMap[i] = make([]ArticleMeta, len(buffer))
			copy(relatedMap[i], buffer)
		} else {
			indices := rand.Perm(len(buffer))[:10]
			related := make([]ArticleMeta, 10)
			for j, idx := range indices {
				related[j] = buffer[idx]
			}
			relatedMap[i] = related
		}
	}
	return relatedMap
}

func (e *Exporter) preComputeFirstArticles(articles []SnippetArticle) []ArticleMeta {
	var first []ArticleMeta
	limit := 20
	if len(articles) < limit {
		limit = len(articles)
	}
	for i := 0; i < limit; i++ {
		row := articles[i]
		title := Ucwords(row.Keyword)
		first = append(first, ArticleMeta{
			Slug:    row.Slug,
			Title:   title,
			Keyword: Ucwords(row.Keyword),
		})
	}
	return first
}

func (e *Exporter) writeSitemap(entries []string) {
	path := filepath.Join(e.exportPath, "sitemap.xml")
	f, err := os.Create(path)
	if err != nil {
		fmt.Printf("Warning: could not create sitemap: %v\n", err)
		return
	}
	defer f.Close()

	w := bufio.NewWriterSize(f, 64*1024)
	w.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
	w.WriteString("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")
	for _, url := range entries {
		w.WriteString(fmt.Sprintf("  <url><loc>%s</loc></url>\n", html.EscapeString(url)))
	}
	w.WriteString("</urlset>")
	w.Flush()
}

func writeFileBuffered(path string, content string) {
	f, err := os.Create(path)
	if err != nil {
		fmt.Printf("Warning: could not create file %s: %v\n", path, err)
		return
	}
	defer f.Close()

	w := bufio.NewWriterSize(f, 64*1024)
	w.WriteString(content)
	w.Flush()
}

func Ucwords(s string) string {
	words := strings.Fields(s)
	for i, word := range words {
		if len(word) > 0 {
			words[i] = strings.ToUpper(word[:1]) + word[1:]
		}
	}
	return strings.Join(words, " ")
}

func escapeJSON(s string) string {
	s = strings.ReplaceAll(s, `\`, `\\`)
	s = strings.ReplaceAll(s, `"`, `\"`)
	s = strings.ReplaceAll(s, "\n", `\n`)
	s = strings.ReplaceAll(s, "\r", `\r`)
	s = strings.ReplaceAll(s, "\t", `\t`)
	return s
}
