package run

import (
	"bufio"
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"pinscrape-allgo/internal/clients/ai"
	"pinscrape-allgo/internal/clients/pinterest"
	"pinscrape-allgo/internal/clients/suggest"
	"pinscrape-allgo/internal/config"
	"pinscrape-allgo/internal/csvgen"
	"pinscrape-allgo/internal/db"
	"pinscrape-allgo/internal/deploy"
	"pinscrape-allgo/internal/export"
	"pinscrape-allgo/internal/metrics"
	"pinscrape-allgo/internal/pipeline"
	"pinscrape-allgo/internal/proxypool"
	"pinscrape-allgo/internal/ratelimit"
	"pinscrape-allgo/internal/report"
	"pinscrape-allgo/internal/submit"
	"pinscrape-allgo/internal/tlspool"
	"pinscrape-allgo/internal/util"
)

type Env struct {
	Cfg     *config.Config
	Metrics *metrics.Metrics
	Pin     *pinterest.Client
	Suggest *suggest.Client
	AI      *ai.Client
}

func NewEnv(cfg *config.Config) *Env {
	m := metrics.New()
	proxyURLs := util.LoadProxies(cfg.Path("proxies.txt"))
	log.Printf("Loaded %d proxies", len(proxyURLs))

	pool := proxypool.New(proxyURLs)
	sessions := tlspool.New(cfg.Go.SessionBankSize, cfg.Go.SessionRotateAfter)
	// Shared by Pinterest and Suggest: one global request rate across all
	// workers, plus a per-proxy bucket keyed by the proxy URL.
	limiter := ratelimit.New(cfg.Go.GlobalRatePerSec, cfg.Go.ProxyRatePerSec)

	keys := ai.LoadKeys(cfg)
	chain := ai.BuildModelChain(cfg)
	aiClient := ai.New(cfg, keys, chain, m)

	return &Env{
		Cfg:     cfg,
		Metrics: m,
		Pin:     pinterest.New(cfg, pool, sessions, limiter, m),
		Suggest: suggest.New(cfg, pool, sessions, limiter, m),
		AI:      aiClient,
	}
}

// Context returns a context canceled on SIGINT/SIGTERM so in-flight batches
// commit and pending rows are simply retried on the next run.
func Context() (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithCancel(context.Background())
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-ch
		log.Println("Shutdown requested: finishing current batches...")
		cancel()
	}()
	return ctx, cancel
}

func (e *Env) pipelineEnv() *pipeline.Env {
	return &pipeline.Env{
		Cfg:     e.Cfg,
		Metrics: e.Metrics,
		Pin:     e.Pin,
		Suggest: e.Suggest,
		AI:      e.AI,
	}
}

// PipelineEnv exposes the pipeline adapter for the CLI commands.
func (e *Env) PipelineEnv() *pipeline.Env {
	return e.pipelineEnv()
}

// ScrapeAll runs the two scrape phases in sequence: phase 1 fills images and
// descriptions from Pinterest (one request per keyword), phase 2 fills
// related keywords from the suggest chain. The phases never run in parallel,
// so at most one request per worker is in flight. Prints the scrape summary
// block when done.
func ScrapeAll(ctx context.Context, e *Env) error {
	if err := scrapePhases(ctx, e); err != nil {
		return err
	}
	printScrape(e.Cfg, report.Want{Images: true, Description: true, RelatedKW: true})
	return nil
}

// scrapePhases runs the two scrape phases without a summary, so workflows can
// print one combined block at the end.
func scrapePhases(ctx context.Context, e *Env) error {
	pe := e.PipelineEnv()
	if err := pipeline.ScrapeImagesDesc(ctx, pe); err != nil {
		return err
	}
	return pipeline.ScrapeRelatedKw(ctx, pe)
}

// workflowTitlesSteps = scrape + AI titles + status update, no summary.
func workflowTitlesSteps(ctx context.Context, e *Env) error {
	if err := scrapePhases(ctx, e); err != nil {
		return err
	}
	if err := pipeline.RunAI(ctx, e.PipelineEnv(), "title"); err != nil {
		return err
	}
	return pipeline.UpdateStatus(e.Cfg)
}

// WorkflowTitles = scrape images + snippets + AI titles. Summary shows the
// three scrape counters plus AI Title.
func WorkflowTitles(ctx context.Context, e *Env) error {
	if err := workflowTitlesSteps(ctx, e); err != nil {
		return err
	}
	printScrape(e.Cfg, report.Want{Images: true, Description: true, RelatedKW: true, AITitle: true})
	return nil
}

