package worker

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync/atomic"
	"time"

	"juhe-factory/api/internal/billing"
	"juhe-factory/api/internal/model"
	"juhe-factory/api/internal/provider/apimart"
	queuepkg "juhe-factory/api/internal/queue"
	"juhe-factory/api/internal/security"

	"github.com/google/uuid"
	"github.com/hibiken/asynq"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type ScriptAnalysis struct {
	DB        *gorm.DB
	Queue     *asynq.Client
	Encryptor *security.Encryptor
	Provider  apimart.Client
}

type scriptAnalysisSnapshot struct {
	Task             model.GenerationTask
	AnalysisID       uuid.UUID
	SourceContent    string
	ModelName        string
	BaseURL          string
	APIKeyCiphertext string
}

type scriptHighlight struct {
	Type    string `json:"type"`
	Content string `json:"content"`
}

type scriptEmotionStage struct {
	Stage       string `json:"stage"`
	Emotion     string `json:"emotion"`
	Description string `json:"description"`
}

type scriptEpisodeOutline struct {
	EpisodeNo int    `json:"episode_no"`
	Title     string `json:"title"`
	Outline   string `json:"outline"`
	Emotion   string `json:"emotion"`
}

type scriptAnalysisCharacter struct {
	Name         string `json:"name"`
	Faction      string `json:"faction"`
	Biography    string `json:"biography"`
	Motivation   string `json:"motivation"`
	Relationship string `json:"relationship"`
}

type scriptEpisodeHook struct {
	EpisodeNo int    `json:"episode_no"`
	HookType  string `json:"hook_type"`
	Hook      string `json:"hook"`
	Basis     string `json:"basis"`
}

type scriptPaywallHook struct {
	HookType string `json:"hook_type"`
	Content  string `json:"content"`
	Appeal   string `json:"appeal"`
}

type scriptCustomAnalysis struct {
	Objective  string   `json:"objective"`
	Conclusion string   `json:"conclusion"`
	Evidence   []string `json:"evidence"`
}

type scriptUnmatchedObjective struct {
	Objective string `json:"objective"`
	Reason    string `json:"reason"`
}

type scriptAnalysisResult struct {
	Tags struct {
		DramaTags           []string `json:"drama_tags"`
		ApplicableSceneTags []string `json:"applicable_scene_tags"`
	} `json:"tags"`
	SellingPoint struct {
		Summary    string            `json:"summary"`
		Highlights []scriptHighlight `json:"highlights"`
	} `json:"selling_point"`
	StoryStructure struct {
		Synopsis        string                    `json:"synopsis"`
		StoryOutline    []string                  `json:"story_outline"`
		EmotionOutline  []scriptEmotionStage      `json:"emotion_outline"`
		EpisodeOutlines []scriptEpisodeOutline    `json:"episode_outlines"`
		Characters      []scriptAnalysisCharacter `json:"characters"`
	} `json:"story_structure"`
	OpeningHook struct {
		FirstScene       string   `json:"first_scene"`
		FirstLine        string   `json:"first_line"`
		AttentionMethods []string `json:"attention_methods"`
		Analysis         string   `json:"analysis"`
		Optimization     string   `json:"optimization"`
	} `json:"opening_hook"`
	EpisodeHooks          []scriptEpisodeHook        `json:"episode_hooks"`
	Episode10PaywallHooks []scriptPaywallHook        `json:"episode_10_paywall_hooks"`
	CustomAnalysis        []scriptCustomAnalysis     `json:"custom_analysis"`
	UnmatchedObjectives   []scriptUnmatchedObjective `json:"unmatched_objectives"`
}

func NewScriptAnalysis(db *gorm.DB, queue *asynq.Client, encryptor *security.Encryptor, client *http.Client) *ScriptAnalysis {
	return &ScriptAnalysis{DB: db, Queue: queue, Encryptor: encryptor, Provider: apimart.NewClient(dramaHTTPClient(client))}
}

