package usecase

import (
	"context"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strings"
	"time"
	"unicode/utf8"

	"apps/backend/internal/module/studio/domain"
)

// PersonaPreview：依指紋一次 LLM 產主貼 + 回文（可抓新聞當話題靈感）。
func (s *Service) PersonaPreview(ctx context.Context, ownerUID int64, personaID, topic string, useNews bool) (_ *domain.PersonaPreview, err error) {
	if ownerUID <= 0 {
		return nil, domain.ErrForbidden
	}
	personaID = strings.TrimSpace(personaID)
	if personaID == "" {
		return nil, fmt.Errorf("%w: empty persona_id", domain.ErrValidation)
	}
	p, err := s.GetPersona(ctx, ownerUID, personaID)
	if err != nil {
		return nil, err
	}
	fp := strings.TrimSpace(p.Style.DraftText)
	if fp == "" {
		fp = serializeDraftText(p.Style.Draft)
	}
	if fp == "" && strings.TrimSpace(p.Style.Draft.Tone) == "" {
		return nil, fmt.Errorf("%w: 人設尚無指紋，請先完成分析或手動填寫指紋", domain.ErrValidation)
	}

	charge, err := s.billAI(ctx, ownerUID, "persona preview", "compose.personaPreview")
	if err != nil {
		return nil, err
	}
	defer charge.Settle(ctx, &err)

	topic = strings.TrimSpace(topic)
	topicSource := "manual"
	if topic == "" {
		if useNews {
			if t, ok := fetchNewsTopic(ctx); ok {
				topic = t
				topicSource = "news"
			}
		}
		if topic == "" {
			topic = pickFallbackTopic(p)
			topicSource = "fallback"
		}
	}

	fpBlock := buildFingerprintBlock(p, fp)
	prompt := buildPreviewBothPrompt(fpBlock, topic, topicSource)

	provider, model, apiKey, kerr := s.resolveUserAI(ctx, ownerUID)
	notes := ""
	var postText, replyText string

	if kerr == nil && strings.TrimSpace(apiKey) != "" && !isSyntheticAIKey(apiKey) {
		// 一次呼叫同時產主貼+回文，避免雙請求卡過 gateway 30s
		raw, aerr := s.completeLLM(ctx, provider, model, apiKey, prompt)
		if aerr != nil {
			notes = "產文失敗，已用指紋規則試產：" + truncate(aerr.Error(), 100)
			postText = rulePreviewPost(p, topic)
			replyText = rulePreviewReply(p, postText)
		} else {
			postText, replyText = parsePreviewPair(raw)
			if postText == "" {
				postText = rulePreviewPost(p, topic)
			}
			if replyText == "" {
				replyText = rulePreviewReply(p, postText)
			}
			if notes == "" && topicSource == "news" {
				notes = "話題取自即時新聞，已改寫成人設口吻"
			}
		}
		_ = model
		_ = provider
	} else {
		postText = rulePreviewPost(p, topic)
		replyText = rulePreviewReply(p, postText)
		notes = "尚未設定 API Key，目前用指紋規則試產。請到設定填寫 Key 後再試。"
	}

	return &domain.PersonaPreview{
		Topic:       topic,
		TopicSource: topicSource,
		PostText:    postText,
		ReplyText:   replyText,
		Notes:       notes,
	}, nil
}

func buildFingerprintBlock(p *domain.Persona, fp string) string {
	var b strings.Builder
	if n := strings.TrimSpace(p.Name); n != "" {
		b.WriteString("名稱：")
		b.WriteString(n)
		b.WriteString("\n")
	}
	if br := strings.TrimSpace(p.Brief); br != "" {
		b.WriteString("定位：")
		b.WriteString(br)
		b.WriteString("\n")
	}
	b.WriteString("【語言指紋】\n")
	b.WriteString(fp)
	if len(p.Style.Dimensions) > 0 {
		b.WriteString("\n【8D 摘要】\n")
		for _, k := range dimKeys {
			if d, ok := p.Style.Dimensions[k]; ok && strings.TrimSpace(d.Summary) != "" {
				b.WriteString(k)
				b.WriteString("：")
				b.WriteString(d.Summary)
				b.WriteString("\n")
			}
		}
	}
	if len(p.Guard.Avoid) > 0 {
		b.WriteString("【禁止】")
		b.WriteString(strings.Join(p.Guard.Avoid, "、"))
		b.WriteString("\n")
	}
	return b.String()
}