// WorkflowAll = WorkflowTitles + AI articles.
func WorkflowAll(ctx context.Context, e *Env) error {
	if err := workflowTitlesSteps(ctx, e); err != nil {
		return err
	}
	if err := pipeline.RunAI(ctx, e.PipelineEnv(), "article"); err != nil {
		return err
	}
	printScrape(e.Cfg, report.Want{Images: true, Description: true, RelatedKW: true, AITitle: true, AIArticles: true})
	return nil
}

// GenerateAI runs one standalone AI step (menu 3/4) and prints its summary
// line.
func GenerateAI(ctx context.Context, e *Env, mode string) error {
	if err := pipeline.RunAI(ctx, e.PipelineEnv(), mode); err != nil {
		return err
	}
	want := report.Want{AITitle: true}
	if mode == "article" {
		want = report.Want{AIArticles: true}
	}
	printScrape(e.Cfg, want)
	return nil
}

// printScrape prints the scrape summary block to stdout.
func printScrape(cfg *config.Config, want report.Want) {
	for _, line := range report.Scrape(cfg, want) {
		fmt.Println(line)
	}
}

// DeleteAITitles resets ai_title for all rows in all databases.
func DeleteAITitles(cfg *config.Config) error {
	return deleteColumnAll(cfg, "ai_title")
}

// DeleteAIArticles resets ai_content for all rows in all databases.
func DeleteAIArticles(cfg *config.Config) error {
	return deleteColumnAll(cfg, "ai_content")
}

func deleteColumnAll(cfg *config.Config, column string) error {
	files, err := db.ListDBFiles(cfg.Path(cfg.DataFolder))
	if err != nil {
		return err
	}
	var total int64
	for _, f := range files {
		conn, err := db.Open(f)
		if err != nil {
			return err
		}
		var n int64
		if column == "ai_title" {
			n, err = db.DeleteAITitles(conn)
		} else {
			n, err = db.DeleteAIArticles(conn)
		}
		db.Close(conn)
		if err != nil {
			return err
		}
		if n > 0 {
			log.Printf("%s: cleared %d %s values", filepath.Base(f), n, column)
		}
		total += n
	}
	log.Printf("Total %s cleared: %d", column, total)
	return nil
}

// ExportOptions carries the interactive export settings.
type ExportOptions struct {
	SplitSize int
	Template  string
	Workers   int
}

// PromptExportOptions asks for split size, template and workers like the main
// project's exporter, then confirms before proceeding.
func PromptExportOptions(cfg *config.Config) (ExportOptions, bool) {
	reader := bufio.NewReader(os.Stdin)
	fmt.Println(strings.Repeat("=", 40))
	fmt.Println("   Export Articles (Go)")
	fmt.Println(strings.Repeat("=", 40))
	fmt.Println()

	opts := ExportOptions{
		SplitSize: cfg.SplitSize,
		Template:  cfg.Template,
		Workers:   cfg.WorkerCount,
	}
	if opts.SplitSize <= 0 {
		opts.SplitSize = 5000
	}
	if opts.Template == "" {
		opts.Template = "modern"
	}
	if opts.Workers <= 0 {
		opts.Workers = 1
	}

	fmt.Printf("Split size [%d]: ", opts.SplitSize)
	if line, _ := reader.ReadString('\n'); strings.TrimSpace(line) != "" {
		if v, err := strconv.Atoi(strings.TrimSpace(line)); err == nil && v > 0 {
			opts.SplitSize = v
		}
	}

	templates := availableTemplates(cfg)
	fmt.Println()
	fmt.Println("Available templates:")
	for i, name := range templates {
		marker := ""
		if name == opts.Template {
			marker = " (current)"
		}
		fmt.Printf("  %2d. %s%s\n", i+1, name, marker)
	}
	fmt.Println()
	fmt.Printf("Template number [1-%d] (default: %s): ", len(templates), opts.Template)
	if line, _ := reader.ReadString('\n'); strings.TrimSpace(line) != "" {
		input := strings.TrimSpace(line)
		if idx, err := strconv.Atoi(input); err == nil && idx >= 1 && idx <= len(templates) {
			opts.Template = templates[idx-1]
		} else {
			for _, name := range templates {
				if name == input {
					opts.Template = name
					break
				}
			}
		}
	}

	fmt.Println()
	fmt.Printf("Workers [%d]: ", opts.Workers)
	if line, _ := reader.ReadString('\n'); strings.TrimSpace(line) != "" {
		if v, err := strconv.Atoi(strings.TrimSpace(line)); err == nil && v > 0 {
			opts.Workers = v
		}
	}

	fmt.Println()
	fmt.Printf("Split size : %d\n", opts.SplitSize)
	fmt.Printf("Template   : %s\n", opts.Template)
	fmt.Printf("Workers    : %d\n", opts.Workers)
	fmt.Println()
	fmt.Print("Proceed with export? [Y/n]: ")
	if line, _ := reader.ReadString('\n'); strings.TrimSpace(strings.ToLower(line)) == "n" {
		fmt.Println("Export cancelled.")
		return opts, false
	}
	return opts, true
}