func (w *ScriptAnalysis) Register(mux *asynq.ServeMux) {
	mux.HandleFunc(queuepkg.TypeAnalyzeScript, w.analyze)
}

func (w *ScriptAnalysis) Recover(ctx context.Context) error {
	if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
		Where("task_type='script_analysis' AND status='processing'").Update("status", "submitted").Error; err != nil {
		return err
	}
	var ids []uuid.UUID
	if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
		Where("task_type='script_analysis' AND status='submitted'").Pluck("id", &ids).Error; err != nil {
		return err
	}
	var firstErr error
	for _, id := range ids {
		if err := queuepkg.EnqueueID(w.Queue, queuepkg.TypeAnalyzeScript, id, 0); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	var cancelledIDs []uuid.UUID
	if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
		Where("task_type='script_analysis' AND status='cancel_requested'").Pluck("id", &cancelledIDs).Error; err != nil {
		return err
	}
	for _, id := range cancelledIDs {
		if err := w.finishCancellation(ctx, id, uuid.Nil); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return firstErr
}

func (w *ScriptAnalysis) analyze(ctx context.Context, raw *asynq.Task) error {
	taskID, err := decodeID(raw)
	if err != nil {
		return err
	}
	snapshot, err := w.claim(ctx, taskID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		return w.fail(ctx, taskID, uuid.Nil, err)
	}
	apiKey, err := w.Encryptor.Decrypt(snapshot.APIKeyCiphertext)
	if err != nil {
		return w.fail(ctx, taskID, snapshot.AnalysisID, errors.New("剧本分析渠道密钥不可用"))
	}
	workCtx, cancel := context.WithTimeout(ctx, 12*time.Minute)
	defer cancel()
	var cancelled atomic.Bool
	go w.watchCancellation(workCtx, cancel, taskID, &cancelled)
	result, chat, inputTokens, outputTokens, tokenSource, err := w.callModel(workCtx, snapshot, apiKey)
	if cancelled.Load() || w.cancellationRequested(ctx, taskID) {
		return w.finishCancellation(ctx, taskID, snapshot.AnalysisID)
	}
	if err != nil {
		return w.fail(ctx, taskID, snapshot.AnalysisID, err)
	}
	if err := w.persist(ctx, snapshot, result, chat, inputTokens, outputTokens, tokenSource); err != nil {
		return w.fail(ctx, taskID, snapshot.AnalysisID, err)
	}
	return nil
}

func (w *ScriptAnalysis) watchCancellation(ctx context.Context, cancel context.CancelFunc, taskID uuid.UUID, flag *atomic.Bool) {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if w.cancellationRequested(ctx, taskID) {
				flag.Store(true)
				cancel()
				return
			}
		}
	}
}

func (w *ScriptAnalysis) cancellationRequested(ctx context.Context, taskID uuid.UUID) bool {
	var status string
	return w.DB.WithContext(ctx).Model(&model.GenerationTask{}).Where("id=?", taskID).Pluck("status", &status).Error == nil &&
		status == "cancel_requested"
}

