package controller

import (
	"errors"
	"strconv"

	"github.com/echochat/backend/app/meeting/model"
	"github.com/echochat/backend/app/meeting/service"
	"github.com/echochat/backend/config"
	"github.com/echochat/backend/pkg/logs"
	"github.com/echochat/backend/pkg/utils"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

// MeetingRecordingController 会议录制相关 REST 接口（Phase B 引入）
//
// 路由（在 router.go 中挂载到 /api/v1/meeting/rooms/:code/recordings 下）：
//   - POST   /rooms/:code/recordings     启动录制（仅 host）
//   - DELETE /rooms/:code/recordings/:id 停止录制（仅 host）
//   - GET    /rooms/:code/recordings     列出录制（host 或参与者）
type MeetingRecordingController struct {
	recordingSvc *service.MeetingRecordingService
	cfg          *config.MeetingConfig
}

// NewMeetingRecordingController Wire Provider
//
// cfg 提供 InternalWebhookSecret，用于校验 media-server 失败回调 X-Internal-Secret header
func NewMeetingRecordingController(
	svc *service.MeetingRecordingService,
	cfg *config.MeetingConfig,
) *MeetingRecordingController {
	return &MeetingRecordingController{recordingSvc: svc, cfg: cfg}
}

// handleErr 将录制相关领域错误映射成 HTTP 状态码
func (ctl *MeetingRecordingController) handleErr(c *gin.Context, err error, fallbackMsg string) {
	switch {
	case errors.Is(err, service.ErrMeetingNotFound),
		errors.Is(err, service.ErrRecordingNotFound):
		utils.ResponseNotFound(c, err.Error())
	case errors.Is(err, service.ErrNotMeetingHost):
		utils.ResponseForbidden(c, err.Error())
	case errors.Is(err, service.ErrMediaServiceUnavailable):
		utils.ResponseError(c, err.Error())
	case errors.Is(err, service.ErrNotInMeeting),
		errors.Is(err, service.ErrMeetingEnded),
		errors.Is(err, service.ErrRecordingAlreadyActive),
		errors.Is(err, service.ErrRecordingNoProducers),
		errors.Is(err, service.ErrRecordingNotStoppable):
		utils.ResponseBadRequest(c, err.Error())
	default:
		logs.Warn(c.Request.Context(), "controller.meeting_recording_controller.handleErr",
			fallbackMsg, zap.Error(err))
		utils.ResponseError(c, fallbackMsg)
	}
}

// StartRecording POST /api/v1/meeting/rooms/:code/recordings
func (ctl *MeetingRecordingController) StartRecording(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	rec, err := ctl.recordingSvc.StartRecording(c.Request.Context(), userID, code)
	if err != nil {
		ctl.handleErr(c, err, "启动录制失败")
		return
	}
	utils.ResponseOK(c, recordingToDTO(rec))
}

// StopRecording DELETE /api/v1/meeting/rooms/:code/recordings/:id
func (ctl *MeetingRecordingController) StopRecording(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	recordingID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil || recordingID <= 0 {
		utils.ResponseBadRequest(c, "录制 ID 非法")
		return
	}
	rec, stopErr := ctl.recordingSvc.StopRecording(c.Request.Context(), userID, code, recordingID)
	if stopErr != nil {
		ctl.handleErr(c, stopErr, "停止录制失败")
		return
	}
	utils.ResponseOK(c, recordingToDTO(rec))
}

// ListRecordings GET /api/v1/meeting/rooms/:code/recordings
func (ctl *MeetingRecordingController) ListRecordings(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	list, err := ctl.recordingSvc.ListRecordings(c.Request.Context(), userID, code)
	if err != nil {
		ctl.handleErr(c, err, "查询录制列表失败")
		return
	}
	out := make([]map[string]any, 0, len(list))
	for i := range list {
		out = append(out, recordingToDTO(&list[i]))
	}
	utils.ResponseOK(c, gin.H{"list": out})
}

// requireUserID 复用 controller 包内的 requireUserID（在 meeting_controller.go 中定义）
// 这里通过同包私有函数访问，避免引入额外依赖

// recordingToDTO 录制记录的精简响应结构
// 不暴露 RemoteID / FileObject（敏感的内部寻址字段），供前端直接渲染列表 + 播放
func recordingToDTO(r *model.MeetingRecording) map[string]any {
	if r == nil {
		return nil
	}
	out := map[string]any{
		"id":             r.ID,
		"room_id":        r.RoomID,
		"room_code":      r.RoomCode,
		"started_by":     r.StartedBy,
		"status":         r.Status,
		"file_url":       r.FileURL,
		"size_bytes":     r.SizeBytes,
		"duration_sec":   r.DurationSec,
		"failure_reason": "",
		"started_at":     r.StartedAt.Unix(),
	}
	// 仅在最终态 failed 时回显失败原因；recording 状态下 FailureReason 字段被当作临时本地路径，不应外泄
	if r.Status == model.MeetingRecordingStatusFailed {
		out["failure_reason"] = r.FailureReason
	}
	if r.StoppedAt != nil {
		out["stopped_at"] = r.StoppedAt.Unix()
	}
	return out
}

// ==================== Internal Webhook（Phase B watchdog） ====================

// mediaServerFailurePayload media-server 上报的失败 JSON 体
// 字段与 media-server/src/services/recording.service.ts::RecordingFailureCallback 对应
type mediaServerFailurePayload struct {
	RecordingID   string `json:"recordingId"`   // media-server 端的 remoteId（UUID 字符串），非 Go 端主键
	RoomCode      string `json:"roomCode"`
	RouterID      string `json:"routerId"`
	ExitCode      *int   `json:"exitCode"`
	FailureReason string `json:"failureReason"`
}

// MediaServerFailureWebhook POST /internal/meeting/recordings/failure
//
// media-server 端 ffmpeg 异常退出时回调此接口（由 RECORDING_FAILURE_WEBHOOK_URL 配置）
// 鉴权：
//   - 必须携带 Header `X-Internal-Secret`，值与环境变量 INTERNAL_WEBHOOK_SECRET 一致
//   - 未配置 secret 时直接 403（默认安全，避免暴露内部端点）
//
// 行为：
//   - 解析 payload.recordingId（media-server remoteId）→ 反查 Go 端 meeting_recordings 表
//   - 委托给 MeetingRecordingService.HandleMediaServerFailure 落 failed + 广播
//   - 始终返回 200（webhook 语义：吞错避免 media-server 端反复重试）
func (ctl *MeetingRecordingController) MediaServerFailureWebhook(c *gin.Context) {
	funcName := "controller.meeting_recording_controller.MediaServerFailureWebhook"
	// 优先 config.meeting.internal_webhook_secret（yaml/viper）。
	// 环境变量覆盖走 ECHOCHAT_MEETING_INTERNAL_WEBHOOK_SECRET（LoadConfig 已启用 viper.AutomaticEnv）。
	secret := ctl.cfg.InternalWebhookSecret
	if secret == "" {
		logs.Warn(c.Request.Context(), funcName, "meeting.internal_webhook_secret 未配置，拒绝 webhook 请求")
		utils.ResponseForbidden(c, "internal webhook not configured")
		return
	}
	if c.GetHeader("X-Internal-Secret") != secret {
		logs.Warn(c.Request.Context(), funcName, "webhook 鉴权失败",
			zap.String("client_ip", c.ClientIP()))
		utils.ResponseForbidden(c, "invalid internal secret")
		return
	}

	var payload mediaServerFailurePayload
	if err := c.ShouldBindJSON(&payload); err != nil {
		logs.Warn(c.Request.Context(), funcName, "解析 payload 失败", zap.Error(err))
		utils.ResponseBadRequest(c, "invalid payload")
		return
	}

	if payload.RecordingID == "" {
		utils.ResponseBadRequest(c, "missing recordingId")
		return
	}

	// 反查 Go 端主键：通过 RemoteID 定位 meeting_recordings 行
	// 调用方（media-server）持有的是 remoteId（UUID 字符串），Go 端用 ID 主键管理
	rec, err := ctl.recordingSvc.GetByRemoteID(c.Request.Context(), payload.RecordingID)
	if err != nil {
		logs.Warn(c.Request.Context(), funcName, "查询录制记录失败（按 remote_id）",
			zap.String("remote_id", payload.RecordingID), zap.Error(err))
		// 仍 200，避免 media-server 重试风暴
		utils.ResponseOK(c, gin.H{"handled": false, "reason": "lookup_failed"})
		return
	}
	if rec == nil {
		logs.Info(c.Request.Context(), funcName, "未找到对应录制记录（可能已被清理）",
			zap.String("remote_id", payload.RecordingID))
		utils.ResponseOK(c, gin.H{"handled": false, "reason": "not_found"})
		return
	}

	if err := ctl.recordingSvc.HandleMediaServerFailure(c.Request.Context(), rec.ID, payload.FailureReason); err != nil {
		logs.Warn(c.Request.Context(), funcName, "处理失败 webhook 出错",
			zap.Int64("recording_id", rec.ID), zap.Error(err))
		utils.ResponseOK(c, gin.H{"handled": false, "reason": "internal_error"})
		return
	}

	utils.ResponseOK(c, gin.H{"handled": true, "recording_id": rec.ID})
}
