package usecase

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/xml"
	"math"
	"path/filepath"
	"regexp"
	"strings"

	"haixun-backend/internal/library/clock"
	app "haixun-backend/internal/library/errors"
	"haixun-backend/internal/library/errors/code"
	"haixun-backend/internal/model/content_ops/domain/entity"
	domrepo "haixun-backend/internal/model/content_ops/domain/repository"
	domusecase "haixun-backend/internal/model/content_ops/domain/usecase"

	"github.com/google/uuid"
)

type useCase struct{ repo domrepo.Repository }

func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &useCase{repo: repo} }

func requireActor(tenantID, ownerUID, personaID string) error {
	if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" || strings.TrimSpace(personaID) == "" {
		return app.For(code.Persona).InputMissingRequired("tenant_id, owner_uid, and persona_id are required")
	}
	return nil
}

func clampScore(n int) int {
	if n < 0 {
		return 0
	}
	if n > 100 {
		return 100
	}
	return n
}

func finalScore(heat, fit, interaction, extend, risk int) float64 {
	score := float64(clampScore(heat))*0.25 + float64(clampScore(fit))*0.30 + float64(clampScore(interaction))*0.15 + float64(clampScore(extend))*0.15 + 70*0.10 - float64(clampScore(risk))*0.20
	return math.Round(score*10) / 10
}

func (u *useCase) CreateTopicCandidate(ctx context.Context, req domusecase.TopicCandidateInput) (*domusecase.TopicCandidateSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
		return nil, err
	}
	name := strings.TrimSpace(req.Name)
	if name == "" {
		return nil, app.For(code.Persona).InputMissingRequired("topic name is required")
	}
	now := clock.NowUnixNano()
	item := &entity.TopicCandidate{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, Name: name, Source: strings.TrimSpace(req.Source), Category: strings.TrimSpace(req.Category), SeedQuery: strings.TrimSpace(req.SeedQuery), TrendReason: strings.TrimSpace(req.TrendReason), TrendKeywords: req.TrendKeywords, HeatScore: clampScore(req.HeatScore), FitScore: clampScore(req.FitScore), InteractionScore: clampScore(req.InteractionScore), ExtendScore: clampScore(req.ExtendScore), RiskScore: clampScore(req.RiskScore), RecommendedMissions: req.RecommendedMissions, Status: "candidate", CreateAt: now, UpdateAt: now}
	item.FinalScore = finalScore(item.HeatScore, item.FitScore, item.InteractionScore, item.ExtendScore, item.RiskScore)
	if err := u.repo.CreateTopicCandidate(ctx, item); err != nil {
		return nil, err
	}
	out := topicSummary(*item)
	return &out, nil
}

func (u *useCase) ListTopicCandidates(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.TopicCandidateSummary, error) {
	items, err := u.repo.ListTopicCandidates(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	out := make([]domusecase.TopicCandidateSummary, 0, len(items))
	for _, item := range items {
		out = append(out, topicSummary(item))
	}
	return out, nil
}

func (u *useCase) CreateContentPlan(ctx context.Context, req domusecase.ContentPlanInput) (*domusecase.ContentPlanSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
		return nil, err
	}
	if strings.TrimSpace(req.Topic) == "" || strings.TrimSpace(req.Mission) == "" {
		return nil, app.For(code.Persona).InputMissingRequired("topic and mission are required")
	}
	now := clock.NowUnixNano()
	item := &entity.ContentPlan{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, TopicCandidateID: strings.TrimSpace(req.TopicCandidateID), Topic: strings.TrimSpace(req.Topic), Mission: strings.TrimSpace(req.Mission), TargetAudience: strings.TrimSpace(req.TargetAudience), Angle: strings.TrimSpace(req.Angle), OpeningType: strings.TrimSpace(req.OpeningType), BodyType: strings.TrimSpace(req.BodyType), Emotion: strings.TrimSpace(req.Emotion), EndingType: strings.TrimSpace(req.EndingType), CtaType: strings.TrimSpace(req.CtaType), RiskLevel: normalizeRisk(req.RiskLevel), RequiresHumanReview: req.RequiresHumanReview || normalizeRisk(req.RiskLevel) != "low", Avoid: req.Avoid, SelectedKnowledge: req.SelectedKnowledge, Status: "planned", CreateAt: now, UpdateAt: now}
	if err := u.repo.CreateContentPlan(ctx, item); err != nil {
		return nil, err
	}
	out := planSummary(*item)
	return &out, nil
}

