package export

import (
	"bufio"
	"database/sql"
	"encoding/json"
	"fmt"
	"html"
	"math/rand"
	"os"
	"path/filepath"
	"strings"

	"pinscrape-allgo/internal/config"
	"pinscrape-allgo/internal/db"
	"pinscrape-allgo/internal/util"
)

type SnippetData struct {
	RelatedKW   []string `json:"related_kw"`
	Description []string `json:"description"`
}

type Article struct {
	ID        int64
	Keyword   string
	Slug      string
	Images    []Image
	AITitle   string
	AIContent string
	Snippet   SnippetData
	CreatedAt string
}

type Exporter struct {
	cfg        *config.Config
	bucket     string
	foldering  string
	authorName string
	baseURL    string
	siteName   string
	exportPath string
	engine     *TemplateEngine
	injects    map[string]string
}

func New(cfg *config.Config, bucket, foldering, authorName string) (*Exporter, error) {
	baseURL := fmt.Sprintf("https://storage.googleapis.com/%s", bucket)
	exportPath := cfg.Path(cfg.ExportFolder, foldering)
	if err := os.MkdirAll(exportPath, 0755); err != nil {
		return nil, err
	}
	engine, err := NewTemplateEngine(cfg.Path("templates"))
	if err != nil {
		return nil, err
	}
	injects := loadInjectFiles(cfg.Path(cfg.InjectFolder))
	return &Exporter{
		cfg:        cfg,
		bucket:     bucket,
		foldering:  foldering,
		authorName: authorName,
		baseURL:    strings.TrimRight(baseURL, "/"),
		siteName:   fmt.Sprintf("%s Ideas", authorName),
		exportPath: exportPath,
		engine:     engine,
		injects:    injects,
	}, nil
}

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

func (e *Exporter) headerInjection() 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() {
	for _, js := range []string{"ars.js", "head1.js", "head2.js"} {
		src := e.cfg.Path(e.cfg.InjectFolder, js)
		dst := filepath.Join(e.exportPath, js)
		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)
}

// Run exports the given chunk of articles with parallel workers.
func (e *Exporter) Run(articles []Article) error {
	e.copyJsFiles()
	e.generateRobotsTxt()

	total := len(articles)
	workers := e.cfg.WorkerCount
	if workers > total {
		workers = total
	}
	if workers < 1 {
		workers = 1
	}
	if workers == 1 || total < workers*50 {
		return e.runSequential(articles)
	}
	return e.runParallel(articles, workers)
}

