package kiro

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"math/rand"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
)

type RuntimeFingerprint struct {
	OIDCSDKVersion      string
	RuntimeSDKVersion   string
	StreamingSDKVersion string
	OSType              string
	OSVersion           string
	NodeVersion         string
	KiroVersion         string
	KiroHash            string
}

type runtimeFingerprintManager struct {
	mu           sync.RWMutex
	fingerprints map[string]*RuntimeFingerprint
}

var (
	globalRuntimeFingerprintManager     *runtimeFingerprintManager
	globalRuntimeFingerprintManagerOnce sync.Once

	oidcSDKVersions      = []string{"3.980.0", "3.975.0", "3.972.0", "3.808.0", "3.738.0", "3.737.0", "3.736.0", "3.735.0"}
	runtimeSDKVersions   = []string{"1.0.0"}
	streamingSDKVersions = []string{"1.0.34"}
	osTypes              = []string{"darwin", "win32"}
	osVersions           = map[string][]string{
		"darwin": {"24.6.0"},
		"win32":  {"10.0.22631"},
	}
	nodeVersions = []string{"22.22.0"}
	kiroVersions = []string{
		"0.11.132", "0.11.131", "0.11.130",
	}
)

func globalRuntimeFingerprints() *runtimeFingerprintManager {
	globalRuntimeFingerprintManagerOnce.Do(func() {
		globalRuntimeFingerprintManager = &runtimeFingerprintManager{
			fingerprints: make(map[string]*RuntimeFingerprint),
		}
	})
	return globalRuntimeFingerprintManager
}

func (m *runtimeFingerprintManager) Get(accountKey, machineID string) *RuntimeFingerprint {
	lookupKey := fingerprintLookupKey(accountKey, "runtime")
	machineID = normalizeMachineIDOrFallback(machineID, lookupKey)

	m.mu.RLock()
	if fp, ok := m.fingerprints[lookupKey]; ok && fp.KiroHash == machineID {
		m.mu.RUnlock()
		return fp
	}
	m.mu.RUnlock()

	m.mu.Lock()
	defer m.mu.Unlock()
	if fp, ok := m.fingerprints[lookupKey]; ok && fp.KiroHash == machineID {
		return fp
	}
	fp := generateRuntimeFingerprint(lookupKey, machineID)
	m.fingerprints[lookupKey] = fp
	return fp
}

func generateRuntimeFingerprint(accountKey, machineID string) *RuntimeFingerprint {
	hash := sha256.Sum256([]byte(accountKey))
	seed := int64(binary.BigEndian.Uint64(hash[:8]))
	rng := rand.New(rand.NewSource(seed))

	osType := goOSToNodePlatform(runtime.GOOS)
	if !containsString(osTypes, osType) {
		osType = osTypes[rng.Intn(len(osTypes))]
	}
	osVersionPool := osVersions[osType]
	if len(osVersionPool) == 0 {
		osVersionPool = osVersions["darwin"]
	}

	return &RuntimeFingerprint{
		OIDCSDKVersion:      oidcSDKVersions[rng.Intn(len(oidcSDKVersions))],
		RuntimeSDKVersion:   runtimeSDKVersions[rng.Intn(len(runtimeSDKVersions))],
		StreamingSDKVersion: streamingSDKVersions[rng.Intn(len(streamingSDKVersions))],
		OSType:              osType,
		OSVersion:           osVersionPool[rng.Intn(len(osVersionPool))],
		NodeVersion:         nodeVersions[rng.Intn(len(nodeVersions))],
		KiroVersion:         kiroVersions[rng.Intn(len(kiroVersions))],
		KiroHash:            machineID,
	}
}

func goOSToNodePlatform(goos string) string {
	switch strings.TrimSpace(goos) {
	case "windows":
		return "win32"
	default:
		return strings.TrimSpace(goos)
	}
}

func containsString(items []string, target string) bool {
	for _, item := range items {
		if item == target {
			return true
		}
	}
	return false
}

func BuildAccountKey(clientID, clientIDHash, refreshToken, profileArn string, accountID int64) string {
	switch {
	case strings.TrimSpace(clientIDHash) != "":
		return clientIDHash
	case strings.TrimSpace(clientID) != "":
		return shortSHA(clientID)
	case strings.TrimSpace(refreshToken) != "":
		return shortSHA(refreshToken)
	case strings.TrimSpace(profileArn) != "":
		return shortSHA(profileArn)
	case accountID > 0:
		return shortSHA(fmt.Sprintf("account:%d", accountID))
	default:
		return shortSHA(uuid.NewString())
	}
}