func (u *useCase) UpdateContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string, patch domusecase.ContentPlanPatch) (*domusecase.ContentPlanSummary, error) {
	m := map[string]any{"update_at": clock.NowUnixNano()}
	setString := func(key string, value *string) {
		if value != nil {
			m[key] = strings.TrimSpace(*value)
		}
	}
	setString("topic", patch.Topic)
	setString("mission", patch.Mission)
	setString("target_audience", patch.TargetAudience)
	setString("angle", patch.Angle)
	setString("opening_type", patch.OpeningType)
	setString("body_type", patch.BodyType)
	setString("emotion", patch.Emotion)
	setString("ending_type", patch.EndingType)
	setString("cta_type", patch.CtaType)
	setString("status", patch.Status)
	if patch.RiskLevel != nil {
		m["risk_level"] = normalizeRisk(*patch.RiskLevel)
	}
	if patch.RequiresHumanReview != nil {
		m["requires_human_review"] = *patch.RequiresHumanReview
	}
	if patch.Avoid != nil {
		m["avoid"] = patch.Avoid
	}
	if patch.SelectedKnowledge != nil {
		m["selected_knowledge"] = patch.SelectedKnowledge
	}
	item, err := u.repo.UpdateContentPlan(ctx, tenantID, ownerUID, personaID, id, m)
	if err != nil {
		return nil, err
	}
	out := planSummary(*item)
	return &out, nil
}

func (u *useCase) GetContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string) (*domusecase.ContentPlanSummary, error) {
	item, err := u.repo.GetContentPlan(ctx, tenantID, ownerUID, personaID, id)
	if err != nil {
		return nil, err
	}
	out := planSummary(*item)
	return &out, nil
}

func (u *useCase) ListContentPlans(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.ContentPlanSummary, error) {
	items, err := u.repo.ListContentPlans(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	out := make([]domusecase.ContentPlanSummary, 0, len(items))
	for _, item := range items {
		out = append(out, planSummary(item))
	}
	return out, nil
}

func (u *useCase) CreateFeedbackEvent(ctx context.Context, req domusecase.FeedbackEventInput) (*domusecase.FeedbackEventSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
		return nil, err
	}
	decision := strings.TrimSpace(req.Decision)
	if decision == "" {
		return nil, app.For(code.Persona).InputMissingRequired("decision is required")
	}
	item := &entity.FeedbackEvent{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, ContentPlanID: strings.TrimSpace(req.ContentPlanID), DraftID: strings.TrimSpace(req.DraftID), Decision: decision, Note: strings.TrimSpace(req.Note), Snapshot: strings.TrimSpace(req.Snapshot), CreateAt: clock.NowUnixNano()}
	if err := u.repo.CreateFeedbackEvent(ctx, item); err != nil {
		return nil, err
	}
	out := feedbackSummary(*item)
	return &out, nil
}

func (u *useCase) ListFeedbackEvents(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.FeedbackEventSummary, error) {
	items, err := u.repo.ListFeedbackEvents(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	out := make([]domusecase.FeedbackEventSummary, 0, len(items))
	for _, item := range items {
		out = append(out, feedbackSummary(item))
	}
	return out, nil
}

func (u *useCase) CreateKnowledgeSource(ctx context.Context, req domusecase.KnowledgeSourceInput) (*domusecase.KnowledgeSourceSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
		return nil, err
	}
	raw, err := decodeSourceContent(req)
	if err != nil {
		return nil, app.For(code.Persona).InputInvalidFormat(err.Error())
	}
	chunksText := splitChunks(raw, 900)
	now := clock.NowUnixNano()
	sourceID := uuid.NewString()
	item := &entity.KnowledgeSource{ID: sourceID, TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, SourceType: strings.TrimSpace(req.SourceType), Title: strings.TrimSpace(req.Title), Filename: strings.TrimSpace(req.Filename), RawContent: raw, ParsedStatus: "parsed", ChunkCount: len(chunksText), CreateAt: now}
	chunks := make([]*entity.KnowledgeChunk, 0, len(chunksText))
	for _, text := range chunksText {
		chunks = append(chunks, &entity.KnowledgeChunk{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, SourceID: sourceID, Content: text, Topics: inferTopics(text), StyleTags: inferStyleTags(text), RiskLevel: inferRisk(text), CreateAt: now})
	}
	if err := u.repo.CreateKnowledgeSource(ctx, item, chunks); err != nil {
		return nil, err
	}
	out := sourceSummary(*item)
	return &out, nil
}