// buildContent renders the body via BuildBody (Case A/B/C per row contents)
// and derives the meta description from the row.
func (e *Exporter) buildContent(row Article) (contentBody, description, schemaJSON, title string) {
	images := row.Images
	title = row.AITitle
	if title == "" {
		title = util.Ucwords(row.Keyword)
	}

	hasAI := strings.TrimSpace(row.AIContent) != ""
	hasDesc := db.SnippetExportable(mustSnippetJSON(row.Snippet))

	switch {
	case hasAI:
		description = ExtractFirstParagraph(row.AIContent)
	case hasDesc:
		description = row.Snippet.Description[0]
	}
	if description != "" && len(description) > 160 {
		description = description[:160] + "..."
	}
	if description == "" {
		description = fmt.Sprintf("Read about %s on %s.", title, e.siteName)
	}

	contentBody = BuildBody(row, e.cfg.Template, e.injects["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))
	return contentBody, description, schemaJSON, title
}

func mustSnippetJSON(sn SnippetData) string {
	if sn.Description == nil && sn.RelatedKW == nil {
		return ""
	}
	b, err := json.Marshal(sn)
	if err != nil {
		return ""
	}
	return string(b)
}

func (e *Exporter) renderArticle(row Article, related []ArticleMeta) (string, error) {
	images := row.Images
	if images == nil {
		images = []Image{}
	}
	contentBody, description, schemaJSON, title := e.buildContent(row)

	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: related,
		InjectHeader:    e.headerInjection(),
		InjectFooter:    e.injects["footer"],
		InjectAds:       e.injects["adsinarticle"],
		HasAIBlocks:     contentBody != "",
		HasRelated:      len(related) > 0,
		ContentBody:     contentBody,
		HeaderInject:    e.headerInjection(),
		FooterInject:    e.injects["footer"],
	}
	if len(images) > 0 {
		data.MainImage = &images[0]
		data.AdditionalImages = images[1:]
	}

	tplFile := e.cfg.Template + ".gohtml"
	tplPath := e.cfg.Path("templates", tplFile)
	if _, err := os.Stat(tplPath); os.IsNotExist(err) {
		return e.engine.Render("article.gohtml", data)
	}
	return e.engine.Render(tplFile, data)
}

func (e *Exporter) runSequential(articles []Article) error {
	sitemapPath := filepath.Join(e.exportPath, "sitemap.xml")
	var sw *bufio.Writer
	var sf *os.File
	if e.cfg.Export.GenerateSitemap {
		f, err := os.Create(sitemapPath)
		if err == nil {
			sf = f
			sw = bufio.NewWriterSize(f, 64*1024)
			sw.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
			sw.WriteString("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")
		}
	}

	var buffer []ArticleMeta
	var first []ArticleMeta
	for _, row := range articles {
		title := row.AITitle
		if title == "" {
			title = util.Ucwords(row.Keyword)
		}
		meta := ArticleMeta{Slug: row.Slug, Title: title, Keyword: util.Ucwords(row.Keyword)}
		if len(first) < 20 {
			first = append(first, meta)
		}
		buffer = append(buffer, meta)
		if len(buffer) > 100 {
			buffer = buffer[1:]
		}
		related := pickRelated(buffer)

		htmlContent, err := e.renderArticle(row, related)
		if err != nil {
			fmt.Printf("Warning: error rendering %s: %v\n", row.Slug, err)
			continue
		}
		writeFileBuffered(filepath.Join(e.exportPath, row.Slug+".html"), htmlContent)
		if sw != nil {
			url := fmt.Sprintf("%s/%s.html", e.baseURL, row.Slug)
			sw.WriteString(fmt.Sprintf("  <url><loc>%s</loc></url>\n", html.EscapeString(url)))
		}
	}

	if sw != nil {
		sw.WriteString("</urlset>")
		sw.Flush()
	}
	if sf != nil {
		sf.Close()
	}
	e.generateIndex(first)
	fmt.Printf("Export completed for bucket: %s (%s)\n", e.bucket, e.foldering)
	return nil
}

func pickRelated(buffer []ArticleMeta) []ArticleMeta {
	if len(buffer) == 0 {
		return nil
	}
	if len(buffer) <= 10 {
		out := make([]ArticleMeta, len(buffer))
		copy(out, buffer)
		return out
	}
	perm := rand.Perm(len(buffer))[:10]
	related := make([]ArticleMeta, 10)
	for i, idx := range perm {
		related[i] = buffer[idx]
	}
	return related
}

func (e *Exporter) runParallel(articles []Article, workers int) error {
	total := len(articles)
	fmt.Printf("  Parallel mode: %d workers for %d articles\n", workers, total)

	type job struct {
		idx     int
		row     Article
		related []ArticleMeta
	}

	var buffer []ArticleMeta
	var first []ArticleMeta
	jobs := make([]job, total)
	for i, row := range articles {
		title := row.AITitle
		if title == "" {
			title = util.Ucwords(row.Keyword)
		}
		meta := ArticleMeta{Slug: row.Slug, Title: title, Keyword: util.Ucwords(row.Keyword)}
		if len(first) < 20 {
			first = append(first, meta)
		}
		buffer = append(buffer, meta)
		if len(buffer) > 100 {
			buffer = buffer[1:]
		}
		jobs[i] = job{idx: i, row: row, related: pickRelated(buffer)}
	}

	sitemapEntries := make([]string, total)
	sem := make(chan struct{}, workers)

	type result struct {
		idx int
		url string
		err error
	}
	resCh := make(chan result, total)

	for _, j := range jobs {
		sem <- struct{}{}
		go func(j job) {
			defer func() { <-sem }()
			htmlContent, err := e.renderArticle(j.row, j.related)
			if err != nil {
				resCh <- result{idx: j.idx, err: err}
				return
			}
			writeFileBuffered(filepath.Join(e.exportPath, j.row.Slug+".html"), htmlContent)
			resCh <- result{idx: j.idx, url: fmt.Sprintf("%s/%s.html", e.baseURL, j.row.Slug)}
		}(j)
	}

	complete := 0
	for complete < total {
		r := <-resCh
		complete++
		if r.err != nil {
			fmt.Printf("  Worker error: %v\n", r.err)
			continue
		}
		sitemapEntries[r.idx] = r.url
	}

	if e.cfg.Export.GenerateSitemap {
		var entries []string
		for _, u := range sitemapEntries {
			if u != "" {
				entries = append(entries, u)
			}
		}
		e.writeSitemap(entries)
	}
	e.generateIndex(first)
	fmt.Printf("  Parallel export completed: %d articles with %d workers\n", total, workers)
	fmt.Printf("Export completed for bucket: %s (%s)\n", e.bucket, e.foldering)
	return nil
}

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 _, u := range entries {
		w.WriteString(fmt.Sprintf("  <url><loc>%s</loc></url>\n", html.EscapeString(u)))
	}
	w.WriteString("</urlset>")
	w.Flush()
}