func availableTemplates(cfg *config.Config) []string {
	entries, err := os.ReadDir(cfg.Path("templates"))
	if err != nil {
		return nil
	}
	seen := map[string]bool{}
	var out []string
	for _, e := range entries {
		if e.IsDir() || filepath.Ext(e.Name()) != ".gohtml" {
			continue
		}
		name := strings.TrimSuffix(e.Name(), ".gohtml")
		if name == "index" || name == "article" || name == "layout" {
			continue
		}
		if !seen[name] {
			seen[name] = true
			out = append(out, name)
		}
	}
	sort.Strings(out)
	return out
}

// RunExport generates CREATE.csv from exportable rows, then exports HTML in
// chunks of exactly splitSize (the last chunk holds the remainder). Prints
// the export summary line when done.
func RunExport(e *Env, opts ExportOptions) error {
	n, err := doExport(e, opts)
	if err != nil {
		return err
	}
	if n > 0 {
		fmt.Println(report.Folders(n))
	}
	return nil
}

// doExport runs the export without a summary and returns the number of export
// folders.
func doExport(e *Env, opts ExportOptions) (int, error) {
	cfg := e.Cfg
	if opts.SplitSize > 0 {
		cfg.SplitSize = opts.SplitSize
	}
	if opts.Template != "" {
		cfg.Template = opts.Template
	}
	if opts.Workers > 0 {
		cfg.WorkerCount = opts.Workers
	}

	mappings, err := csvgen.Generate(cfg, cfg.SplitSize)
	if err != nil {
		return 0, err
	}
	if len(mappings) == 0 {
		log.Println("No exportable articles, nothing to export.")
		return 0, nil
	}
	return len(mappings), exportRun(e, mappings)
}

func exportRun(e *Env, mappings []csvgen.Mapping) error {
	cfg := e.Cfg
	log.Printf("Found %d bucket mappings in CREATE.csv", len(mappings))

	articleChan := make(chan export.Article, cfg.Memory.BatchSize)
	errChan := make(chan error, 1)
	go export.StreamArticles(cfg, articleChan, errChan)

	splitSize := cfg.SplitSize
	if splitSize <= 0 {
		splitSize = 5000
	}

	chunk := make([]export.Article, 0, splitSize)
	total := 0
	mappingIdx := 0

	flush := func() error {
		if len(chunk) == 0 || mappingIdx >= len(mappings) {
			return nil
		}
		m := mappings[mappingIdx]
		log.Printf("[%s] Bucket: %s | Name: %s | Articles: %d", m.Foldering, m.Bucket, m.Name, len(chunk))
		exp, err := export.New(cfg, m.Bucket, m.Foldering, m.Name)
		if err != nil {
			return err
		}
		if err := exp.Run(chunk); err != nil {
			return err
		}
		chunk = chunk[:0]
		mappingIdx++
		return nil
	}

	for article := range articleChan {
		total++
		chunk = append(chunk, article)
		if len(chunk) >= splitSize {
			if err := flush(); err != nil {
				return err
			}
		}
	}
	if err := <-errChan; err != nil {
		return err
	}
	if err := flush(); err != nil {
		return err
	}
	for ; mappingIdx < len(mappings); mappingIdx++ {
		log.Printf("[%s] SKIPPED - no articles left", mappings[mappingIdx].Foldering)
	}
	log.Printf("Total articles streamed: %d", total)
	return nil
}

// DeployStats summarizes the upload stage for the summary block.
type DeployStats struct {
	Folders              int
	UploadOK, UploadFail int
}

// SubmitStats summarizes the IndexNow stage for the summary block.
type SubmitStats struct {
	Folders              int
	SubmitOK, SubmitFail int
}

