package handlers

import (
	"errors"
	"fmt"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"log"
	"net/url"
	"sort"
	"strings"
	"time"

	"wa-assistant/backend/database"
	"wa-assistant/backend/models"
	"wa-assistant/backend/services"

	"github.com/gin-gonic/gin"
)

// crawlLimitsForTenant — tidak ada batas untuk instalasi internal.
func crawlLimitsForTenant(tenantID uint) (maxChars, maxPages int) {
	return 0, 0 // unlimited
}

func knowledgeCharsUsed(agentID uint) int64 {
	var used int64
	database.DB.Model(&models.Knowledge{}).Where("agent_id = ?", agentID).
		Select("COALESCE(SUM(char_count),0)").Scan(&used)
	return used
}

// StartCrawl memulai crawl website untuk satu agent (nomor) sebagai background job.
func StartCrawl(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	var req struct {
		URL string `json:"url"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.URL) == "" {
		c.JSON(400, gin.H{"error": "URL website wajib diisi"})
		return
	}
	raw := strings.TrimSpace(req.URL)
	if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
		raw = "https://" + raw
	}
	if u, err := url.Parse(raw); err != nil || u.Host == "" {
		c.JSON(400, gin.H{"error": "URL tidak valid"})
		return
	}

	_, maxPages := crawlLimitsForTenant(currentTenantID(c))
	job := models.CrawlJob{AgentID: aid, RootURL: raw, Status: "pending"}
	err := database.DB.Transaction(func(tx *gorm.DB) error {
		var agent models.Agent
		if err := tx.Select("id").Clauses(clause.Locking{Strength: "UPDATE"}).First(&agent, aid).Error; err != nil {
			return err
		}
		var running int64
		if err := tx.Model(&models.CrawlJob{}).Where("agent_id = ? AND status IN ?", aid, []string{"pending", "crawling", "training", "stopping"}).Count(&running).Error; err != nil {
			return err
		}
		if running > 0 {
			return errCrawlBusy
		}
		return tx.Create(&job).Error
	})
	if errors.Is(err, errCrawlBusy) {
		c.JSON(409, gin.H{"error": err.Error()})
		return
	}
	if err != nil {
		c.JSON(500, gin.H{"error": "Gagal membuat proses pengambilan website"})
		return
	}
	services.Go("RunCrawl", func() { services.RunCrawl(job.ID, maxPages) })
	c.JSON(201, gin.H{"data": job, "max_pages": maxPages})
}

// CrawlStatus mengembalikan satu job + daftar halamannya (untuk polling UI).
func CrawlStatus(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	var job models.CrawlJob
	if database.DB.Where("agent_id = ?", aid).First(&job, c.Param("jobId")).Error != nil {
		c.JSON(404, gin.H{"error": "Job tidak ditemukan"})
		return
	}
	c.JSON(200, gin.H{"job": withCrawlDraftCount(job), "pages": crawlPagesOf(job.ID)})
}

// LatestCrawl mengembalikan job crawl terakhir agent (agar UI bisa lanjut polling setelah refresh).
func LatestCrawl(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	var job models.CrawlJob
	if database.DB.Where("agent_id = ?", aid).Order("id desc").First(&job).Error != nil {
		c.JSON(200, gin.H{"job": nil})
		return
	}
	c.JSON(200, gin.H{"job": withCrawlDraftCount(job), "pages": crawlPagesOf(job.ID)})
}

func crawlPagesOf(jobID uint) []models.CrawlPage {
	var pages []models.CrawlPage
	// Rekomendasi skor tinggi dulu agar UI menampilkan halaman terbaik di atas.
	database.DB.Where("job_id = ?", jobID).
		Order("recommend_score desc, recommended desc, char_count desc, id asc").
		Find(&pages)
	for i := range pages {
		page := &pages[i]
		page.ContentIssue = services.CrawlContentIssue(page.Content)
		page.CanTrain = (page.Status == "crawled" || page.Status == "failed") && page.ContentIssue == ""
		if page.ContentIssue != "" {
			page.Recommended = false
		}
		preview := []rune(page.Content)
		if len(preview) > 320 {
			page.ContentPreview = string(preview[:320]) + "…"
		} else {
			page.ContentPreview = string(preview)
		}
	}
	return pages
}

// TrainCrawlPages memulai pelatihan halaman terpilih menjadi FAQ (background job).
// Tiap halaman diubah AI jadi Q&A bersih lalu di-embed. Status job -> "training"; UI polling.
func TrainCrawlPages(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	var req struct {
		PageIDs       []uint `json:"page_ids"`
		UpdatePersona *bool  `json:"update_persona"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || len(req.PageIDs) == 0 {
		c.JSON(400, gin.H{"error": "Pilih minimal satu halaman"})
		return
	}
	var job models.CrawlJob
	if database.DB.Where("agent_id = ?", aid).First(&job, c.Param("jobId")).Error != nil {
		c.JSON(404, gin.H{"error": "Job tidak ditemukan"})
		return
	}
	if err := claimCrawlTraining(aid, job.ID, req.PageIDs); err != nil {
		switch {
		case errors.Is(err, errCrawlBusy):
			c.JSON(409, gin.H{"error": err.Error()})
		case errors.Is(err, errCrawlPages):
			c.JSON(400, gin.H{"error": err.Error()})
		default:
			c.JSON(500, gin.H{"error": "Penyusunan belum bisa dimulai"})
		}
		return
	}
	maxChars, _ := crawlLimitsForTenant(currentTenantID(c))
	updatePersona := req.UpdatePersona != nil && *req.UpdatePersona
	services.Go("runWebTraining", func() { runWebTraining(aid, job.ID, req.PageIDs, maxChars, updatePersona) })
	c.JSON(202, gin.H{"started": true})
}

// runWebTraining (background) mengubah tiap halaman terpilih menjadi FAQ Q&A via AI lalu menyimpannya
// sebagai knowledge. Hormati kuota karakter; halaman tanpa info berguna dilewati (bukan sampah masuk KB).
func runWebTraining(agentID, jobID uint, pageIDs []uint, maxChars int, updatePersona bool) {
	runWebTrainingUsing(agentID, jobID, pageIDs, maxChars, updatePersona, services.GenerateWebFAQ)
}

func runWebTrainingUsing(agentID, jobID uint, pageIDs []uint, maxChars int, updatePersona bool, generate func(string, string) ([]services.QAPair, error)) {
	terminalError := ""
	defer func() {
		if r := recover(); r != nil {
			log.Printf("[train] PANIC agent %d: %v", agentID, r)
			finishCrawlTraining(jobID, "failed", "Penyusunan terhenti karena kesalahan internal")
			return
		}
		status := "done"
		if terminalError != "" {
			status = "failed"
		}
		if err := finishCrawlTraining(jobID, status, terminalError); err != nil {
			log.Printf("[train] gagal menyelesaikan status job %d: %v", jobID, err)
		}
	}()

	used := knowledgeCharsUsed(agentID)
	var pages []models.CrawlPage
	if err := database.DB.Where("agent_id = ? AND job_id = ? AND id IN ? AND status = ?", agentID, jobID, pageIDs, "queued").Order("id ASC").Find(&pages).Error; err != nil {
		terminalError = "Daftar halaman belum bisa dibaca. Coba ulang proses penyusunan."
		return
	}
	writer := newKnowledgeUpserter(agentID)
	log.Printf("[train] mulai agent %d job %d: %d halaman dipilih", agentID, jobID, len(pages))

	trainedN, skippedN, failedN, totalFAQ := 0, 0, 0, 0
	stopped := false
	for i := range pages {
		// Hormati permintaan Stop dari user: berhenti rapi antar-halaman.
		// Halaman yang sudah jadi FAQ tetap tersimpan; sisanya bisa dilatih lagi nanti.
		if jobStatusIs(jobID, "stopping") {
			log.Printf("[train] dihentikan user pada halaman %d/%d (agent %d)", i, len(pages), agentID)
			stopped = true
			break
		}
		p := pages[i]
		writer.excerpt = p.Content
		if issue := services.CrawlContentIssue(p.Content); issue != "" {
			setPageStatus(p.ID, "failed", issue)
			failedN++
			continue
		}
		if maxChars > 0 && used >= int64(maxChars) {
			setPageStatus(p.ID, "failed", "kuota knowledge penuh")
			failedN++
			continue
		}
		setPageStatus(p.ID, "training", "")

		faqs, err := generate(p.Title, p.Content)
		if err != nil {
			errMsg := webTrainingErrorMessage(err)
			setPageStatus(p.ID, "failed", errMsg)
			failedN++
			log.Printf("[train] FAQ gagal page %d (%s): %v", p.ID, p.URL, err)
			continue
		}
		if len(faqs) == 0 {
			setPageStatus(p.ID, "skipped", "tidak ada info berguna untuk pelanggan")
			skippedN++
			log.Printf("[train] page %d (%s) -> dilewati (tak ada info berguna)", p.ID, p.URL)
			continue
		}

		added := 0
		saveFailure := ""
		knownIDs := map[uint]bool{}
		for _, existing := range writer.rows {
			knownIDs[existing.ID] = true
		}
		for _, f := range faqs {
			ans := strings.TrimSpace(f.Answer)
			if ans == "" {
				continue
			}
			if maxChars > 0 && used+int64(len([]rune(ans))) > int64(maxChars) {
				saveFailure = "Penyimpanan pengetahuan penuh. Sebagian draf belum tersimpan."
				break
			}
			writer.excerpt = f.Evidence
			if writer.excerpt == "" {
				writer.excerpt = p.Content
			}
			stored, covered, saveErr := writer.save(f.Question, ans, f.Tags, "web", p.URL)
			if saveErr != nil {
				saveFailure = "Sebagian draf belum bisa disimpan. Coba ulang halaman ini."
				log.Printf("[train] gagal simpan FAQ page %d: %v", p.ID, saveErr)
				continue
			}
			if !covered {
				continue
			}
			if !knownIDs[stored.ID] {
				used += int64(stored.CharCount)
				knownIDs[stored.ID] = true
				added++
			}
		}
		now := time.Now()
		st := "trained"
		errMsg := ""
		if saveFailure != "" {
			st, errMsg = "failed", saveFailure
			failedN++
		} else {
			trainedN++
			totalFAQ += added
		}
		log.Printf("[train] page %d (%s) -> %s (%d FAQ)", p.ID, p.URL, st, added)
		database.DB.Model(&models.CrawlPage{}).Where("id = ?", p.ID).
			Updates(map[string]any{"status": st, "error": errMsg, "trained_at": &now})
	}
	services.InvalidateKB(agentID)
	if trainedN == 0 && failedN > 0 {
		terminalError = "Penyusunan belum berhasil. Periksa alasan pada halaman yang gagal, lalu coba ulang."
	}
	log.Printf("[train] SELESAI agent %d job %d: %d dilatih (%d FAQ), %d dilewati, %d gagal", agentID, jobID, trainedN, totalFAQ, skippedN, failedN)
	// Legacy clients may still request this option. Preparing drafts must not
	// publish their contents indirectly through the live persona.
	if !stopped && trainedN > 0 && updatePersona {
		database.DB.Model(&models.CrawlJob{}).Where("id = ?", jobID).Updates(map[string]any{"persona_updated": false, "persona_error": "Terbitkan bahan yang sudah ditinjau, lalu susun saran persona melalui Persona."})
	}
}

func webTrainingErrorMessage(err error) string {
	if err == nil {
		return ""
	}
	msg := strings.ToLower(err.Error())
	switch {
	case errors.Is(err, services.ErrKnowledgeSourceTooLarge):
		return services.ErrKnowledgeSourceTooLarge.Error()
	case errors.Is(err, services.ErrKnowledgeValidation):
		return services.ErrKnowledgeValidation.Error()
	case strings.Contains(msg, "provider_credit_exhausted") || strings.Contains(msg, "402"):
		return "Saldo OpenRouter tidak cukup untuk menyusun draf. Isi saldo, lalu coba ulang halaman yang gagal."
	case strings.Contains(msg, "api key") || strings.Contains(msg, "unauthorized") || strings.Contains(msg, "401"):
		return "API key AI belum diisi atau tidak valid"
	case strings.Contains(msg, "429") || strings.Contains(msg, "rate limit") || strings.Contains(msg, "quota") || strings.Contains(msg, "credit"):
		return "Kuota/kredit provider AI habis atau sedang dibatasi"
	case strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline"):
		return "Provider AI terlalu lama merespons"
	default:
		return "AI gagal mengolah konten halaman. Periksa konfigurasi provider lalu coba lagi"
	}
}

func setPageStatus(pageID uint, status, errMsg string) {
	database.DB.Model(&models.CrawlPage{}).Where("id = ?", pageID).
		Updates(map[string]any{"status": status, "error": errMsg})
}

// jobStatusIs membaca status job terkini dari DB (dipakai untuk mendeteksi permintaan Stop saat training).
func jobStatusIs(jobID uint, status string) bool {
	var s string
	database.DB.Model(&models.CrawlJob{}).Where("id = ?", jobID).Select("status").Scan(&s)
	return s == status
}

// StopTraining menandai job pelatihan agar berhenti rapi pada halaman berikutnya.
func StopTraining(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	var job models.CrawlJob
	if database.DB.Where("agent_id = ?", aid).First(&job, c.Param("jobId")).Error != nil {
		c.JSON(404, gin.H{"error": "Job tidak ditemukan"})
		return
	}
	if job.Status != "training" {
		c.JSON(409, gin.H{"error": "Tidak ada pelatihan yang sedang berjalan"})
		return
	}
	result := database.DB.Model(&job).Where("status = ?", "training").Update("status", "stopping")
	if result.Error != nil {
		c.JSON(500, gin.H{"error": "Belum bisa menghentikan proses"})
		return
	}
	if result.RowsAffected != 1 {
		c.JSON(409, gin.H{"error": "Proses sudah selesai"})
		return
	}
	c.JSON(200, gin.H{"stopping": true})
}

// KnowledgeUsage menampilkan pemakaian kuota knowledge agent (untuk UI).
func KnowledgeUsage(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	maxChars, maxPages := crawlLimitsForTenant(currentTenantID(c))
	var total int64
	database.DB.Model(&models.Knowledge{}).Where("agent_id = ?", aid).Count(&total)
	index, err := services.KnowledgeIndexStatus(c.Request.Context(), aid)
	if err != nil {
		c.JSON(503, gin.H{"error": "Status pengetahuan belum bisa dimuat. Coba lagi."})
		return
	}
	c.JSON(200, gin.H{
		"used_chars": knowledgeCharsUsed(aid), "max_chars": maxChars,
		"max_pages": maxPages, "total_knowledge": total,
		"semantic_search": index.Configured && index.Embedded > 0, "embedded_knowledge": index.Embedded,
		"available_knowledge": index.Available, "embedding_configured": index.Configured, "search_status": index.Status, "indexing": index.Running, "index_error": index.Error,
	})
}

// personaGenerator = titik suntik untuk tes; produksi memakai services.GeneratePersona.
var personaGenerator = services.GeneratePersona

// knowledgePersonaSamples mengambil contoh dari bahan pengetahuan agen (bisa kosong).
func knowledgePersonaSamples(agentID uint) []string {
	var rows []models.Knowledge
	now := time.Now()
	if err := database.DB.Select("id", "question", "answer", "review_status", "active", "effective_from", "effective_until").
		Where("agent_id = ? AND review_status = ? AND active = ?", agentID, "published", true).
		Where("(effective_from IS NULL OR effective_from <= ?) AND (effective_until IS NULL OR effective_until > ?)", now, now).
		Order("verified_at DESC, id DESC").Limit(200).Find(&rows).Error; err != nil {
		return nil
	}
	return pickKnowledgeSamples(rows)
}

// personaSourceLabel memberi label asal bahan supaya model tahu konteks yang diberikan.
func personaSourceLabel(source string) string {
	switch source {
	case "website+knowledge":
		return "Konten website dan bahan pengetahuan bisnis (pasangan tanya jawab resmi):"
	case "website":
		return "Konten website:"
	default:
		return "Bahan pengetahuan bisnis (pasangan tanya jawab resmi):"
	}
}

// Only reviewed facts that apply now may shape the persona. Drafts and retired
// facts cannot bypass publication by entering the identity/style prompt.
func pickKnowledgeSamples(rows []models.Knowledge) []string {
	var urutan []models.Knowledge
	now := time.Now()
	for _, row := range rows {
		if row.ReviewStatus != "published" || !row.Active || row.Question == "" || row.Answer == "" ||
			(row.EffectiveFrom != nil && row.EffectiveFrom.After(now)) ||
			(row.EffectiveUntil != nil && !row.EffectiveUntil.After(now)) {
			continue
		}
		urutan = append(urutan, row)
	}
	sort.SliceStable(urutan, func(i, j int) bool {
		pi, pj := personaQuestionPriority(urutan[i].Question), personaQuestionPriority(urutan[j].Question)
		if pi != pj {
			return pi < pj
		}
		return len(urutan[i].Answer) > len(urutan[j].Answer)
	})
	var samples []string
	for _, row := range urutan {
		if len(samples) >= 3 {
			break
		}
		jawaban := row.Answer
		if r := []rune(jawaban); len(r) > 1200 {
			jawaban = string(r[:1200])
		}
		samples = append(samples, fmt.Sprintf("Tanya: %s\nJawab: %s", row.Question, jawaban))
	}
	return samples
}

// personaQuestionPriority menandai pertanyaan identitas (0) yang paling mewakili bisnis,
// lalu pertanyaan umum (1), dan sisanya (2).
func personaQuestionPriority(question string) int {
	q := strings.ToLower(question)
	for _, kata := range []string{"apa itu", "profil", "tentang", "siapa", "brand", "bisnis", "usaha", "layanan apa", "produk apa", "course apa"} {
		if strings.Contains(q, kata) {
			return 0
		}
	}
	for _, kata := range []string{"layanan", "produk", "harga", "cara", "fitur", "keunggulan"} {
		if strings.Contains(q, kata) {
			return 1
		}
	}
	return 2
}

// RegeneratePersona returns an editable suggestion based only on reviewed, current knowledge.
func RegeneratePersona(c *gin.Context) {
	aid, ok := resolveAgent(c)
	if !ok {
		return
	}
	// Generate persona pakai AI — kenakan ke kuota AI bulanan tenant.
	knowledge := knowledgePersonaSamples(aid)
	samples := knowledge
	if len(samples) == 0 {
		c.JSON(400, gin.H{"error": "Terbitkan pengetahuan yang sudah ditinjau terlebih dahulu, lalu susun persona."})
		return
	}
	source := "knowledge"
	persona, err := personaGenerator(personaSourceLabel(source), samples)
	if err != nil {
		log.Printf("[persona] gagal menyusun agent %d dari %s: %v", aid, source, err)
		c.JSON(502, gin.H{"error": "Gagal menyusun persona. Coba lagi."})
		return
	}
	if strings.TrimSpace(persona) == "" {
		c.JSON(502, gin.H{"error": "Persona belum terbentuk. Coba lagi sebentar lagi."})
		return
	}
	// Return a preview; the existing agent Save endpoint applies the operator's
	// reviewed text. Generating a suggestion must not change the active persona.
	c.JSON(200, gin.H{"system_prompt": persona, "source": source, "preview": true})
}

// pickPersonaSamples memilih maksimal 3 cuplikan konten paling relevan untuk persona (Home/About dulu).
func pickPersonaSamples(pages []models.CrawlPage) []string {
	var home, about, rest []models.CrawlPage
	for _, p := range pages {
		lu := strings.ToLower(p.URL)
		switch {
		case isHomeURL(p.URL):
			home = append(home, p)
		case strings.Contains(lu, "about") || strings.Contains(lu, "tentang") ||
			strings.Contains(lu, "profil") || strings.Contains(lu, "company") ||
			strings.Contains(lu, "perusahaan"):
			about = append(about, p)
		default:
			rest = append(rest, p)
		}
	}
	ordered := append(append(home, about...), rest...)

	var samples []string
	for _, p := range ordered {
		if len(samples) >= 3 {
			break
		}
		c := p.Content
		if len([]rune(c)) > 1500 {
			c = string([]rune(c)[:1500])
		}
		if strings.TrimSpace(c) != "" {
			samples = append(samples, p.Title+"\n"+c)
		}
	}
	return samples
}

// isHomeURL true bila URL menunjuk ke halaman beranda (tanpa path atau hanya "/").
func isHomeURL(raw string) bool {
	u, err := url.Parse(raw)
	if err != nil {
		return false
	}
	return u.Path == "" || u.Path == "/"
}