func NormalizeMachineID(machineID string) (string, bool) {
	trimmed := strings.TrimSpace(machineID)
	if len(trimmed) == 64 && isHexString(trimmed) {
		return strings.ToLower(trimmed), true
	}
	withoutDashes := strings.ReplaceAll(trimmed, "-", "")
	if len(withoutDashes) == 32 && isHexString(withoutDashes) {
		normalized := strings.ToLower(withoutDashes)
		return normalized + normalized, true
	}
	return "", false
}

func BuildMachineID(refreshToken, apiKey, fallbackKey string) string {
	if refreshToken = strings.TrimSpace(refreshToken); refreshToken != "" {
		return sha256Hex("KotlinNativeAPI/" + refreshToken)
	}
	if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
		return sha256Hex("KiroAPIKey/" + apiKey)
	}
	if fallbackKey = strings.TrimSpace(fallbackKey); fallbackKey != "" {
		return sha256Hex("KiroFallback/" + fallbackKey)
	}
	return sha256Hex("KiroFallback/default")
}

func shortSHA(seed string) string {
	sum := sha256.Sum256([]byte(seed))
	return hex.EncodeToString(sum[:8])
}

func sha256Hex(seed string) string {
	sum := sha256.Sum256([]byte(seed))
	return hex.EncodeToString(sum[:])
}

func isHexString(value string) bool {
	for _, c := range value {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}

func normalizeMachineIDOrFallback(machineID, fallbackKey string) string {
	if normalized, ok := NormalizeMachineID(machineID); ok {
		return normalized
	}
	return BuildMachineID("", "", fallbackKey)
}

func fingerprintLookupKey(accountKey, fallback string) string {
	key := strings.TrimSpace(accountKey)
	if key != "" {
		return key
	}
	return fallback
}

func BuildRuntimeUserAgent(accountKey, machineID string) string {
	fp := globalRuntimeFingerprints().Get(accountKey, machineID)
	return fmt.Sprintf(
		"aws-sdk-js/%s ua/2.1 os/%s#%s lang/js md/nodejs#%s api/codewhispererstreaming#%s m/E KiroIDE-%s-%s",
		fp.StreamingSDKVersion,
		fp.OSType,
		fp.OSVersion,
		fp.NodeVersion,
		fp.StreamingSDKVersion,
		fp.KiroVersion,
		fp.KiroHash,
	)
}

func BuildRuntimeAmzUserAgent(accountKey, machineID string) string {
	fp := globalRuntimeFingerprints().Get(accountKey, machineID)
	return fmt.Sprintf(
		"aws-sdk-js/%s KiroIDE-%s-%s",
		fp.StreamingSDKVersion,
		fp.KiroVersion,
		fp.KiroHash,
	)
}

func BuildOIDCHeaders(accountKey, machineID string) map[string]string {
	fp := globalRuntimeFingerprints().Get(fingerprintLookupKey(accountKey, "oidc-session"), machineID)
	return map[string]string{
		"Content-Type":          "application/json",
		"x-amz-user-agent":      fmt.Sprintf("aws-sdk-js/%s KiroIDE", fp.OIDCSDKVersion),
		"User-Agent":            fmt.Sprintf("aws-sdk-js/%s ua/2.1 os/%s#%s lang/js md/nodejs#%s api/sso-oidc#%s m/E KiroIDE", fp.OIDCSDKVersion, fp.OSType, fp.OSVersion, fp.NodeVersion, fp.OIDCSDKVersion),
		"amz-sdk-invocation-id": uuid.NewString(),
		"amz-sdk-request":       "attempt=1; max=4",
	}
}

func BuildLoginHeaders(accountKey, machineID string) map[string]string {
	fp := globalRuntimeFingerprints().Get(fingerprintLookupKey(accountKey, "login"), machineID)
	return map[string]string{
		"Content-Type": "application/json",
		"User-Agent":   fmt.Sprintf("KiroIDE-%s-%s", fp.KiroVersion, fp.KiroHash),
		"Accept":       "application/json, text/plain, */*",
	}
}

func ExponentialBackoffWithJitter(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
	if attempt < 0 {
		attempt = 0
	}
	delay := baseDelay << attempt
	if delay > maxDelay {
		delay = maxDelay
	}
	const jitterFactor = 0.3
	seed := rand.New(rand.NewSource(time.Now().UnixNano()))
	jitter := 1 + ((seed.Float64()*2 - 1) * jitterFactor)
	return time.Duration(float64(delay) * jitter)
}
