package ai

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
	"time"
)

const (
	ProviderXAI        = "xai"
	ProviderOpenCodeGo = "opencode-go"

	// ModelsCacheTTL is the fresh model-list window.
	ModelsCacheTTL = 5 * time.Minute
	// ModelsCacheStaleTTL is the maximum stale fallback window.
	ModelsCacheStaleTTL = 15 * time.Minute

	modelsRefreshLockTTL = 30 * time.Second
	modelsRefreshWait    = 25 * time.Millisecond
	modelsRefreshChecks  = 20
)

// ModelsCacheKey identifies one provider credential without containing the credential.
type ModelsCacheKey struct {
	Provider              string
	CredentialFingerprint string
}

// ModelsCacheValue is the JSON payload persisted by a ModelsCache.
type ModelsCacheValue struct {
	Models    []string `json:"models"`
	FetchedAt int64    `json:"fetchedAt"`
}

// ModelsCache stores model lists and coordinates refreshes across instances.
type ModelsCache interface {
	Get(ctx context.Context, key ModelsCacheKey) (ModelsCacheValue, bool, error)
	Set(ctx context.Context, key ModelsCacheKey, value ModelsCacheValue, ttl time.Duration) error
	AcquireRefresh(ctx context.Context, key ModelsCacheKey, token string, ttl time.Duration) (bool, error)
	ReleaseRefresh(ctx context.Context, key ModelsCacheKey, token string) error
}

// Registry holds the immutable provider client registry and a shared models cache.
type Registry struct {
	clients           map[string]Client
	cache             ModelsCache
	fingerprintSecret []byte
}

func NewRegistry(cache ModelsCache, fingerprintSecret string) *Registry {
	return newRegistry(cache, fingerprintSecret, map[string]Client{
		ProviderXAI:        NewOpenAICompatible(ProviderXAI, "https://api.x.ai/v1"),
		ProviderOpenCodeGo: NewOpenAICompatible(ProviderOpenCodeGo, "https://opencode.ai/zen/go/v1"),
	})
}

// NewRegistryWithClients injects provider clients for focused tests.
func NewRegistryWithClients(cache ModelsCache, fingerprintSecret string, clients map[string]Client) *Registry {
	return newRegistry(cache, fingerprintSecret, clients)
}

func newRegistry(cache ModelsCache, fingerprintSecret string, clients map[string]Client) *Registry {
	immutableClients := make(map[string]Client, len(clients))
	for provider, client := range clients {
		immutableClients[NormalizeProvider(provider)] = client
	}
	return &Registry{clients: immutableClients, cache: cache, fingerprintSecret: []byte(fingerprintSecret)}
}

func NormalizeProvider(p string) string {
	switch strings.ToLower(strings.TrimSpace(p)) {
	case "opencode-go", "opencode", "opencode_go":
		return ProviderOpenCodeGo
	case "xai", "grok", "":
		return ProviderXAI
	default:
		return strings.ToLower(strings.TrimSpace(p))
	}
}

func (r *Registry) Client(provider string) (Client, error) {
	id := NormalizeProvider(provider)
	c, ok := r.clients[id]
	if !ok {
		return nil, fmt.Errorf("unsupported provider: %s (use xai or opencode-go)", provider)
	}
	return c, nil
}

