package controller

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/songquanpeng/one-api/common"
	"github.com/songquanpeng/one-api/common/config"
	"github.com/songquanpeng/one-api/common/ctxkey"
	"github.com/songquanpeng/one-api/common/errfilter"
	"github.com/songquanpeng/one-api/common/helper"
	"github.com/songquanpeng/one-api/common/logger"
	"github.com/songquanpeng/one-api/middleware"
	dbmodel "github.com/songquanpeng/one-api/model"
	"github.com/songquanpeng/one-api/monitor"
	"github.com/songquanpeng/one-api/relay/adaptor/seedance"
	"github.com/songquanpeng/one-api/relay/billing"
	"github.com/songquanpeng/one-api/relay/channeltype"
	"github.com/songquanpeng/one-api/relay/controller"
	"github.com/songquanpeng/one-api/relay/meta"
	"github.com/songquanpeng/one-api/relay/model"
	"github.com/songquanpeng/one-api/relay/relaymode"
)

// https://platform.openai.com/docs/api-reference/chat

func relayHelper(c *gin.Context, relayMode int) *model.ErrorWithStatusCode {
	var err *model.ErrorWithStatusCode
	switch relayMode {
	case relaymode.ImagesGenerations:
		err = controller.RelayImageHelper(c, relayMode)
	case relaymode.AudioSpeech:
		fallthrough
	case relaymode.AudioTranslation:
		fallthrough
	case relaymode.AudioTranscription:
		err = controller.RelayAudioHelper(c, relayMode)
	case relaymode.VideosGenerations:
		err = controller.RelayVideoHelper(c)
	case relaymode.Proxy:
		err = controller.RelayProxyHelper(c, relayMode)
	default:
		err = controller.RelayTextHelper(c)
	}
	return err
}

func Relay(c *gin.Context) {
	ctx := c.Request.Context()
	relayMode := relaymode.GetByPath(c.Request.URL.Path)
	if config.DebugEnabled {
		requestBody, _ := common.GetRequestBody(c)
		logger.Debugf(ctx, "request body: %s", string(requestBody))
	}
	channelId := c.GetInt(ctxkey.ChannelId)
	userId := c.GetInt(ctxkey.Id)
	bizErr := relayHelper(c, relayMode)
	if bizErr == nil {
		monitor.Emit(channelId, true)
		return
	}
	lastFailedChannelId := channelId
	channelName := c.GetString(ctxkey.ChannelName)
	group := c.GetString(ctxkey.Group)
	originalModel := c.GetString(ctxkey.OriginalModel)
	go processChannelRelayError(ctx, userId, channelId, channelName, *bizErr)
	requestId := c.GetString(helper.RequestIdKey)
	retryTimes := config.RetryTimes
	if !shouldRetry(c, bizErr) {
		logger.Errorf(ctx, "relay error happen, status code is %d, won't retry in this case", bizErr.StatusCode)
		retryTimes = 0
	}
	for i := retryTimes; i > 0; i-- {
		channel, err := dbmodel.CacheGetRandomSatisfiedChannel(group, originalModel, i != retryTimes)
		if err != nil {
			logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %+v", err)
			break
		}
		logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.Id, i)
		if channel.Id == lastFailedChannelId {
			continue
		}
		middleware.SetupContextForSelectedChannel(c, channel, originalModel)
		requestBody, err := common.GetRequestBody(c)
		c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
		bizErr = relayHelper(c, relayMode)
		if bizErr == nil {
			return
		}
		channelId := c.GetInt(ctxkey.ChannelId)
		lastFailedChannelId = channelId
		channelName := c.GetString(ctxkey.ChannelName)
		go processChannelRelayError(ctx, userId, channelId, channelName, *bizErr)
	}
	if bizErr != nil {
		if bizErr.StatusCode == http.StatusTooManyRequests {
			bizErr.Error.Message = "当前分组上游负载已饱和，请稍后再试"
		}

		// 退款收口（2026-07）：preCheck 成功（金额已冻结）的请求，只有走到
		// 最终失败才在这里退一次款；重试的中间失败不退（重试复用冻结）。
		// 这样 ruoyi 对同一 requestId 只会看到一种终态回调（failed 或 succeeded），
		// 其"按 requestId 幂等"的模型不会被 failed→succeeded 序列打破
		// （failed 先落地会把成功的结算拦截掉 = 收不到钱）。
		// 视频链路不走这里：Seedance 不重试，其回调由 RelayVideoHelper/调度器负责。
		if c.GetBool(controller.CtxkeyBillingPreCheckDone) {
			tenantID := c.GetString("username")
			if tenantID == "" {
				tenantID = fmt.Sprintf("%d", userId)
			}
			modelName := originalModel
			if m := meta.GetByContext(c); m != nil && m.ActualModelName != "" {
				modelName = m.ActualModelName
			}
			billing.EnqueueCallback(&billing.CallbackReq{
				RequestID: requestId,
				TenantID:  tenantID,
				Model:     modelName,
				Status:    "failed",
				TaskType:  "chat",
				ErrorMsg:  errfilter.Sanitize(bizErr.Error.Message),
			})
		}

		// 错误透出前的统一兜底过滤：底层 BaseUrl（渠道/ruoyi/上游域名）
		// 不允许出现在给客户端的错误信息里，scheme://host 剔除、path 保留。
		// 先把本次渠道的 baseURL 注册进过滤集（渠道配置在 DB，无法做成常量），
		// 这样即使某条错误在产生处没过滤，到这里也会被兜底。
		if m := meta.GetByContext(c); m != nil {
			errfilter.RegisterBaseURL(m.BaseURL)
		}
		bizErr.Error.Message = errfilter.Sanitize(bizErr.Error.Message)

		bizErr.Error.Message = helper.MessageWithRequestId(bizErr.Error.Message, requestId)
		c.JSON(bizErr.StatusCode, gin.H{
			"error": bizErr.Error,
		})
	}
}

