// Package service 提供 transcribe 模块的业务服务
package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	meetingModel "github.com/echochat/backend/app/meeting/model"
	"github.com/echochat/backend/app/transcribe/dao"
	"github.com/echochat/backend/app/transcribe/model"
	"github.com/echochat/backend/pkg/logs"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// 业务错误
var (
	ErrRecordingNotFound  = errors.New("录制不存在")
	ErrRecordingNotReady  = errors.New("录制尚未就绪，无法转写")
	ErrTranscribeRunning  = errors.New("已有转写任务进行中，请稍后再试")
	ErrSTTNotConfigured   = errors.New("语音转写服务未配置")
)

// TranscribeService 转写编排服务
//
// 设计要点：
//   - Submit 立即返回 (transcript, 已 running)，真正的 STT 调用走 goroutine
//   - 同一 recording 只允许一个 running 任务，重复提交直接返回当前进行中行
//   - 转写超时 10 分钟；超时后任务自标记 failed
//   - 服务重启会留下 running 状态的孤儿行，留给后续兜底任务处理（暂不在本期实现）
type TranscribeService struct {
	db           *gorm.DB
	transcripts  *dao.TranscriptDAO
	llmConfig    *dao.LLMConfigDAO
	sttClient    *OpenAICompatibleClient
}

// NewTranscribeService 创建实例
func NewTranscribeService(
	db *gorm.DB,
	transcripts *dao.TranscriptDAO,
	llmConfig *dao.LLMConfigDAO,
	sttClient *OpenAICompatibleClient,
) *TranscribeService {
	return &TranscribeService{
		db:          db,
		transcripts: transcripts,
		llmConfig:   llmConfig,
		sttClient:   sttClient,
	}
}

// IsAvailable 转写功能是否可用（LLM 配置库连通）
func (s *TranscribeService) IsAvailable() bool {
	return s.llmConfig != nil && s.llmConfig.IsEnabled()
}

// Submit 提交转写任务
//
// 行为：
//   - 录制必须存在且 status=ready
//   - 如果已有 running 行，返回 ErrTranscribeRunning
//   - 如果已有 ready 行且 force=false，直接返回已有结果
//   - 否则：插入或重置 transcript 行 → 启动 goroutine 跑真实调用 → 同步返回 running 状态
//
// 参数：
//   - force：true 表示强制重跑（覆盖 ready 行）
//   - language：可选，"" 表示自动检测
//
// 返回值为 Submit 时刻的 transcript 快照，调用方可继续轮询 Get 拿最新状态。
func (s *TranscribeService) Submit(ctx context.Context, recordingID int64, force bool, language string) (*model.Transcript, error) {
	if !s.IsAvailable() {
		return nil, ErrSTTNotConfigured
	}

	// 1) 校验录制存在且已就绪
	var rec meetingModel.MeetingRecording
	err := s.db.WithContext(ctx).Where("id = ?", recordingID).First(&rec).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrRecordingNotFound
		}
		return nil, err
	}
	if rec.Status != meetingModel.MeetingRecordingStatusReady || rec.FileURL == "" {
		return nil, ErrRecordingNotReady
	}

	// 2) 检查现有 transcript
	existing, err := s.transcripts.GetByRecordingID(ctx, recordingID)
	if err != nil {
		return nil, err
	}
	if existing != nil {
		switch existing.Status {
		case model.TranscriptStatusRunning, model.TranscriptStatusPending:
			return existing, ErrTranscribeRunning
		case model.TranscriptStatusReady:
			if !force {
				return existing, nil
			}
		}
	}

	// 3) 创建/更新行为 pending（即将进入 running）
	now := time.Now()
	t := &model.Transcript{
		RecordingID: recordingID,
		RoomID:      rec.RoomID,
		Status:      model.TranscriptStatusPending,
		Segments:    "[]",
		StartedAt:   &now,
	}
	if existing != nil {
		t.ID = existing.ID
	}
	if err := s.transcripts.Upsert(ctx, t); err != nil {
		return nil, fmt.Errorf("写入 transcript 失败: %w", err)
	}

	// 4) 取最新 ID（Upsert 后 t.ID 已填）
	if err := s.transcripts.MarkRunning(ctx, t.ID); err != nil {
		return nil, fmt.Errorf("切换 running 状态失败: %w", err)
	}

	// 5) 异步执行真实调用
	//    使用全新 context，超时 10min；不沿用入参 ctx，避免 HTTP 请求结束后 ctx 被 cancel 中断后台任务
	go s.runJob(t.ID, rec, language)

	// 返回 running 状态快照
	updated, _ := s.transcripts.GetByRecordingID(ctx, recordingID)
	if updated != nil {
		return updated, nil
	}
	return t, nil
}