func (u *useCase) ListKnowledgeSources(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.KnowledgeSourceSummary, error) {
	items, err := u.repo.ListKnowledgeSources(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	out := make([]domusecase.KnowledgeSourceSummary, 0, len(items))
	for _, item := range items {
		out = append(out, sourceSummary(item))
	}
	return out, nil
}

func (u *useCase) ListKnowledgeChunks(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.KnowledgeChunkSummary, error) {
	items, err := u.repo.ListKnowledgeChunks(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	out := make([]domusecase.KnowledgeChunkSummary, 0, len(items))
	for _, item := range items {
		out = append(out, chunkSummary(item))
	}
	return out, nil
}

func (u *useCase) CreateFormulaPool(ctx context.Context, req domusecase.FormulaPoolInput) (*domusecase.FormulaPoolSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
		return nil, err
	}
	if strings.TrimSpace(req.Type) == "" || strings.TrimSpace(req.Name) == "" {
		return nil, app.For(code.Persona).InputMissingRequired("type and name are required")
	}
	now := clock.NowUnixNano()
	weight := req.Weight
	if weight <= 0 {
		weight = 50
	}
	item := &entity.FormulaPool{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, Type: strings.TrimSpace(req.Type), Name: strings.TrimSpace(req.Name), Pattern: strings.TrimSpace(req.Pattern), UseCases: req.UseCases, Avoid: req.Avoid, Weight: weight, CreateAt: now, UpdateAt: now}
	if err := u.repo.CreateFormulaPool(ctx, item); err != nil {
		return nil, err
	}
	out := formulaSummary(*item)
	return &out, nil
}

func (u *useCase) ListFormulaPools(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.FormulaPoolSummary, error) {
	items, err := u.repo.ListFormulaPools(ctx, tenantID, ownerUID, personaID, limit)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		_ = u.seedDefaultFormulas(ctx, tenantID, ownerUID, personaID)
		items, _ = u.repo.ListFormulaPools(ctx, tenantID, ownerUID, personaID, limit)
	}
	out := make([]domusecase.FormulaPoolSummary, 0, len(items))
	for _, item := range items {
		out = append(out, formulaSummary(item))
	}
	return out, nil
}

func (u *useCase) seedDefaultFormulas(ctx context.Context, tenantID, ownerUID, personaID string) error {
	defs := []domusecase.FormulaPoolInput{{Type: "opening", Name: "問題開場", Pattern: "用讀者正在問的具體問題開場", UseCases: []string{"互動文"}, Weight: 70}, {Type: "body", Name: "先共鳴再整理", Pattern: "先接住情緒，再整理 2-3 個可執行觀點", UseCases: []string{"共鳴文", "知識文"}, Weight: 75}, {Type: "emotion", Name: "被理解感", Pattern: "講出讀者不一定說出口的焦慮或委屈", UseCases: []string{"情緒共鳴文"}, Weight: 70}, {Type: "cta", Name: "經驗募集型", Pattern: "邀請讀者補充自己的經驗，不硬導購", UseCases: []string{"互動文"}, Weight: 65}}
	for _, def := range defs {
		def.TenantID = tenantID
		def.OwnerUID = ownerUID
		def.PersonaID = personaID
		if _, err := u.CreateFormulaPool(ctx, def); err != nil {
			return err
		}
	}
	return nil
}

func decodeSourceContent(req domusecase.KnowledgeSourceInput) (string, error) {
	if strings.TrimSpace(req.ContentBase64) != "" {
		data, err := base64.StdEncoding.DecodeString(req.ContentBase64)
		if err != nil {
			return "", err
		}
		if strings.EqualFold(filepath.Ext(req.Filename), ".xlsx") {
			return parseXLSX(data)
		}
		return string(data), nil
	}
	return strings.TrimSpace(req.Content), nil
}

type xlsxText struct {
	T string `xml:",chardata"`
}

func parseXLSX(data []byte) (string, error) {
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		return "", err
	}
	var b strings.Builder
	for _, f := range zr.File {
		if !strings.HasPrefix(f.Name, "xl/sharedStrings") && !strings.HasPrefix(f.Name, "xl/worksheets") {
			continue
		}
		rc, err := f.Open()
		if err != nil {
			continue
		}
		var texts []xlsxText
		_ = xml.NewDecoder(rc).Decode(&texts)
		_ = rc.Close()
		for _, t := range texts {
			if s := strings.TrimSpace(t.T); s != "" {
				b.WriteString(s)
				b.WriteString("\n")
			}
		}
	}
	text := strings.TrimSpace(b.String())
	if text == "" {
		return "", nil
	}
	return text, nil
}

