package service

import (
	"context"
	"errors"
	"io"
	"math/rand"
	"time"

	"gorm.io/gorm"

	"oci-portal/internal/aiwire"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

// ChatMeta 是一次网关调用的路由结果,供 API 层写调用日志。
type ChatMeta struct {
	ChannelID   uint
	ChannelName string
	Retries     int
}

// aiCandidate 是某模型的一个可用渠道及其区域内模型 OCID。
type aiCandidate struct {
	ch        model.AiChannel
	modelOcid string
}

// RespPassthrough 编排一次非流式直通调用:选渠道(priority→加权随机)→ 调用 →
// 可重试错误换渠道(整请求上限 3 次)并维护熔断;group 非空时只在同分组渠道内路由。
// 上游为 OpenAI-compatible /actions/v1/responses(实测可用,无 Oracle 文档合同)。
func (s *AiGatewayService) RespPassthrough(ctx context.Context, raw []byte, modelName, group string) ([]byte, ChatMeta, error) {
	meta := ChatMeta{}
	excluded := map[uint]bool{}
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		cand, err := s.pick(ctx, modelName, group, "CHAT", excluded)
		if err != nil {
			return nil, meta, firstErr(lastErr, err)
		}
		meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
		payload, err := s.passthroughOnce(ctx, cand, raw)
		if err == nil {
			s.markSuccess(ctx, cand.ch.ID)
			return payload, meta, nil
		}
		retry, penalize := switchable(err)
		if !retry {
			return nil, meta, err
		}
		if penalize {
			s.markFailure(ctx, cand.ch.ID)
		}
		excluded[cand.ch.ID] = true
		meta.Retries++
		lastErr = err
	}
	return nil, meta, lastErr
}

func (s *AiGatewayService) passthroughOnce(ctx context.Context, cand *aiCandidate, raw []byte) ([]byte, error) {
	cred, err := s.configs.credentialsByID(ctx, cand.ch.OciConfigID)
	if err != nil {
		return nil, err
	}
	return s.client.GenAiCompatResponses(ctx, cred, cand.ch.Region, raw, s.UpstreamWait())
}

// RespPassthroughStream 编排流式直通:流建立成功即绑定渠道,建立失败按 switchable
// 换渠道重试;建立后的中断不重试、不计熔断(与 OpenStream 语义一致)。
func (s *AiGatewayService) RespPassthroughStream(ctx context.Context, raw []byte, modelName, group string) (io.ReadCloser, ChatMeta, error) {
	meta := ChatMeta{}
	excluded := map[uint]bool{}
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		cand, err := s.pick(ctx, modelName, group, "CHAT", excluded)
		if err != nil {
			return nil, meta, firstErr(lastErr, err)
		}
		meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
		cred, err := s.configs.credentialsByID(ctx, cand.ch.OciConfigID)
		if err != nil {
			return nil, meta, err
		}
		stream, err := s.client.GenAiCompatResponsesStream(ctx, cred, cand.ch.Region, raw, s.UpstreamWait())
		if err == nil {
			s.markSuccess(ctx, cand.ch.ID)
			return stream, meta, nil
		}
		retry, penalize := switchable(err)
		if !retry {
			return nil, meta, err
		}
		if penalize {
			s.markFailure(ctx, cand.ch.ID)
		}
		excluded[cand.ch.ID] = true
		meta.Retries++
		lastErr = err
	}
	return nil, meta, lastErr
}

// firstErr 在换渠道后仍失败时优先返回上游错误(而非「无渠道」)。
func firstErr(lastErr, pickErr error) error {
	if lastErr != nil && errors.Is(pickErr, ErrAiNoChannel) {
		return lastErr
	}
	return pickErr
}

// switchable 判断调用失败是否换渠道重试、是否计入熔断:模型级不可用(微调基座
// 400 / 实体不存在 404)换渠道但不计熔断——这是模型×区域供给问题而非渠道健康
// 问题,持久解决靠加入模型黑名单;429 / 5xx / 网络错误换渠道并计熔断;其余 4xx 直接透传。
func switchable(err error) (retry, penalize bool) {
	if oci.IsModelUnavailable(err) {
		return true, false
	}
	if status, ok := oci.ServiceStatus(err); ok {
		return status == 429 || status >= 500, true
	}
	return true, true
}

// pick 选出支持该模型的最优渠道:能力匹配 → 启用 → 分组匹配 → 未熔断 → 最小优先级组 → 加权随机。
func (s *AiGatewayService) pick(ctx context.Context, modelName, group, capability string, excluded map[uint]bool) (*aiCandidate, error) {
	ocids, channelIDs, err := s.modelChannels(ctx, modelName, capability)
	if err != nil {
		return nil, err
	}
	if len(channelIDs) == 0 {
		return nil, ErrAiUnknownModel
	}
	q := s.db.WithContext(ctx).Where("id IN ? AND enabled = ?", channelIDs, true)
	if group != "" {
		q = q.Where("channel_group = ?", group)
	}
	var channels []model.AiChannel
	if err := q.Find(&channels).Error; err != nil {
		return nil, err
	}
	now := time.Now()
	avail := channels[:0]
	for _, ch := range channels {
		if excluded[ch.ID] || (ch.DisabledUntil != nil && ch.DisabledUntil.After(now)) {
			continue
		}
		avail = append(avail, ch)
	}
	if len(avail) == 0 {
		return nil, ErrAiNoChannel
	}
	chosen := weightedPick(topPriority(avail))
	return &aiCandidate{ch: chosen, modelOcid: ocids[chosen.ID]}, nil
}

