package dao

import (
	"context"
	"errors"
	"time"

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

// MeetingRecordingDAO 会议录制数据访问对象（Phase B 新增）
//
// 设计要点：
//   - Create 后立刻获得 ID 主键，作为前端可见的 recordingId
//   - 状态流转通过 UpdateStatus / UpdateForUpload / UpdateReady / UpdateFailed 等显式方法做，
//     避免业务层直接 db.Save 大对象造成不一致写
//   - 每个状态切换都基于"前置状态白名单"行级 WHERE，幂等 + 防止 race
type MeetingRecordingDAO struct {
	db *gorm.DB
}

// NewMeetingRecordingDAO 构造 DAO（Wire Provider）
func NewMeetingRecordingDAO(db *gorm.DB) *MeetingRecordingDAO {
	return &MeetingRecordingDAO{db: db}
}

// Create 写入一条新录制记录（recording 初始状态）
// 调用方需保证 RemoteID 已由 media-server 返回；若尚未拿到（先建本地壳记录），传空串即可，
// 后续用 UpdateRemoteID 补全。
func (d *MeetingRecordingDAO) Create(ctx context.Context, rec *model.MeetingRecording) error {
	funcName := "dao.meeting_recording_dao.Create"
	if err := d.db.WithContext(ctx).Create(rec).Error; err != nil {
		logs.Error(ctx, funcName, "创建录制记录失败",
			zap.String("room_code", rec.RoomCode),
			zap.Int64("started_by", rec.StartedBy),
			zap.Error(err))
		return err
	}
	return nil
}

// GetByID 按主键查询，记录不存在返回 (nil, nil)
func (d *MeetingRecordingDAO) GetByID(ctx context.Context, id int64) (*model.MeetingRecording, error) {
	var rec model.MeetingRecording
	err := d.db.WithContext(ctx).First(&rec, id).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return &rec, nil
}

// GetActiveByRoom 查询某会议正在进行中的录制（status=recording 或 uploading）
// 同会议同一时刻只允许 1 条活跃录制；用于"开始录制"前置防重 + EndRoom 自动停录
// 不存在返回 (nil, nil)
func (d *MeetingRecordingDAO) GetActiveByRoom(ctx context.Context, roomID int64) (*model.MeetingRecording, error) {
	var rec model.MeetingRecording
	err := d.db.WithContext(ctx).
		Where("room_id = ? AND status IN ?", roomID, []string{
			model.MeetingRecordingStatusRecording,
			model.MeetingRecordingStatusUploading,
		}).
		Order("id DESC").
		First(&rec).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return &rec, nil
}

// ListByRoom 按 room_id 倒序列出全部录制（用于"录制列表"页）
// 不分页：单会议录制条数上限受业务约束（一般 ≤ 20）；如需分页可后续扩展
func (d *MeetingRecordingDAO) ListByRoom(ctx context.Context, roomID int64) ([]model.MeetingRecording, error) {
	var list []model.MeetingRecording
	err := d.db.WithContext(ctx).
		Where("room_id = ?", roomID).
		Order("id DESC").
		Find(&list).Error
	return list, err
}

// GetByRemoteID 按 media-server 返回的 remoteId 反查本地记录，不存在返回 (nil, nil)
// 供 webhook sink 在收到 media-server 的失败回调时定位本地行
func (d *MeetingRecordingDAO) GetByRemoteID(ctx context.Context, remoteID string) (*model.MeetingRecording, error) {
	if remoteID == "" {
		return nil, nil
	}
	var rec model.MeetingRecording
	err := d.db.WithContext(ctx).Where("remote_id = ?", remoteID).First(&rec).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return &rec, nil
}

// UpdateRemoteID 写入 media-server 返回的 recordingId
// 用于先 Create 本地记录、再调 media-server 拿到 RemoteID 后回填的两阶段流程
func (d *MeetingRecordingDAO) UpdateRemoteID(ctx context.Context, id int64, remoteID string) error {
	return d.db.WithContext(ctx).
		Model(&model.MeetingRecording{}).
		Where("id = ?", id).
		Update("remote_id", remoteID).Error
}

// MarkUploading 录制结束 → 上传中：仅当 status=recording 才推进，幂等
// 同时落 stopped_at + duration_sec
func (d *MeetingRecordingDAO) MarkUploading(ctx context.Context, id int64, stoppedAt time.Time, durationSec int) (int64, error) {
	res := d.db.WithContext(ctx).
		Model(&model.MeetingRecording{}).
		Where("id = ? AND status = ?", id, model.MeetingRecordingStatusRecording).
		Updates(map[string]interface{}{
			"status":       model.MeetingRecordingStatusUploading,
			"stopped_at":   stoppedAt,
			"duration_sec": durationSec,
		})
	return res.RowsAffected, res.Error
}

// MarkReady 上传成功 → ready；记录 file_url / file_object / size_bytes
// 仅当 status=uploading 才推进
func (d *MeetingRecordingDAO) MarkReady(ctx context.Context, id int64, fileURL, fileObject string, sizeBytes int64) (int64, error) {
	res := d.db.WithContext(ctx).
		Model(&model.MeetingRecording{}).
		Where("id = ? AND status = ?", id, model.MeetingRecordingStatusUploading).
		Updates(map[string]interface{}{
			"status":      model.MeetingRecordingStatusReady,
			"file_url":    fileURL,
			"file_object": fileObject,
			"size_bytes":  sizeBytes,
		})
	return res.RowsAffected, res.Error
}

// MarkFailed 终态失败：从任意非 ready 状态都允许推进，记录 failure_reason
// 调用方需在 reason 为空时传一个有意义的兜底字符串（如 "unknown"）
func (d *MeetingRecordingDAO) MarkFailed(ctx context.Context, id int64, reason string) (int64, error) {
	if reason == "" {
		reason = "unknown"
	}
	res := d.db.WithContext(ctx).
		Model(&model.MeetingRecording{}).
		Where("id = ? AND status != ?", id, model.MeetingRecordingStatusReady).
		Updates(map[string]interface{}{
			"status":         model.MeetingRecordingStatusFailed,
			"failure_reason": reason,
		})
	return res.RowsAffected, res.Error
}

// ListUnfinishedSince 查询启动时间早于 cutoff 但仍未到终态（ready/failed）的录制 ID
// 供兜底定时任务在 media-server 重启后清理"卡死的录制"，避免 DB 永远停留在 recording 状态
func (d *MeetingRecordingDAO) ListUnfinishedSince(ctx context.Context, cutoff time.Time, limit int) ([]int64, error) {
	var ids []int64
	err := d.db.WithContext(ctx).
		Model(&model.MeetingRecording{}).
		Where("status IN ? AND started_at < ?",
			[]string{model.MeetingRecordingStatusRecording, model.MeetingRecordingStatusUploading},
			cutoff).
		Order("started_at ASC").
		Limit(limit).
		Pluck("id", &ids).Error
	return ids, err
}