func splitChunks(raw string, max int) []string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	paras := strings.Split(raw, "\n")
	var out []string
	var b strings.Builder
	for _, p := range paras {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		if b.Len()+len([]rune(p)) > max && b.Len() > 0 {
			out = append(out, b.String())
			b.Reset()
		}
		if b.Len() > 0 {
			b.WriteString("\n")
		}
		b.WriteString(p)
	}
	if b.Len() > 0 {
		out = append(out, b.String())
	}
	return out
}

var topicRE = regexp.MustCompile(`[A-Za-z0-9_\p{Han}]{2,12}`)

func inferTopics(text string) []string {
	seen := map[string]bool{}
	var out []string
	for _, m := range topicRE.FindAllString(text, 20) {
		if !seen[m] && len(out) < 8 {
			seen[m] = true
			out = append(out, m)
		}
	}
	return out
}
func inferStyleTags(text string) []string {
	var out []string
	if strings.Contains(text, "我") {
		out = append(out, "第一人稱")
	}
	if strings.Contains(text, "？") || strings.Contains(text, "?") {
		out = append(out, "提問")
	}
	if strings.Contains(text, "。") {
		out = append(out, "自然標點")
	}
	return out
}
func inferRisk(text string) string {
	lower := strings.ToLower(text)
	for _, k := range []string{"醫", "藥", "amh", "投資", "法律"} {
		if strings.Contains(lower, k) {
			return "medium"
		}
	}
	return "low"
}

func normalizeRisk(raw string) string {
	raw = strings.ToLower(strings.TrimSpace(raw))
	if raw == "high" || raw == "low" {
		return raw
	}
	return "medium"
}
func topicSummary(item entity.TopicCandidate) domusecase.TopicCandidateSummary {
	return domusecase.TopicCandidateSummary{ID: item.ID, PersonaID: item.PersonaID, Name: item.Name, Source: item.Source, Category: item.Category, SeedQuery: item.SeedQuery, TrendReason: item.TrendReason, TrendKeywords: item.TrendKeywords, HeatScore: item.HeatScore, FitScore: item.FitScore, InteractionScore: item.InteractionScore, ExtendScore: item.ExtendScore, RiskScore: item.RiskScore, FinalScore: item.FinalScore, RecommendedMissions: item.RecommendedMissions, Status: item.Status, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
}
func planSummary(item entity.ContentPlan) domusecase.ContentPlanSummary {
	return domusecase.ContentPlanSummary{ID: item.ID, PersonaID: item.PersonaID, TopicCandidateID: item.TopicCandidateID, Topic: item.Topic, Mission: item.Mission, TargetAudience: item.TargetAudience, Angle: item.Angle, OpeningType: item.OpeningType, BodyType: item.BodyType, Emotion: item.Emotion, EndingType: item.EndingType, CtaType: item.CtaType, RiskLevel: item.RiskLevel, RequiresHumanReview: item.RequiresHumanReview, Avoid: item.Avoid, SelectedKnowledge: item.SelectedKnowledge, Status: item.Status, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
}
func feedbackSummary(item entity.FeedbackEvent) domusecase.FeedbackEventSummary {
	return domusecase.FeedbackEventSummary{ID: item.ID, PersonaID: item.PersonaID, ContentPlanID: item.ContentPlanID, DraftID: item.DraftID, Decision: item.Decision, Note: item.Note, Snapshot: item.Snapshot, CreateAt: item.CreateAt}
}
func sourceSummary(item entity.KnowledgeSource) domusecase.KnowledgeSourceSummary {
	return domusecase.KnowledgeSourceSummary{ID: item.ID, PersonaID: item.PersonaID, SourceType: item.SourceType, Title: item.Title, Filename: item.Filename, ParsedStatus: item.ParsedStatus, ChunkCount: item.ChunkCount, CreateAt: item.CreateAt}
}
func chunkSummary(item entity.KnowledgeChunk) domusecase.KnowledgeChunkSummary {
	return domusecase.KnowledgeChunkSummary{ID: item.ID, PersonaID: item.PersonaID, SourceID: item.SourceID, Content: item.Content, Topics: item.Topics, StyleTags: item.StyleTags, RiskLevel: item.RiskLevel, CreateAt: item.CreateAt}
}
func formulaSummary(item entity.FormulaPool) domusecase.FormulaPoolSummary {
	return domusecase.FormulaPoolSummary{ID: item.ID, PersonaID: item.PersonaID, Type: item.Type, Name: item.Name, Pattern: item.Pattern, UseCases: item.UseCases, Avoid: item.Avoid, Weight: item.Weight, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
}