func (w *ScriptAnalysis) claim(ctx context.Context, taskID uuid.UUID) (scriptAnalysisSnapshot, error) {
	var snapshot scriptAnalysisSnapshot
	err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("id=? AND task_type='script_analysis' AND status='submitted'", taskID).Take(&snapshot.Task).Error; err != nil {
			return err
		}
		if snapshot.Task.ScriptAnalysisID == nil {
			return errors.New("剧本分析任务缺少剧本标识")
		}
		snapshot.AnalysisID = *snapshot.Task.ScriptAnalysisID
		var input struct {
			SourceContent string `json:"source_content"`
		}
		if err := json.Unmarshal(snapshot.Task.InputData, &input); err != nil || strings.TrimSpace(input.SourceContent) == "" {
			return errors.New("剧本分析原文快照无效")
		}
		snapshot.SourceContent = input.SourceContent
		var config struct {
			ModelName        string
			BaseURL          string
			APIKeyCiphertext string
		}
		result := tx.Raw(`SELECT model.name AS model_name,channel.base_url,channel.api_key_ciphertext
			FROM models model JOIN channels channel ON channel.id=model.channel_id
			WHERE model.id=? AND channel.id=? AND model.model_type='text'
			AND model.enabled=true AND model.deleted_at IS NULL AND channel.enabled=true AND channel.deleted_at IS NULL`,
			snapshot.Task.ModelID, snapshot.Task.ChannelID).Scan(&config)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return errors.New("剧本分析模型或渠道不可用")
		}
		snapshot.ModelName, snapshot.BaseURL, snapshot.APIKeyCiphertext = config.ModelName, config.BaseURL, config.APIKeyCiphertext
		if err := tx.Model(&snapshot.Task).Update("status", "processing").Error; err != nil {
			return err
		}
		return tx.Model(&model.ScriptAnalysis{}).Where("id=?", snapshot.AnalysisID).
			Updates(map[string]any{"analysis_status": "running", "analysis_message": "正在分析剧本"}).Error
	})
	return snapshot, err
}

func (w *ScriptAnalysis) callModel(ctx context.Context, snapshot scriptAnalysisSnapshot, apiKey string) (scriptAnalysisResult, apimart.ChatResult, int64, int64, string, error) {
	var parsed scriptAnalysisResult
	var chat apimart.ChatResult
	pricing, err := billing.ParseTextPricingSnapshot(snapshot.Task.BillingSnapshot)
	if err != nil {
		return parsed, chat, 0, 0, "", err
	}
	systemPrompt := scriptAnalysisSystemPrompt(snapshot.Task.PromptSnapshot)
	userPrompt := "请分析以下剧本原文：\n\n" + snapshot.SourceContent
	if pricing.Mode == billing.TextBillingPerToken {
		estimated := billing.EstimateTextTokens(systemPrompt + userPrompt)
		if err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
			_, reserveErr := billing.ReserveTextCall(tx, snapshot.Task.UserID, snapshot.Task.ID.String(), "analysis", pricing, estimated, "剧本分析")
			return reserveErr
		}); err != nil {
			return parsed, chat, 0, 0, "", err
		}
	}
	payload := map[string]any{
		"model": snapshot.ModelName,
		"messages": []map[string]any{
			{"role": "system", "content": systemPrompt},
			{"role": "user", "content": userPrompt},
		},
		"temperature":     0.2,
		"response_format": map[string]any{"type": "json_object"},
	}
	var parseErr error
	for attempt := 0; attempt < 2; attempt++ {
		if attempt > 0 {
			payload["temperature"] = 0
			payload["messages"] = []map[string]any{
				{"role": "system", "content": systemPrompt},
				{"role": "user", "content": userPrompt + "\n\n上次返回未通过结构校验。请严格补齐固定协议的全部字段，并按分析目标决定标准字段、补充分析或无法完成的目标，只返回完整合法 JSON。"},
			}
		}
		chat, err = w.Provider.ChatWithUsage(ctx, snapshot.BaseURL, apiKey, payload)
		if err != nil {
			return parsed, chat, 0, 0, "", err
		}
		parsed = scriptAnalysisResult{}
		parseErr = json.Unmarshal([]byte(stripJSONFence(chat.Content)), &parsed)
		if parseErr == nil {
			parseErr = validateScriptAnalysisResult(parsed)
		}
		if parseErr == nil {
			break
		}
	}
	if parseErr != nil {
		finishReason := strings.TrimSpace(chat.FinishReason)
		if finishReason == "" {
			finishReason = "未知"
		}
		return parsed, chat, 0, 0, "", fmt.Errorf("文本模型连续两次返回无法解析（结束原因：%s）: %w", finishReason, parseErr)
	}
	inputTokens, outputTokens := chat.InputTokens, chat.OutputTokens
	tokenSource := "upstream"
	if inputTokens == 0 {
		inputTokens = billing.EstimateTextTokens(systemPrompt + userPrompt)
		tokenSource = "local"
	}
	if outputTokens == 0 {
		outputTokens = billing.EstimateTextTokens(chat.Content)
		if tokenSource == "upstream" {
			tokenSource = "mixed"
		}
	}
	if pricing.Mode == billing.TextBillingPerToken {
		if err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
			settled, settleErr := billing.SettleTextCall(tx, snapshot.Task.UserID, snapshot.Task.ID.String(), "analysis", pricing,
				billing.TextUsage{Input: inputTokens, Output: outputTokens}, "剧本分析")
			if settleErr != nil {
				return settleErr
			}
			return tx.Model(&model.GenerationTask{}).Where("id=?", snapshot.Task.ID).Updates(map[string]any{
				"estimated_points": gorm.Expr("estimated_points+?::numeric", settled),
				"prepaid_points":   gorm.Expr("prepaid_points+?::numeric", settled),
			}).Error
		}); err != nil {
			return parsed, chat, inputTokens, outputTokens, tokenSource, err
		}
	}
	return parsed, chat, inputTokens, outputTokens, tokenSource, nil
}