// Deploy runs bucket creation + upload from CREATE.csv, merges the results
// into report/deploy-submit.csv and prints the deploy summary block.
func Deploy(cfg *config.Config) error {
	dep, err := doDeploy(cfg)
	if err != nil {
		return err
	}
	printDeploySummary(dep.Folders, &dep, nil)
	return nil
}

// Submit runs IndexNow submission from CREATE.csv, merges the results into
// report/deploy-submit.csv and prints the submit summary line.
func Submit(cfg *config.Config) error {
	sub, err := doSubmit(cfg)
	if err != nil {
		return err
	}
	fmt.Println(report.Outcome("Submit", sub.SubmitOK, sub.SubmitFail))
	return nil
}

// ExportAll runs export + deploy + submit (menu 12) and prints one combined
// deploy summary block.
func ExportAll(e *Env, opts ExportOptions) error {
	cfg := e.Cfg
	folders, err := doExport(e, opts)
	if err != nil {
		return err
	}
	dep, err := doDeploy(cfg)
	if err != nil {
		return err
	}
	sub, err := doSubmit(cfg)
	if err != nil {
		return err
	}
	printDeploySummary(folders, &dep, &sub)
	return nil
}

// doDeploy uploads every export folder without printing a summary.
func doDeploy(cfg *config.Config) (DeployStats, error) {
	results, err := deploy.Run(cfg, "us-central1", 3, "CREATE.csv")
	if err != nil {
		return DeployStats{}, err
	}
	stats := DeployStats{Folders: len(results)}
	rows := make([]report.FolderRow, 0, len(results))
	for _, r := range results {
		if r.Status == "SUCCESS" {
			stats.UploadOK++
		} else {
			stats.UploadFail++
		}
		rows = append(rows, report.FolderRow{
			Bucket:       r.Bucket,
			Foldering:    r.Foldering,
			Name:         r.Name,
			Source:       r.Source,
			Files:        r.Files,
			URL:          r.URL,
			Create:       r.Create,
			Public:       r.Public,
			Upload:       r.Upload,
			DeployStatus: r.Status,
			DeployError:  r.Error,
			Duration:     r.Duration,
		})
	}
	writeDeploySubmitCSV(cfg, report.StageDeploy, rows)
	return stats, nil
}

// doSubmit submits every bucket to IndexNow without printing a summary.
func doSubmit(cfg *config.Config) (SubmitStats, error) {
	results, err := submit.Run(cfg, "CREATE.csv")
	if err != nil {
		return SubmitStats{}, err
	}
	stats := SubmitStats{Folders: len(results)}
	rows := make([]report.FolderRow, 0, len(results))
	for _, r := range results {
		if r.Submit == "SUCCESS" {
			stats.SubmitOK++
		} else {
			stats.SubmitFail++
		}
		rows = append(rows, report.FolderRow{
			Bucket:       r.Bucket,
			Foldering:    r.Foldering,
			Source:       r.Source,
			SubmitStatus: r.Submit,
			URLCount:     r.URLCount,
			SubmitNotes:  r.Notes,
		})
	}
	writeDeploySubmitCSV(cfg, report.StageSubmit, rows)
	return stats, nil
}

// writeDeploySubmitCSV merges one stage into the combined deploy/submit
// report. A write failure is logged but never fails the run itself.
func writeDeploySubmitCSV(cfg *config.Config, stage report.Stage, rows []report.FolderRow) {
	path := cfg.Path("report", "deploy-submit.csv")
	if err := report.MergeCSV(path, stage, rows); err != nil {
		log.Printf("report write failed for %s: %v", path, err)
		return
	}
	log.Printf("Report written to %s", path)
}

// printDeploySummary prints the deploy block of the summary:
//
//	Export : 50 Folder
//	Upload : SUCCESS 49, FAIL 1
//	Submit : SUCCESS 47, FAIL 3
//
// The Upload line appears when the deploy stage ran, the Submit line when the
// IndexNow stage ran.
func printDeploySummary(folders int, dep *DeployStats, sub *SubmitStats) {
	fmt.Println(report.Folders(folders))
	if dep != nil {
		fmt.Println(report.Outcome("Upload", dep.UploadOK, dep.UploadFail))
	}
	if sub != nil {
		fmt.Println(report.Outcome("Submit", sub.SubmitOK, sub.SubmitFail))
	}
}