func (e *Exporter) generateIndex(first []ArticleMeta) {
	if !e.cfg.Export.GenerateIndex {
		return
	}
	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:     first,
		InjectHeader: e.headerInjection(),
		InjectFooter: e.injects["footer"],
	}
	htmlContent, err := e.engine.Render("index.gohtml", data)
	if err != nil {
		fmt.Printf("Warning: error rendering index: %v\n", err)
		return
	}
	writeFileBuffered(filepath.Join(e.exportPath, "index.html"), htmlContent)
}

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

// StreamArticles feeds exportable articles from all databases through a
// channel. Exportable means images present AND (snippet description OR
// ai_content present). Rows outside that set are skipped entirely.
func StreamArticles(cfg *config.Config, out chan<- Article, errChan chan<- error) {
	defer close(out)
	files, err := db.ListDBFiles(cfg.Path(cfg.DataFolder))
	if err != nil {
		errChan <- err
		return
	}
	batchSize := cfg.Memory.BatchSize
	if batchSize <= 0 {
		batchSize = 100
	}
	for _, dbPath := range files {
		conn, err := db.Open(dbPath)
		if err != nil {
			errChan <- err
			return
		}
		lastID := int64(0)
		for {
			rows, qerr := conn.Query(`
				SELECT id, keyword, slug, images, ai_title, ai_content, snippet, created_at
				FROM posts
				WHERE id > ?
					AND images IS NOT NULL AND images != '' AND images != '[]'
				ORDER BY id ASC LIMIT ?`, lastID, batchSize)
			if qerr != nil {
				db.Close(conn)
				errChan <- qerr
				return
			}
			fetched := false
			for rows.Next() {
				var (
					id         int64
					keyword    string
					slug       string
					imagesJSON string
					aiTitle    sql.NullString
					aiContent  sql.NullString
					snippetRaw sql.NullString
					createdAt  string
				)
				if err := rows.Scan(&id, &keyword, &slug, &imagesJSON, &aiTitle, &aiContent, &snippetRaw, &createdAt); err != nil {
					rows.Close()
					db.Close(conn)
					errChan <- err
					return
				}
				fetched = true
				lastID = id

				hasAI := strings.TrimSpace(aiContent.String) != ""
				var sn SnippetData
				hasDesc := false
				if snippetRaw.Valid && snippetRaw.String != "" {
					if err := json.Unmarshal([]byte(snippetRaw.String), &sn); err == nil {
						hasDesc = db.SnippetExportable(snippetRaw.String)
					}
				}
				if !hasAI && !hasDesc {
					continue
				}

				var images []Image
				var rawImages []struct {
					Title    string `json:"title"`
					ImageURL string `json:"image_url"`
				}
				if err := json.Unmarshal([]byte(imagesJSON), &rawImages); err == nil {
					for _, ri := range rawImages {
						images = append(images, Image{ImageURL: ri.ImageURL, Title: ri.Title})
					}
				}

				title := ""
				if aiTitle.Valid {
					title = aiTitle.String
				}

				art := Article{
					ID:        id,
					Keyword:   keyword,
					Slug:      slug,
					Images:    images,
					AITitle:   title,
					AIContent: strings.TrimSpace(aiContent.String),
					Snippet:   sn,
					CreatedAt: createdAt,
				}
				out <- art
			}
			rows.Close()
			if !fetched {
				break
			}
		}
		db.Close(conn)
	}
	errChan <- nil
}