func validateScriptAnalysisResult(result scriptAnalysisResult) error {
	if strings.TrimSpace(result.SellingPoint.Summary) == "" && strings.TrimSpace(result.StoryStructure.Synopsis) == "" &&
		len(result.CustomAnalysis) == 0 && len(result.UnmatchedObjectives) == 0 {
		return errors.New("缺少标准分析结果、通用分析结果或无法匹配目标说明")
	}
	if len(result.EpisodeHooks) != 0 && len(result.EpisodeHooks) != 10 {
		return errors.New("前十集钩子数量必须为10")
	}
	for index, hook := range result.EpisodeHooks {
		if hook.EpisodeNo != index+1 {
			return errors.New("前十集钩子集数或顺序无效")
		}
	}
	if len(result.Episode10PaywallHooks) != 0 &&
		(len(result.Episode10PaywallHooks) < 3 || len(result.Episode10PaywallHooks) > 5) {
		return errors.New("第十集付费卡点候选必须为3至5个")
	}
	for _, character := range result.StoryStructure.Characters {
		if strings.TrimSpace(character.Name) == "" {
			return errors.New("人物名称不能为空")
		}
		if character.Faction != "正派" && character.Faction != "反派" && character.Faction != "中立派" {
			return errors.New("人物阵营无效")
		}
	}
	return nil
}

func (w *ScriptAnalysis) persist(ctx context.Context, snapshot scriptAnalysisSnapshot, result scriptAnalysisResult, chat apimart.ChatResult, inputTokens, outputTokens int64, tokenSource string) error {
	encoded, err := json.Marshal(result)
	if err != nil {
		return err
	}
	pretty, _ := json.MarshalIndent(result, "", "  ")
	return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var task model.GenerationTask
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", snapshot.Task.ID).Take(&task).Error; err != nil {
			return err
		}
		if task.Status == "cancel_requested" {
			return w.finishCancellationLocked(tx, &task, snapshot.AnalysisID)
		}
		if task.Status != "processing" {
			return errors.New("剧本分析任务状态已变化")
		}
		if err := tx.Where("script_analysis_id=?", snapshot.AnalysisID).Delete(&model.ScriptAnalysisCharacter{}).Error; err != nil {
			return err
		}
		seen := make(map[string]bool)
		for index, character := range result.StoryStructure.Characters {
			name := strings.TrimSpace(character.Name)
			key := strings.ToLower(name)
			if seen[key] {
				continue
			}
			seen[key] = true
			item := model.ScriptAnalysisCharacter{
				ScriptAnalysisID: snapshot.AnalysisID, Name: name, Faction: character.Faction,
				Biography: strings.TrimSpace(character.Biography), Motivation: strings.TrimSpace(character.Motivation),
				Relationship: strings.TrimSpace(character.Relationship), SortOrder: index,
			}
			if err := tx.Create(&item).Error; err != nil {
				return err
			}
		}
		taskUpdates := map[string]any{
			"status": "succeeded", "finished_at": time.Now(), "actual_points": task.PrepaidPoints,
			"input_tokens": inputTokens, "output_tokens": outputTokens, "total_tokens": inputTokens + outputTokens,
			"token_count_source": tokenSource, "usage_raw": chat.UsageRaw, "result_data": gorm.Expr("?::jsonb", string(encoded)),
			"error_code": nil, "error_message": nil,
		}
		if err := tx.Model(&task).Updates(taskUpdates).Error; err != nil {
			return err
		}
		return tx.Model(&model.ScriptAnalysis{}).Where("id=?", snapshot.AnalysisID).Updates(map[string]any{
			"analysis_result": gorm.Expr("?::jsonb", string(encoded)), "result_content": string(pretty),
			"analysis_status": "succeeded", "analysis_message": "剧本分析完成",
		}).Error
	})
}