// modelChannels 查出提供该模型的渠道 ID 及各自的模型 OCID;
// capability=CHAT 时兼容存量空串(加列前只同步对话模型)。
func (s *AiGatewayService) modelChannels(ctx context.Context, modelName, capability string) (map[uint]string, []uint, error) {
	q := s.db.WithContext(ctx).Where("name = ?", modelName)
	if capability == "CHAT" {
		q = q.Where("capability IN ?", []string{"CHAT", ""})
	} else {
		q = q.Where("capability = ?", capability)
	}
	if s.FilterDeprecated() {
		q = q.Where("deprecated_at IS NULL")
	}
	var rows []model.AiModelCache
	if err := q.Find(&rows).Error; err != nil {
		return nil, nil, err
	}
	ocids := make(map[uint]string, len(rows))
	ids := make([]uint, 0, len(rows))
	for _, r := range rows {
		ocids[r.ChannelID] = r.ModelOcid
		ids = append(ids, r.ChannelID)
	}
	return ocids, ids, nil
}

// topPriority 保留最小 Priority 值的渠道组。
func topPriority(chs []model.AiChannel) []model.AiChannel {
	best := chs[0].Priority
	for _, ch := range chs[1:] {
		if ch.Priority < best {
			best = ch.Priority
		}
	}
	out := chs[:0]
	for _, ch := range chs {
		if ch.Priority == best {
			out = append(out, ch)
		}
	}
	return out
}

// weightedPick 组内按权重随机;权重全部非正时等权。
func weightedPick(chs []model.AiChannel) model.AiChannel {
	total := 0
	for _, ch := range chs {
		if ch.Weight > 0 {
			total += ch.Weight
		}
	}
	if total <= 0 {
		return chs[rand.Intn(len(chs))]
	}
	r := rand.Intn(total)
	for _, ch := range chs {
		if ch.Weight <= 0 {
			continue
		}
		r -= ch.Weight
		if r < 0 {
			return ch
		}
	}
	return chs[len(chs)-1]
}

// Embeddings 编排向量化调用:按 EMBEDDING 能力选渠道,可重试错误换渠道(整请求上限 3 次)。
func (s *AiGatewayService) Embeddings(ctx context.Context, req aiwire.EmbeddingsRequest, group string) (*aiwire.EmbeddingsResponse, ChatMeta, error) {
	meta := ChatMeta{}
	excluded := map[uint]bool{}
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		cand, err := s.pick(ctx, req.Model, group, "EMBEDDING", excluded)
		if err != nil {
			return nil, meta, firstErr(lastErr, err)
		}
		meta.ChannelID, meta.ChannelName = cand.ch.ID, cand.ch.Name
		resp, err := s.embedOnce(ctx, cand, req)
		if err == nil {
			s.markSuccess(ctx, cand.ch.ID)
			return resp, meta, nil
		}
		retry, penalize := switchable(err)
		if !retry {
			return nil, meta, err
		}
		if penalize {
			s.markFailure(ctx, cand.ch.ID)
		}
		excluded[cand.ch.ID] = true
		meta.Retries++
		lastErr = err
	}
	return nil, meta, lastErr
}

// embedOnce 调用渠道向量化并装配 OpenAI 形态响应。
func (s *AiGatewayService) embedOnce(ctx context.Context, cand *aiCandidate, req aiwire.EmbeddingsRequest) (*aiwire.EmbeddingsResponse, error) {
	cred, err := s.configs.credentialsByID(ctx, cand.ch.OciConfigID)
	if err != nil {
		return nil, err
	}
	vecs, usage, err := s.client.GenAiEmbed(ctx, cred, cand.ch.Region, cand.modelOcid, req.Input, req.Dimensions)
	if err != nil {
		return nil, err
	}
	out := &aiwire.EmbeddingsResponse{Object: "list", Model: req.Model, Data: make([]aiwire.Embedding, 0, len(vecs))}
	for i, v := range vecs {
		out.Data = append(out.Data, aiwire.Embedding{Object: "embedding", Index: i, Embedding: v})
	}
	if usage != nil {
		out.Usage = &aiwire.EmbedUsage{PromptTokens: usage.PromptTokens, TotalTokens: usage.TotalTokens}
	}
	return out, nil
}

// markSuccess 复位失败计数与熔断窗口(无异常状态时零写库)。
func (s *AiGatewayService) markSuccess(ctx context.Context, id uint) {
	s.db.WithContext(ctx).Model(&model.AiChannel{}).
		Where("id = ? AND (fail_count > 0 OR disabled_until IS NOT NULL)", id).
		Updates(map[string]any{"fail_count": 0, "disabled_until": gorm.Expr("NULL")})
}

// markFailure 递增失败计数;达到阈值后按 2^(超出次数) 分钟指数退避,封顶 30 分钟。
func (s *AiGatewayService) markFailure(ctx context.Context, id uint) {
	s.db.WithContext(ctx).Model(&model.AiChannel{}).Where("id = ?", id).
		Update("fail_count", gorm.Expr("fail_count + 1"))
	var ch model.AiChannel
	if err := s.db.WithContext(ctx).First(&ch, id).Error; err != nil {
		return
	}
	if ch.FailCount < aiFailThreshold {
		return
	}
	n := ch.FailCount - aiFailThreshold
	if n > 10 {
		n = 10
	}
	backoff := time.Duration(1<<uint(n)) * time.Minute
	if backoff > aiBackoffCap {
		backoff = aiBackoffCap
	}
	until := time.Now().Add(backoff)
	s.db.WithContext(ctx).Model(&model.AiChannel{}).Where("id = ?", id).Update("disabled_until", until)
}
