package httpapi

import (
	"context"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
	"github.com/jackc/pgx/v5/pgxpool"
)

type modelAccessFixture struct {
	ID        string   `json:"id"`
	ModelName string   `json:"modelName"`
	ModelType []string `json:"modelType"`
}

type modelAccessRuleDiagnostic struct {
	RuleID     string `json:"ruleId"`
	ResourceID string `json:"resourceId"`
	Effective  bool   `json:"effective"`
	Reason     string `json:"reason"`
}

type modelAccessAssignableResponse struct {
	Items           []modelAccessFixture        `json:"items"`
	RuleDiagnostics []modelAccessRuleDiagnostic `json:"ruleDiagnostics"`
}

func TestAPIKeyModelAccessUsesGroupKeyAndScopeLayers(t *testing.T) {
	databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
	if databaseURL == "" {
		t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run the API key model access integration flow")
	}
	ctx := context.Background()
	applyMigration(t, ctx, databaseURL)
	db, err := store.Connect(ctx, databaseURL)
	if err != nil {
		t.Fatalf("connect store: %v", err)
	}
	defer db.Close()

	serverCtx, cancelServer := context.WithCancel(ctx)
	defer cancelServer()
	server := httptest.NewServer(NewServerWithContext(serverCtx, config.Config{
		AppEnv:            "test",
		HTTPAddr:          ":0",
		DatabaseURL:       databaseURL,
		IdentityMode:      "hybrid",
		JWTSecret:         "test-secret",
		CORSAllowedOrigin: "*",
	}, db, slog.New(slog.NewTextHandler(io.Discard, nil))))
	defer server.Close()

	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatalf("connect test pool: %v", err)
	}
	defer pool.Close()

	suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
	register := func(prefix string) (string, string) {
		t.Helper()
		username := prefix + "_" + suffix
		password := "password123"
		var registered struct {
			AccessToken string `json:"accessToken"`
		}
		doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/register", "", map[string]any{
			"username": username,
			"email":    username + "@example.com",
			"password": password,
		}, http.StatusCreated, &registered)
		return username, password
	}
	login := func(username string, password string) string {
		t.Helper()
		var response struct {
			AccessToken string `json:"accessToken"`
		}
		doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/login", "", map[string]any{
			"account": username, "password": password,
		}, http.StatusOK, &response)
		return response.AccessToken
	}
	adminName, adminPassword := register("layered_admin")
	userBName, userBPassword := register("layered_user_b")
	if _, err := pool.Exec(ctx, `UPDATE gateway_users SET roles = '["admin"]'::jsonb WHERE username = $1`, adminName); err != nil {
		t.Fatalf("promote admin: %v", err)
	}
	adminToken := login(adminName, adminPassword)

	createGroup := func(key string) string {
		t.Helper()
		var group struct {
			ID string `json:"id"`
		}
		doJSON(t, server.URL, http.MethodPost, "/api/admin/user-groups", adminToken, map[string]any{
			"groupKey": key + "-" + suffix,
			"name":     key,
			"source":   "gateway",
			"status":   "active",
		}, http.StatusCreated, &group)
		return group.ID
	}
	groupAID := createGroup("layered-group-a")
	groupBID := createGroup("layered-group-b")
	if _, err := pool.Exec(ctx, `UPDATE gateway_users SET default_user_group_id = $1::uuid WHERE username = $2`, groupAID, adminName); err != nil {
		t.Fatalf("assign group A: %v", err)
	}
	if _, err := pool.Exec(ctx, `UPDATE gateway_users SET default_user_group_id = $1::uuid WHERE username = $2`, groupBID, userBName); err != nil {
		t.Fatalf("assign group B: %v", err)
	}
	adminToken = login(adminName, adminPassword)
	userBToken := login(userBName, userBPassword)

	createKey := func(token string, name string, scopes []string) (string, string) {
		t.Helper()
		var created struct {
			APIKey struct {
				ID string `json:"id"`
			} `json:"apiKey"`
			Secret string `json:"secret"`
		}
		doJSON(t, server.URL, http.MethodPost, "/api/v1/api-keys", token, map[string]any{
			"name": name, "scopes": scopes,
		}, http.StatusCreated, &created)
		return created.APIKey.ID, created.Secret
	}
	keyAID, keyASecret := createKey(adminToken, "group A image key", []string{"image"})
	keyAInheritedID, keyAInheritedSecret := createKey(adminToken, "group A inherited image key", []string{"image"})
	keyMultiID, keyMultiSecret := createKey(adminToken, "group A multi-source image key", []string{"image"})
	keyScopeID, _ := createKey(adminToken, "group A narrowed scope key", []string{"all"})
	keyLegacyID, _ := createKey(adminToken, "group A narrowed group key", []string{"all"})
	keyBID, keyBSecret := createKey(userBToken, "group B chat key", []string{"chat"})
	keyBImageID, keyBImageSecret := createKey(userBToken, "group B image key", []string{"image"})

	createSimulationBaseModel(t, server.URL, adminToken, "layered-text-"+suffix, []string{"text_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-image-"+suffix, []string{"image_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-multi-"+suffix, []string{"text_generate", "image_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-key-deny-image-"+suffix, []string{"image_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-group-b-image-"+suffix, []string{"image_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-scope-text-"+suffix, []string{"text_generate"})
	createSimulationBaseModel(t, server.URL, adminToken, "layered-legacy-text-"+suffix, []string{"text_generate"})

	createPlatform := func(key string, status string) string {
		t.Helper()
		var platform struct {
			ID string `json:"id"`
		}
		doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", adminToken, map[string]any{
			"provider":    "openai",
			"platformKey": key + "-" + suffix,
			"name":        key,
			"baseUrl":     "https://api.openai.com/v1",
			"authType":    "bearer",
			"credentials": map[string]any{"mode": "simulation"},
			"config":      map[string]any{"testMode": true},
			"status":      status,
		}, http.StatusCreated, &platform)
		return platform.ID
	}
	enabledPlatformID := createPlatform("layered-enabled", "enabled")
	secondPlatformID := createPlatform("layered-second", "enabled")
	disabledPlatformID := createPlatform("layered-disabled", "disabled")

	createModel := func(platformID string, modelName string, modelTypes []string) modelAccessFixture {
		t.Helper()
		var model modelAccessFixture
		doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms/"+platformID+"/models", adminToken, map[string]any{
			"canonicalModelKey": "openai:" + modelName,
			"modelName":         modelName,
			"modelAlias":        modelName,
			"modelType":         modelTypes,
			"displayName":       modelName,
		}, http.StatusCreated, &model)
		return model
	}
	textName := "layered-text-" + suffix
	imageName := "layered-image-" + suffix
	multiName := "layered-multi-" + suffix
	keyDenyImageName := "layered-key-deny-image-" + suffix
	groupBImageName := "layered-group-b-image-" + suffix
	scopeTextName := "layered-scope-text-" + suffix
	legacyTextName := "layered-legacy-text-" + suffix
	textModel := createModel(enabledPlatformID, textName, []string{"text_generate"})
	imageModel := createModel(enabledPlatformID, imageName, []string{"image_generate"})
	multiModel := createModel(enabledPlatformID, multiName, []string{"text_generate", "image_generate"})
	multiSecondModel := createModel(secondPlatformID, multiName, []string{"text_generate", "image_generate"})
	disabledImageModel := createModel(disabledPlatformID, imageName, []string{"image_generate"})
	keyDenyImageModel := createModel(enabledPlatformID, keyDenyImageName, []string{"image_generate"})
	groupBImageModel := createModel(enabledPlatformID, groupBImageName, []string{"image_generate"})
	scopeTextModel := createModel(enabledPlatformID, scopeTextName, []string{"text_generate"})
	legacyTextModel := createModel(enabledPlatformID, legacyTextName, []string{"text_generate"})

	createRule := func(subjectType string, subjectID string, resourceID string, effect string) {
		t.Helper()
		doJSON(t, server.URL, http.MethodPost, "/api/admin/access-rules", adminToken, map[string]any{
			"subjectType": subjectType, "subjectId": subjectID,
			"resourceType": "platform_model", "resourceId": resourceID,
			"effect": effect, "priority": 10, "status": "active",
		}, http.StatusCreated, nil)
	}
	batchKeyRule := func(ownerToken string, keyID string, resourceID string, effect string, expectedStatus int) {
		t.Helper()
		doJSON(t, server.URL, http.MethodPost, "/api/v1/api-keys/access-rules/batch", ownerToken, map[string]any{
			"subjectType": "api_key", "subjectId": keyID, "effect": effect,
			"upsertResources": []map[string]any{{"resourceType": "platform_model", "resourceId": resourceID, "status": "active"}},
			"deleteResources": []map[string]any{},
		}, expectedStatus, nil)
	}
	// Create valid legacy rules first, then narrow the group or scope. The rows
	// must remain for diagnostics while becoming ineffective globally.
	batchKeyRule(adminToken, keyLegacyID, legacyTextModel.ID, "allow", http.StatusOK)
	batchKeyRule(adminToken, keyScopeID, scopeTextModel.ID, "allow", http.StatusOK)
	createRule("user_group", groupAID, imageModel.ID, "allow")
	createRule("user_group", groupAID, multiModel.ID, "allow")
	createRule("user_group", groupAID, multiSecondModel.ID, "allow")
	createRule("user_group", groupAID, keyDenyImageModel.ID, "allow")
	createRule("user_group", groupAID, scopeTextModel.ID, "allow")
	createRule("user_group", groupAID, textModel.ID, "deny")
	createRule("user_group", groupAID, legacyTextModel.ID, "deny")
	createRule("user_group", groupBID, groupBImageModel.ID, "allow")
	createRule("user_group", groupBID, textModel.ID, "allow")
	createRule("user_group", groupBID, scopeTextModel.ID, "allow")
	createRule("user_group", groupBID, legacyTextModel.ID, "allow")
	doJSON(t, server.URL, http.MethodPatch, "/api/v1/api-keys/"+keyScopeID+"/scopes", adminToken, map[string]any{
		"scopes": []string{"image"},
	}, http.StatusOK, nil)
	// Admin-created historical rules can refer to resources that are no longer
	// assignable. They are retained but cannot expand or reserve the resource.
	createRule("api_key", keyAID, textModel.ID, "allow")
	createRule("api_key", keyScopeID, disabledImageModel.ID, "allow")
	batchKeyRule(adminToken, keyAID, imageModel.ID, "allow", http.StatusOK)
	batchKeyRule(adminToken, keyAID, keyDenyImageModel.ID, "allow", http.StatusOK)
	batchKeyRule(adminToken, keyAID, keyDenyImageModel.ID, "deny", http.StatusOK)

	loadAssignable := func(token string, keyID string) modelAccessAssignableResponse {
		t.Helper()
		var response modelAccessAssignableResponse
		doJSON(t, server.URL, http.MethodGet, "/api/v1/api-keys/"+keyID+"/assignable-models", token, nil, http.StatusOK, &response)
		return response
	}
	assignable := loadAssignable(adminToken, keyAID)
	if containsModelID(assignable.Items, textModel.ID) {
		t.Fatalf("group-denied text model leaked into key candidates: %+v", assignable.Items)
	}
	if !containsModelID(assignable.Items, imageModel.ID) || !containsModelID(assignable.Items, multiModel.ID) {
		t.Fatalf("image-scope key candidates missing allowed models: %+v", assignable.Items)
	}
	if !containsModelID(assignable.Items, keyDenyImageModel.ID) {
		t.Fatalf("key rules incorrectly removed a model from the assignable set: %+v", assignable.Items)
	}
	if containsModelID(assignable.Items, disabledImageModel.ID) || containsModelID(assignable.Items, groupBImageModel.ID) {
		t.Fatalf("disabled or group-A-whitelist-excluded source leaked into assignable models: %+v", assignable.Items)
	}
	for _, model := range assignable.Items {
		if model.ID == multiModel.ID && (len(model.ModelType) != 1 || model.ModelType[0] != "image_generate") {
			t.Fatalf("multi-capability model was not scope-pruned: %+v", model)
		}
	}
	foundRevoked := false
	for _, diagnostic := range assignable.RuleDiagnostics {
		if !diagnostic.Effective && diagnostic.Reason == "owner_access_revoked" {
			foundRevoked = true
		}
	}
	if !foundRevoked {
		t.Fatalf("stale key rule diagnostic missing: %+v", assignable.RuleDiagnostics)
	}
	scopeDiagnostics := loadAssignable(adminToken, keyScopeID).RuleDiagnostics
	if !containsDiagnosticReason(scopeDiagnostics, "scope_not_allowed") || !containsDiagnosticReason(scopeDiagnostics, "resource_unavailable") {
		t.Fatalf("scope/resource diagnostics missing after narrowing: %+v", scopeDiagnostics)
	}
	legacyDiagnostics := loadAssignable(adminToken, keyLegacyID).RuleDiagnostics
	if !containsDiagnosticReason(legacyDiagnostics, "owner_access_revoked") {
		t.Fatalf("group-narrowed rule diagnostic missing: %+v", legacyDiagnostics)
	}
	t.Logf("候选与诊断：keyA=%d models, diagnostics=%d; scope=%v; legacy=%v", len(assignable.Items), len(assignable.RuleDiagnostics), diagnosticReasons(scopeDiagnostics), diagnosticReasons(legacyDiagnostics))

	// The regular key rule endpoint must reject the same group-denied resource.
	doJSON(t, server.URL, http.MethodPost, "/api/v1/api-keys/access-rules/batch", adminToken, map[string]any{
		"subjectType": "api_key", "subjectId": keyAID, "effect": "allow",
		"upsertResources": []map[string]any{{"resourceType": "platform_model", "resourceId": textModel.ID, "status": "active"}},
		"deleteResources": []map[string]any{},
	}, http.StatusForbidden, nil)

	var platformModels struct {
		Items []modelAccessFixture `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/v1/platform-models", keyASecret, nil, http.StatusOK, &platformModels)
	if containsModelID(platformModels.Items, textModel.ID) {
		t.Fatalf("group-denied text model leaked into key model list: %+v", platformModels.Items)
	}
	if !containsModelID(platformModels.Items, imageModel.ID) || containsModelID(platformModels.Items, multiModel.ID) || containsModelID(platformModels.Items, keyDenyImageModel.ID) || containsModelID(platformModels.Items, disabledImageModel.ID) {
		t.Fatalf("key allow/deny or global availability was not reflected in rich list: %+v", platformModels.Items)
	}
	if !containsModelID(loadAssignable(adminToken, keyAInheritedID).Items, imageModel.ID) {
		t.Fatalf("assignable list must ignore another key's whitelist")
	}
	var inheritedPlatformModels struct {
		Items []modelAccessFixture `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/v1/platform-models", keyAInheritedSecret, nil, http.StatusOK, &inheritedPlatformModels)
	if !containsModelID(inheritedPlatformModels.Items, imageModel.ID) || !containsModelID(inheritedPlatformModels.Items, keyDenyImageModel.ID) || !containsModelID(inheritedPlatformModels.Items, multiModel.ID) {
		t.Fatalf("key whitelist/deny isolation mismatch for sibling key: %+v", inheritedPlatformModels.Items)
	}
	groupBAssignable := loadAssignable(userBToken, keyBImageID)
	if containsModelID(groupBAssignable.Items, imageModel.ID) || !containsModelID(groupBAssignable.Items, groupBImageModel.ID) {
		t.Fatalf("group whitelist rules were not applied to key candidates: %+v", groupBAssignable.Items)
	}
	var groupBChatModels struct {
		Items []modelAccessFixture `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/v1/platform-models", keyBSecret, nil, http.StatusOK, &groupBChatModels)
	for _, model := range []modelAccessFixture{textModel, scopeTextModel, legacyTextModel} {
		if !containsModelID(groupBChatModels.Items, model.ID) {
			t.Fatalf("stale key/group-A rule blocked group B model %s: %+v", model.ID, groupBChatModels.Items)
		}
	}
	t.Logf("用户组与 KEY 隔离：keyA rich=%d, sibling rich=%d, groupB image candidates=%d", len(platformModels.Items), len(inheritedPlatformModels.Items), len(groupBAssignable.Items))

	var openAIList struct {
		Object string `json:"object"`
		Data   []struct {
			ID string `json:"id"`
		} `json:"data"`
	}
	doJSON(t, server.URL, http.MethodGet, "/v1/models", keyAInheritedSecret, nil, http.StatusOK, &openAIList)
	if openAIList.Object != "list" || countOpenAIModel(openAIList.Data, multiName) != 1 {
		t.Fatalf("openai model list is not deduplicated: %+v", openAIList)
	}
	legacyHeaders := doJSONWithHeaders(t, server.URL, http.MethodGet, "/api/v1/models", keyASecret, nil, nil, http.StatusOK, &platformModels)
	if legacyHeaders.Get("Deprecation") != "true" || !strings.Contains(legacyHeaders.Get("Link"), "/api/v1/platform-models") {
		t.Fatalf("legacy model list deprecation headers missing: %+v", legacyHeaders)
	}

	// Two sources with the same logical name collapse to one OpenAI model. A
	// source-level deny removes only that source until the final source is gone.
	var multiRich struct {
		Items []modelAccessFixture `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/v1/platform-models", keyMultiSecret, nil, http.StatusOK, &multiRich)
	if countRichModel(multiRich.Items, multiName) != 2 {
		t.Fatalf("expected two initial rich sources for %s: %+v", multiName, multiRich.Items)
	}
	batchKeyRule(adminToken, keyMultiID, multiModel.ID, "deny", http.StatusOK)
	doJSON(t, server.URL, http.MethodGet, "/api/v1/platform-models", keyMultiSecret, nil, http.StatusOK, &multiRich)
	if countRichModel(multiRich.Items, multiName) != 1 || containsModelID(multiRich.Items, multiModel.ID) || !containsModelID(multiRich.Items, multiSecondModel.ID) {
		t.Fatalf("first source deny did not leave exactly the second source: %+v", multiRich.Items)
	}
	var multiOpenAI struct {
		Object string `json:"object"`
		Data   []struct {
			ID string `json:"id"`
		} `json:"data"`
	}
	doJSON(t, server.URL, http.MethodGet, "/v1/models", keyMultiSecret, nil, http.StatusOK, &multiOpenAI)
	if countOpenAIModel(multiOpenAI.Data, multiName) != 1 {
		t.Fatalf("logical model disappeared while one source remained: %+v", multiOpenAI.Data)
	}
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyMultiSecret, map[string]any{
		"model": multiName, "prompt": "layered multi source", "runMode": "simulation", "simulation": true,
	}, http.StatusOK, nil)
	batchKeyRule(adminToken, keyMultiID, multiSecondModel.ID, "deny", http.StatusOK)
	doJSON(t, server.URL, http.MethodGet, "/v1/models", keyMultiSecret, nil, http.StatusOK, &multiOpenAI)
	if countOpenAIModel(multiOpenAI.Data, multiName) != 0 {
		t.Fatalf("logical model remained after all sources were denied: %+v", multiOpenAI.Data)
	}
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyMultiSecret, map[string]any{
		"model": multiName, "prompt": "no sources", "runMode": "simulation", "simulation": true,
	}, http.StatusNotFound, nil)
	t.Logf("多来源模型：初始来源=2，排除一个后 OpenAI 逻辑模型=1，全部排除后=0；调用状态=200/404")

	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyASecret, map[string]any{
		"model": imageName, "prompt": "key whitelist", "runMode": "simulation", "simulation": true,
	}, http.StatusOK, nil)
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyAInheritedSecret, map[string]any{
		"model": imageName, "prompt": "sibling key", "runMode": "simulation", "simulation": true,
	}, http.StatusOK, nil)
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyASecret, map[string]any{
		"model": keyDenyImageName, "prompt": "key deny", "runMode": "simulation", "simulation": true,
	}, http.StatusNotFound, nil)
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyBImageSecret, map[string]any{
		"model": groupBImageName, "prompt": "group B whitelist", "runMode": "simulation", "simulation": true,
	}, http.StatusOK, nil)
	var taskCountBefore, taskCountAfter int
	if err := pool.QueryRow(ctx, `SELECT count(*) FROM gateway_tasks`).Scan(&taskCountBefore); err != nil {
		t.Fatalf("count tasks before rejected whitelist request: %v", err)
	}
	doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", keyASecret, map[string]any{
		"model": multiName, "prompt": "not in current key whitelist", "runMode": "simulation", "simulation": true,
	}, http.StatusNotFound, nil)
	if err := pool.QueryRow(ctx, `SELECT count(*) FROM gateway_tasks`).Scan(&taskCountAfter); err != nil {
		t.Fatalf("count tasks after rejected whitelist request: %v", err)
	}
	if taskCountAfter != taskCountBefore {
		t.Fatalf("rejected whitelist request created a task: before=%d after=%d", taskCountBefore, taskCountAfter)
	}
	t.Logf("实际调用：KEY 白名单允许 200，兄弟 KEY 不受影响 200，KEY deny 404，组 B 白名单 200；拒绝请求未创建任务")

	// Group B can still use text: group A and stale API-key allows are never
	// evaluated for this group's keys.
	if !containsModelID(loadAssignable(userBToken, keyBID).Items, textModel.ID) {
		t.Fatalf("group B chat key candidate list lost the group-A denied model")
	}
	for _, modelName := range []string{textName, scopeTextName, legacyTextName} {
		doJSON(t, server.URL, http.MethodPost, "/api/v1/chat/completions", keyBSecret, map[string]any{
			"model": modelName, "messages": []map[string]any{{"role": "user", "content": "layered access"}},
			"runMode": "simulation", "simulation": true,
		}, http.StatusOK, nil)
	}
	t.Logf("失效旧规则：组 B 对 3 个文本模型的 simulation 调用均为 200")

	// The image-only key is rejected before candidate selection for chat.
	doJSON(t, server.URL, http.MethodPost, "/api/v1/chat/completions", keyASecret, map[string]any{
		"model": multiName, "messages": []map[string]any{{"role": "user", "content": "scope denial"}},
	}, http.StatusForbidden, nil)
	t.Logf("scope：image KEY 的多能力候选仅保留 image_generate；图像调用 200，文本调用 403")
}

func containsModelID(items []modelAccessFixture, id string) bool {
	for _, item := range items {
		if item.ID == id {
			return true
		}
	}
	return false
}

func countRichModel(items []modelAccessFixture, modelName string) int {
	count := 0
	for _, item := range items {
		if item.ModelName == modelName {
			count++
		}
	}
	return count
}

func containsDiagnosticReason(items []modelAccessRuleDiagnostic, reason string) bool {
	for _, item := range items {
		if !item.Effective && item.Reason == reason {
			return true
		}
	}
	return false
}

func diagnosticReasons(items []modelAccessRuleDiagnostic) []string {
	reasons := make([]string, 0, len(items))
	for _, item := range items {
		if !item.Effective {
			reasons = append(reasons, item.Reason)
		}
	}
	return reasons
}

func countOpenAIModel(items []struct {
	ID string `json:"id"`
}, id string) int {
	count := 0
	for _, item := range items {
		if item.ID == id {
			count++
		}
	}
	return count
}