func buildPreviewBothPrompt(fpBlock, topic, topicSource string) string {
	srcHint := "自訂話題"
	if topicSource == "news" {
		srcHint = "今日新聞標題（請轉成個人觀察／情緒／提問，不要硬抄標題、不要寫成新聞稿）"
	} else if topicSource == "fallback" {
		srcHint = "生活靈感"
	}
	return strings.TrimSpace(fmt.Sprintf(`
你正在扮演一位 Threads 使用者本人（不是分析師、不是助手）。
任務：依語言指紋寫「一則主貼」與「一則短回文」。

規則（強制）：
- 只輸出一個 JSON 物件，不要 markdown 代碼塊、不要前言。
- 繁體中文（台灣用語）、口語、像真人滑手機打的。
- 嚴格遵守指紋的語氣／節奏／用字／CTA／禁忌。
- 主貼依內容需要自然展開，把觀點與情緒完整講完，不設固定字數；回文約 20～100 字。
- 話題僅作靈感，必須用「這個人會怎麼講」重寫。

【話題靈感】（來源：%s）
%s

%s

輸出格式：
{"post":"主貼正文","reply":"回文正文"}
`, srcHint, topic, fpBlock))
}

// parsePreviewPair 解析 {"post","reply"} 或寬鬆切割。
func parsePreviewPair(raw string) (post, reply string) {
	s := strings.TrimSpace(raw)
	if s == "" {
		return "", ""
	}
	// strip fences
	if i := strings.Index(s, "{"); i >= 0 {
		if j := strings.LastIndex(s, "}"); j > i {
			s = s[i : j+1]
		}
	}
	var parsed struct {
		Post  string `json:"post"`
		Reply string `json:"reply"`
		// 別名
		PostText  string `json:"post_text"`
		ReplyText string `json:"reply_text"`
	}
	if err := json.Unmarshal([]byte(s), &parsed); err == nil {
		post = cleanGeneratedPostText(coalesceStr(parsed.Post, parsed.PostText))
		reply = cleanGeneratedText(coalesceStr(parsed.Reply, parsed.ReplyText))
		return post, reply
	}
	// 非 JSON：整段當主貼
	return cleanGeneratedPostText(raw), ""
}

func coalesceStr(a, b string) string {
	if strings.TrimSpace(a) != "" {
		return a
	}
	return b
}

func cleanGeneratedText(raw string) string {
	return cleanGeneratedTextMax(raw, 500)
}

func cleanGeneratedPostText(raw string) string {
	return cleanGeneratedTextMax(raw, 0)
}

func cleanGeneratedTextMax(raw string, maxRunes int) string {
	s := strings.TrimSpace(raw)
	if s == "" {
		return ""
	}
	// 去掉常見 markdown 圍欄
	if strings.HasPrefix(s, "```") {
		s = strings.TrimPrefix(s, "```json")
		s = strings.TrimPrefix(s, "```")
		s = strings.TrimSpace(s)
		if i := strings.LastIndex(s, "```"); i >= 0 {
			s = strings.TrimSpace(s[:i])
		}
	}
	s = strings.TrimSpace(s)
	for _, p := range []string{"主貼：", "主贴：", "貼文：", "仿寫：", "回覆：", "回文：", "Reply:", "Post:", "Mimic:"} {
		s = strings.TrimPrefix(s, p)
		s = strings.TrimSpace(s)
	}
	if maxRunes > 0 && utf8.RuneCountInString(s) > maxRunes {
		r := []rune(s)
		s = string(r[:maxRunes]) + "…"
	}
	return s
}