func shouldRetry(c *gin.Context, bizErr *model.ErrorWithStatusCode) bool {
	if _, ok := c.Get(ctxkey.SpecificChannelId); ok {
		return false
	}
	//Seedance 视频任务不重试（重试会重复冻结金额、重复创建上游任务）
	channelType := c.GetInt(ctxkey.Channel)
	if channelType == channeltype.Seedance {
		return false
	}
	// 计费类错误不重试：不是渠道故障，换渠道/重试都解决不了，
	// 只会放大 preCheck 压力和退款回调噪音
	if bizErr != nil {
		if code, ok := bizErr.Error.Code.(string); ok {
			switch code {
			case "billing_error", "pre_check_failed", "pre_check_denied",
				"insufficient_balance", "account_frozen", "wallet_disabled":
				return false
			}
		}
	}
	statusCode := bizErr.StatusCode
	if statusCode == http.StatusTooManyRequests {
		return true
	}
	if statusCode/100 == 5 {
		return true
	}
	if statusCode == http.StatusBadRequest {
		return false
	}
	if statusCode/100 == 2 {
		return false
	}
	return true
}

func processChannelRelayError(ctx context.Context, userId int, channelId int, channelName string, err model.ErrorWithStatusCode) {
	logger.Errorf(ctx, "relay error (channel id %d, user id: %d): %s", channelId, userId, err.Message)
	// https://platform.openai.com/docs/guides/error-codes/api-errors
	if monitor.ShouldDisableChannel(&err.Error, err.StatusCode) {
		monitor.DisableChannel(channelId, channelName, err.Message)
	} else {
		monitor.Emit(channelId, false)
	}
}

func RelayNotImplemented(c *gin.Context) {
	err := model.Error{
		Message: "API not implemented",
		Type:    "one_api_error",
		Param:   "",
		Code:    "api_not_implemented",
	}
	c.JSON(http.StatusNotImplemented, gin.H{
		"error": err,
	})
}

func RelayNotFound(c *gin.Context) {
	err := model.Error{
		Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path),
		Type:    "invalid_request_error",
		Param:   "",
		Code:    "",
	}
	c.JSON(http.StatusNotFound, gin.H{
		"error": err,
	})
}

// QuerySeedanceTask 查询视频任务状态。
// 安全要求：必须校验任务归属，只允许查询自己的任务；
// 找不到与无权访问返回同样的 404，避免泄露任务是否存在。
func QuerySeedanceTask(c *gin.Context) {
	taskID := c.Query("id")
	if taskID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
		return
	}

	userID := c.GetInt(ctxkey.Id)
	result, ok := seedance.GetTaskStatusForUser(taskID, userID)
	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	c.JSON(http.StatusOK, result)
}