func (w *ScriptAnalysis) fail(ctx context.Context, taskID, analysisID uuid.UUID, taskErr error) error {
	return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var task model.GenerationTask
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", taskID).Take(&task).Error; err != nil {
			return err
		}
		if task.Status == "succeeded" || task.Status == "failed" || task.Status == "cancelled" {
			return nil
		}
		if task.Status == "cancel_requested" {
			return w.finishCancellationLocked(tx, &task, analysisID)
		}
		refunded, err := billing.RefundTextGenerationTask(tx, &task, "剧本分析失败返还")
		if err != nil {
			return err
		}
		updates := map[string]any{
			"status": "failed", "actual_points": "0.00", "finished_at": time.Now(),
			"error_code": "script_analysis_failed", "error_message": truncate(taskErr.Error(), 4000),
		}
		if refunded {
			updates["cost_refunded"] = true
		}
		if err := tx.Model(&task).Updates(updates).Error; err != nil {
			return err
		}
		if analysisID == uuid.Nil && task.ScriptAnalysisID != nil {
			analysisID = *task.ScriptAnalysisID
		}
		if analysisID == uuid.Nil {
			return nil
		}
		return tx.Model(&model.ScriptAnalysis{}).Where("id=?", analysisID).Updates(map[string]any{
			"analysis_status": "failed", "analysis_message": truncate(taskErr.Error(), 1000),
		}).Error
	})
}

func (w *ScriptAnalysis) finishCancellation(ctx context.Context, taskID, analysisID uuid.UUID) error {
	return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var task model.GenerationTask
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", taskID).Take(&task).Error; err != nil {
			return err
		}
		return w.finishCancellationLocked(tx, &task, analysisID)
	})
}

func (w *ScriptAnalysis) finishCancellationLocked(tx *gorm.DB, task *model.GenerationTask, analysisID uuid.UUID) error {
	if task.Status == "cancelled" {
		return nil
	}
	if task.Status == "succeeded" || task.Status == "failed" {
		return nil
	}
	actual := task.PrepaidPoints
	committed, err := billing.CommitTextReserves(tx, task.UserID, task.ID.String(), "剧本分析取消")
	if err != nil {
		return err
	}
	if committed != "" && committed != "0" && committed != "0.00" {
		actual = committed
	}
	if err := tx.Model(task).Updates(map[string]any{
		"status": "cancelled", "prepaid_points": actual, "actual_points": actual, "finished_at": time.Now(),
		"error_code": "user_cancelled", "error_message": "用户取消，已扣积分不退",
	}).Error; err != nil {
		return err
	}
	if analysisID == uuid.Nil && task.ScriptAnalysisID != nil {
		analysisID = *task.ScriptAnalysisID
	}
	if analysisID == uuid.Nil {
		return nil
	}
	return tx.Model(&model.ScriptAnalysis{}).Where("id=?", analysisID).Updates(map[string]any{
		"analysis_status": "idle", "analysis_message": "剧本分析已取消，已扣积分不退",
	}).Error
}
