package service

import (
	"context"
	"encoding/json"
	"errors"
	"strings"

	"backend/internal/model"
	"backend/internal/repo"
)

type UserGenerationService struct {
	v1     *V1Service
	events *repo.EventRepository
	users  *repo.UserRepository
	models *repo.ModelRepository
}

func NewUserGenerationService(v1 *V1Service, events *repo.EventRepository, users *repo.UserRepository, models *repo.ModelRepository) *UserGenerationService {
	return &UserGenerationService{
		v1:     v1,
		events: events,
		users:  users,
		models: models,
	}
}

type UserGenerateRequest struct {
	Model           string
	Prompt          string
	Ratio           string
	Resolution      string
	Duration        string
	ReferenceImages []string
}

func (s *UserGenerationService) Generate(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
	if user == nil || strings.TrimSpace(user.ID) == "" {
		return nil, errors.New("未登录或会话已过期")
	}
	// No single-job lock anymore — concurrent generations are allowed, capped by
	// the user's concurrency group (enforced in prepareImageExecution/Video).

	modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model))
	if err != nil {
		return nil, ErrUnknownModel
	}

	principal := &APIPrincipal{
		User:      user,
		TokenType: "session",
	}

	switch modelItem.Type {
	case "video":
		resp, err := s.v1.prepareSessionVideo(ctx, principal, V1VideoRequest{
			Model:           in.Model,
			Prompt:          in.Prompt,
			Duration:        in.Duration,
			AspectRatio:     in.Ratio,
			Resolution:      in.Resolution,
			ReferenceImages: in.ReferenceImages,
		})
		if err != nil {
			return nil, err
		}
		return resp, nil
	default:
		resp, err := s.v1.prepareSessionImage(ctx, principal, V1ImageRequest{
			Model:           in.Model,
			Prompt:          in.Prompt,
			AspectRatio:     in.Ratio,
			Resolution:      in.Resolution,
			ReferenceImages: in.ReferenceImages,
		})
		if err != nil {
			return nil, err
		}
		return resp, nil
	}
}

func (s *UserGenerationService) AdminTest(ctx context.Context, user *model.User, in UserGenerateRequest) (map[string]any, error) {
	if user == nil || strings.TrimSpace(user.ID) == "" {
		return nil, errors.New("未登录或会话已过期")
	}
	modelItem, err := s.models.Get(ctx, strings.TrimSpace(in.Model))
	if err != nil {
		return nil, ErrUnknownModel
	}
	principal := &APIPrincipal{
		User:      user,
		TokenType: "session",
	}
	switch modelItem.Type {
	case "video":
		return s.v1.prepareAdminTestVideo(ctx, principal, V1VideoRequest{
			Model:           in.Model,
			Prompt:          in.Prompt,
			Duration:        in.Duration,
			AspectRatio:     in.Ratio,
			Resolution:      in.Resolution,
			ReferenceImages: in.ReferenceImages,
		})
	default:
		return s.v1.prepareAdminTestImage(ctx, principal, V1ImageRequest{
			Model:           in.Model,
			Prompt:          in.Prompt,
			AspectRatio:     in.Ratio,
			Resolution:      in.Resolution,
			ReferenceImages: in.ReferenceImages,
		})
	}
}

func (s *UserGenerationService) MyJobs(ctx context.Context, user *model.User, source string) (map[string]any, error) {
	if user == nil || strings.TrimSpace(user.ID) == "" {
		return map[string]any{"pending": nil, "latest": nil}, nil
	}
	// source scopes the lookup: "user" = 画图台(默认),"admin" = 后台测试模型。
	// Both are this caller's own events; the admin-test poll uses "admin" so a
	// gateway-timed-out (524) test can still recover its result.
	if source != "admin" {
		source = "user"
	}
	modelNames, err := s.ModelNameMap(ctx)
	if err != nil {
		return nil, err
	}
	pending, err := s.events.PendingByUser(ctx, user.ID, source)
	if err != nil {
		return nil, err
	}
	latest, err := s.events.LatestByUser(ctx, user.ID, source)
	if err != nil {
		return nil, err
	}
	return map[string]any{
		"pending": shapeJobEvent(pending, modelNames),
		"latest":  shapeJobEvent(latest, modelNames),
	}, nil
}

func (s *UserGenerationService) ModelNameMap(ctx context.Context) (map[string]string, error) {
	return s.models.NameMap(ctx)
}

func shapeJobEvent(item *model.EventLog, modelNames map[string]string) map[string]any {
	if item == nil {
		return nil
	}
	status := item.Status
	url := ""
	if strings.TrimSpace(item.File) != "" {
		url = "/images/" + strings.ReplaceAll(strings.TrimSpace(item.File), "\\", "/")
	}
	return map[string]any{
		"id":             item.ID,
		"kind":           item.Kind,
		"model":          displayModelName(modelNames, item.Model),
		"prompt":         item.Prompt,
		"ratio":          item.Ratio,
		"resolution":     item.Resolution,
		"duration":       item.Duration,
		"status":         status,
		"file":           emptyOrNil(item.File),
		"url":            emptyOrNil(url),
		"reference_urls": referenceURLs(item.RefFiles),
		"elapsed_ms":     item.ElapsedMS,
		"error":          emptyOrNil(item.Error),
		"charged":        item.Cost,
		"cost":           item.Cost,
		"ts":             item.TS.Unix(),
	}
}

func displayModelName(modelNames map[string]string, raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if modelNames != nil {
		if name, ok := modelNames[raw]; ok && strings.TrimSpace(name) != "" {
			return name
		}
	}
	return raw
}

// referenceURLs turns the stored relative reference paths into /images URLs so
// the playground can re-display the uploaded reference image(s) after a reload.
func referenceURLs(raw []byte) []string {
	if len(raw) == 0 {
		return []string{}
	}
	var paths []string
	if err := json.Unmarshal(raw, &paths); err != nil {
		return []string{}
	}
	out := make([]string, 0, len(paths))
	for _, p := range paths {
		p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
		if p != "" {
			out = append(out, "/images/"+p)
		}
	}
	return out
}

func emptyOrNil(v string) any {
	if strings.TrimSpace(v) == "" {
		return nil
	}
	return v
}