// FullAutoSnippet: convert + scrape images/snippets + export + deploy + submit.
// Split size and template come from config (no prompts). Prints one combined
// summary block (scrape counters, then export/upload/submit).
func FullAutoSnippet(ctx context.Context, e *Env) error {
	cfg := e.Cfg
	log.Println("== Step 0/5: Convert Keywords ==")
	if err := pipeline.Convert(cfg); err != nil {
		return err
	}
	log.Println("== Step 1/5: Scrape Images + Descriptions ==")
	if err := scrapePhases(ctx, e); err != nil {
		return err
	}
	log.Println("== Step 2/5: Export (CREATE.csv + HTML) ==")
	folders, err := doExport(e, ExportOptions{SplitSize: cfg.SplitSize, Template: cfg.Template, Workers: cfg.WorkerCount})
	if err != nil {
		return err
	}
	log.Println("== Step 3/5: Create & Upload to GCS ==")
	dep, err := doDeploy(cfg)
	if err != nil {
		return err
	}
	log.Println("== Step 4/5: Submit to IndexNow ==")
	sub, err := doSubmit(cfg)
	if err != nil {
		return err
	}
	printScrape(cfg, report.Want{Images: true, Description: true, RelatedKW: true})
	fmt.Println(report.Separator)
	printDeploySummary(folders, &dep, &sub)
	log.Println(e.Metrics.Snapshot())
	log.Println("FULL AUTO SNIPPET completed!")
	return nil
}

// FullAutoAI: convert + scrape + AI titles + AI articles + export + deploy +
// submit. Split size and template come from config (no prompts). Prints one
// combined summary block (scrape + AI counters, then export/upload/submit).
func FullAutoAI(ctx context.Context, e *Env) error {
	cfg := e.Cfg
	log.Println("== Step 0/6: Convert Keywords ==")
	if err := pipeline.Convert(cfg); err != nil {
		return err
	}
	log.Println("== Step 1/6: Scrape Images + Descriptions ==")
	if err := scrapePhases(ctx, e); err != nil {
		return err
	}
	log.Println("== Step 2/6: Generate AI Titles ==")
	if err := pipeline.RunAI(ctx, e.PipelineEnv(), "title"); err != nil {
		return err
	}
	log.Println("== Step 3/6: Generate AI Articles ==")
	if err := pipeline.RunAI(ctx, e.PipelineEnv(), "article"); err != nil {
		return err
	}
	log.Println("== Step 4/6: Export (CREATE.csv + HTML) ==")
	folders, err := doExport(e, ExportOptions{SplitSize: cfg.SplitSize, Template: cfg.Template, Workers: cfg.WorkerCount})
	if err != nil {
		return err
	}
	log.Println("== Step 5/6: Create & Upload to GCS ==")
	dep, err := doDeploy(cfg)
	if err != nil {
		return err
	}
	log.Println("== Step 6/6: Submit to IndexNow ==")
	sub, err := doSubmit(cfg)
	if err != nil {
		return err
	}
	printScrape(cfg, report.Want{Images: true, Description: true, RelatedKW: true, AITitle: true, AIArticles: true})
	fmt.Println(report.Separator)
	printDeploySummary(folders, &dep, &sub)
	log.Println(e.Metrics.Snapshot())
	log.Println("FULL AUTO AI completed!")
	return nil
}

// Report writes the scrape progress report (parity with generate_scrape_report).
func Report(cfg *config.Config) error {
	files, err := db.ListDBFiles(cfg.Path(cfg.DataFolder))
	if err != nil {
		return err
	}
	reportDir := cfg.Path("report")
	os.MkdirAll(reportDir, 0755)
	for _, f := range files {
		conn, err := db.Open(f)
		if err != nil {
			continue
		}
		var images, titles, articles int64
		conn.QueryRow(`SELECT COUNT(*) FROM posts WHERE images IS NOT NULL AND images != '' AND images != '[]'`).Scan(&images)
		conn.QueryRow(`SELECT COUNT(*) FROM posts WHERE ai_title IS NOT NULL AND ai_title != ''`).Scan(&titles)
		conn.QueryRow(`SELECT COUNT(*) FROM posts WHERE ai_content IS NOT NULL AND ai_content != ''`).Scan(&articles)
		db.Close(conn)
		name := filepath.Base(f)
		log.Printf("%s: images=%d titles=%d articles=%d", name, images, titles, articles)
	}
	printScrape(cfg, report.Want{Images: true, Description: true, RelatedKW: true, AITitle: true, AIArticles: true})
	return nil
}