// ListModelsCached fetches models via provider API; caches per provider+credential.
// fromCache indicates whether result was served from cache.
func (r *Registry) ListModelsCached(ctx context.Context, provider, apiKey string) (models []string, fromCache bool, err error) {
	id := NormalizeProvider(provider)
	if _, err := r.Client(id); err != nil {
		return nil, false, err
	}
	key := r.cacheKey(id, apiKey)
	now := time.Now().UTC()
	stale, hasStale := r.cached(ctx, key, now)
	if hasStale && cacheAge(now, stale.FetchedAt) <= ModelsCacheTTL {
		return cloneModels(stale.Models), true, nil
	}

	token, tokenErr := refreshToken()
	locked := false
	if tokenErr != nil {
		err = tokenErr
	} else if r.cache != nil {
		locked, err = r.cache.AcquireRefresh(ctx, key, token, modelsRefreshLockTTL)
		if err != nil {
			locked = false
		}
	}
	if locked {
		defer func() {
			releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
			defer cancel()
			_ = r.cache.ReleaseRefresh(releaseCtx, key, token)
		}()
	} else if r.cache != nil && err == nil {
		for range modelsRefreshChecks {
			select {
			case <-ctx.Done():
				return nil, false, ctx.Err()
			case <-time.After(modelsRefreshWait):
			}
			if current, ok := r.cached(ctx, key, time.Now().UTC()); ok && cacheAge(time.Now().UTC(), current.FetchedAt) <= ModelsCacheTTL {
				return cloneModels(current.Models), true, nil
			}
		}
	}

	c, err := r.Client(id)
	list, err := c.ListModels(ctx, apiKey)
	if err != nil {
		if hasStale {
			return cloneModels(stale.Models), true, nil
		}
		return nil, false, err
	}

	if r.cache != nil {
		_ = r.cache.Set(ctx, key, ModelsCacheValue{
			Models:    cloneModels(list),
			FetchedAt: time.Now().UTC().UnixNano(),
		}, ModelsCacheStaleTTL)
	}
	return list, false, nil
}

func (r *Registry) cached(ctx context.Context, key ModelsCacheKey, now time.Time) (ModelsCacheValue, bool) {
	if r.cache == nil {
		return ModelsCacheValue{}, false
	}
	value, ok, err := r.cache.Get(ctx, key)
	if err != nil || !ok || value.FetchedAt <= 0 {
		return ModelsCacheValue{}, false
	}
	if cacheAge(now, value.FetchedAt) > ModelsCacheStaleTTL {
		return ModelsCacheValue{}, false
	}
	return value, true
}

func (r *Registry) cacheKey(provider, apiKey string) ModelsCacheKey {
	mac := hmac.New(sha256.New, r.fingerprintSecret)
	_, _ = mac.Write([]byte(apiKey))
	return ModelsCacheKey{Provider: provider, CredentialFingerprint: hex.EncodeToString(mac.Sum(nil))}
}

func refreshToken() (string, error) {
	var token [16]byte
	if _, err := rand.Read(token[:]); err != nil {
		return "", err
	}
	return hex.EncodeToString(token[:]), nil
}

func cacheAge(now time.Time, fetchedAt int64) time.Duration {
	age := now.Sub(time.Unix(0, fetchedAt).UTC())
	if age < 0 {
		return 0
	}
	return age
}

func cloneModels(models []string) []string {
	return append([]string(nil), models...)
}

// FallbackModels when no key / provider error (never empty for UI).
// OpenCode Go 模型 id 見 https://opencode.ai/docs/go/（不是 OpenAI gpt-*）
func FallbackModels(provider string) []string {
	switch NormalizeProvider(provider) {
	case ProviderOpenCodeGo:
		return []string{
			"kimi-k2.6",
			"glm-5.1",
			"glm-5.2",
			"deepseek-v4-flash",
			"deepseek-v4-pro",
			"qwen3.7-plus",
			"minimax-m2.7",
			"mimo-v2.5",
		}
	default:
		return []string{"grok-3", "grok-3-mini", "grok-2"}
	}
}

// DefaultModel for provider when member settings model empty / invalid.
func DefaultModel(provider string) string {
	list := FallbackModels(provider)
	if len(list) == 0 {
		return "grok-3"
	}
	return list[0]
}

// SanitizeModel remaps invalid model ids for a provider (e.g. gpt-4o-mini on opencode-go).
func SanitizeModel(provider, model string) string {
	provider = NormalizeProvider(provider)
	model = strings.TrimSpace(model)
	if model == "" {
		return DefaultModel(provider)
	}
	if provider != ProviderOpenCodeGo {
		// xAI：允許 grok-*；若誤存 opencode 模型名，仍交由 API 驗證
		return model
	}
	// OpenCode Go：常見錯誤 — 從 xAI / OpenAI 帶過來的模型名
	lower := strings.ToLower(model)
	if strings.HasPrefix(lower, "gpt-") ||
		strings.HasPrefix(lower, "o1") ||
		strings.HasPrefix(lower, "o3") ||
		strings.HasPrefix(lower, "grok") ||
		strings.HasPrefix(lower, "claude") ||
		strings.HasPrefix(lower, "gemini") {
		return DefaultModel(provider)
	}
	return model
}