func rulePreviewPost(p *domain.Persona, topic string) string {
	d := p.Style.Draft
	hooks := d.Hooks
	if hooks == "" {
		hooks = "先丟自己的卡關"
	}
	cta := d.CtaStyle
	if cta == "" {
		cta = "你們呢？"
	}
	ex := d.Examples
	lines := []string{
		fmt.Sprintf("最近卡在「%s」。", truncate(topic, 40)),
		"",
		hooks + "——講真的，不是規格文那種。",
	}
	if ex != "" {
		lines = append(lines, truncate(ex, 80))
	}
	lines = append(lines, "", cta)
	return strings.Join(lines, "\n")
}

func rulePreviewReply(p *domain.Persona, postText string) string {
	_ = p
	return fmt.Sprintf("懂，我自己也遇過類似的。%s——你們後來怎麼解的？", truncate(postText, 24))
}

func pickFallbackTopic(p *domain.Persona) string {
	seeds := []string{
		"最近大家在煩的時間管理",
		"網購踩雷又不能退的感覺",
		"加班後只想滑手機不想動",
		"突然很想整理房間但動不了",
		"朋友安利的東西到底能不能信",
		"週末計劃很多結果什麼都沒做",
		"天氣一變心情就跟著飄",
		"想學新東西但資訊量爆炸",
	}
	if a := strings.TrimSpace(p.Style.Draft.Audience); a != "" {
		seeds = append(seeds, "跟「"+truncate(a, 20)+"」有關的日常卡關")
	}
	return seeds[rand.Intn(len(seeds))]
}

// fetchNewsTopic — Google News 繁中 RSS（短超時，失敗就走 fallback）
func fetchNewsTopic(ctx context.Context) (string, bool) {
	urls := []string{
		"https://news.google.com/rss?hl=zh-TW&gl=TW&ceid=TW:zh-Hant",
		"https://news.google.com/rss/headlines/section/topic/TECHNOLOGY?hl=zh-TW&gl=TW&ceid=TW:zh-Hant",
	}
	u := urls[rand.Intn(len(urls))]
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return "", false
	}
	req.Header.Set("User-Agent", "HarborDesk/1.0 (+persona-preview)")
	client := &http.Client{Timeout: 3 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		return "", false
	}
	defer res.Body.Close()
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return "", false
	}
	body, err := io.ReadAll(io.LimitReader(res.Body, 512<<10))
	if err != nil || len(body) == 0 {
		return "", false
	}
	titles := parseRSSTitles(body)
	if len(titles) == 0 {
		return "", false
	}
	t := cleanNewsTitle(titles[rand.Intn(len(titles))])
	if t == "" {
		return "", false
	}
	return t, true
}

type rssFeed struct {
	XMLName xml.Name  `xml:"rss"`
	Items   []rssItem `xml:"channel>item"`
}

type rssItem struct {
	Title string `xml:"title"`
}

func parseRSSTitles(body []byte) []string {
	var feed rssFeed
	if err := xml.Unmarshal(body, &feed); err == nil {
		var out []string
		for _, it := range feed.Items {
			if t := strings.TrimSpace(it.Title); t != "" {
				out = append(out, t)
			}
		}
		if len(out) > 0 {
			return out
		}
	}
	raw := string(body)
	var out []string
	for {
		i := strings.Index(raw, "<title>")
		if i < 0 {
			break
		}
		raw = raw[i+7:]
		j := strings.Index(raw, "</title>")
		if j < 0 {
			break
		}
		t := strings.TrimSpace(raw[:j])
		raw = raw[j+8:]
		if strings.Contains(strings.ToLower(t), "google news") {
			continue
		}
		if t != "" {
			out = append(out, t)
		}
		if len(out) >= 15 {
			break
		}
	}
	return out
}

func cleanNewsTitle(t string) string {
	t = strings.TrimSpace(t)
	if i := strings.LastIndex(t, " - "); i > 8 {
		t = strings.TrimSpace(t[:i])
	}
	t = strings.TrimPrefix(t, "<![CDATA[")
	t = strings.TrimSuffix(t, "]]>")
	t = strings.TrimSpace(t)
	if utf8.RuneCountInString(t) < 6 {
		return ""
	}
	if utf8.RuneCountInString(t) > 80 {
		r := []rune(t)
		t = string(r[:80]) + "…"
	}
	return t
}