// runJob goroutine 内执行真实 STT 调用
//
// 不返回错误：所有失败都写回数据库 status=failed + error_msg
func (s *TranscribeService) runJob(transcriptID int64, rec meetingModel.MeetingRecording, language string) {
	const funcName = "TranscribeService.runJob"
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	logs.Info(ctx, funcName, "STT 任务开始",
		zap.Int64("transcript_id", transcriptID),
		zap.Int64("recording_id", rec.ID),
	)

	cfg, err := s.llmConfig.PickActiveSTT(ctx)
	if err != nil {
		s.fail(ctx, transcriptID, "选择 STT 配置失败: "+err.Error())
		return
	}

	// 把 provider/model/key 元信息写回 transcript，便于审计
	_ = s.db.WithContext(ctx).Model(&model.Transcript{}).
		Where("id = ?", transcriptID).
		Updates(map[string]any{
			"provider_code": cfg.Provider.ProviderCode,
			"model_code":    cfg.Model.ModelCode,
			"key_id":        cfg.Key.ID,
		}).Error
	logs.Info(ctx, funcName, "已选择 STT 配置",
		zap.Int64("transcript_id", transcriptID),
		zap.String("provider_code", cfg.Provider.ProviderCode),
		zap.String("model_code", cfg.Model.ModelCode),
		zap.String("base_url", cfg.Provider.BaseURL),
		zap.String("file_url", rec.FileURL))

	result, err := s.sttClient.Transcribe(ctx, cfg, rec.FileURL, language)
	if err != nil {
		s.fail(ctx, transcriptID, "STT 调用失败: "+err.Error())
		return
	}

	segmentsJSON, err := json.Marshal(result.Segments)
	if err != nil {
		segmentsJSON = []byte("[]")
	}

	if err := s.transcripts.MarkReady(ctx, transcriptID,
		result.Text, string(segmentsJSON),
		result.Language, int(result.Duration),
	); err != nil {
		logs.Error(ctx, funcName, "写回 ready 状态失败", zap.Error(err))
		return
	}
	logs.Info(ctx, funcName, "STT 任务完成",
		zap.Int64("transcript_id", transcriptID),
		zap.Int("text_len", len(result.Text)),
		zap.Int("segments", len(result.Segments)),
	)
}

// fail 统一失败收尾：写日志 + 落库
func (s *TranscribeService) fail(ctx context.Context, transcriptID int64, msg string) {
	logs.Warn(ctx, "TranscribeService.fail", msg, zap.Int64("transcript_id", transcriptID))
	if err := s.transcripts.MarkFailed(ctx, transcriptID, msg); err != nil {
		logs.Error(ctx, "TranscribeService.fail", "写回 failed 状态失败", zap.Error(err))
	}
}

// GetByRecording 拉取某段录制的转写记录（不存在返回 nil, nil）
func (s *TranscribeService) GetByRecording(ctx context.Context, recordingID int64) (*model.Transcript, error) {
	return s.transcripts.GetByRecordingID(ctx, recordingID)
}

// ListByRoom 拉取一场会议下所有转写
func (s *TranscribeService) ListByRoom(ctx context.Context, roomID int64) ([]model.Transcript, error) {
	return s.transcripts.ListByRoomID(ctx, roomID)
}
