package results

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"github.com/oklog/ulid/v2"
	log "github.com/sirupsen/logrus"
)

// ID obfuscation provides an optional privacy layer for test result URLs.
// When enabled, the telemetry endpoint returns an obfuscated ULID that must
// be deobfuscated before looking up the result.
//
// This is NOT cryptographically secure — it prevents casual ID guessing,
// matching the behavior of the PHP version's idObfuscation.php.

var (
	obfuscationSalt     uint32
	obfuscationSaltOnce sync.Once
	obfuscationSaltErr  error
)

const obfuscationSaltFile = "idObfuscation_salt.bin"

func getOrCreateObfuscationSalt() (uint32, error) {
	obfuscationSaltOnce.Do(func() {
		data, err := os.ReadFile(obfuscationSaltFile)
		if err == nil && len(data) == 4 {
			obfuscationSalt = binary.LittleEndian.Uint32(data)
			return
		}

		saltBytes := make([]byte, 4)
		if _, err := rand.Read(saltBytes); err != nil {
			obfuscationSaltErr = fmt.Errorf("failed to generate obfuscation salt: %w", err)
			return
		}
		obfuscationSalt = binary.LittleEndian.Uint32(saltBytes)

		dir := filepath.Dir(obfuscationSaltFile)
		if dir != "." {
			if err := os.MkdirAll(dir, 0755); err != nil {
				log.Warnf("Could not create directory for obfuscation salt file: %s", err)
			}
		}
		if err := os.WriteFile(obfuscationSaltFile, saltBytes, 0644); err != nil {
			log.Warnf("Could not save obfuscation salt file: %s", err)
		}
	})

	return obfuscationSalt, obfuscationSaltErr
}

// obfuscateBytes applies reversible transform on ULID bytes:
// XOR the first 4 bytes with the salt (simple but effective for casual privacy)
func obfuscateBytes(data []byte) []byte {
	salt, err := getOrCreateObfuscationSalt()
	if err != nil || len(data) < 4 {
		return data
	}
	result := make([]byte, len(data))
	copy(result, data)
	val := binary.LittleEndian.Uint32(result[:4])
	val ^= salt
	binary.LittleEndian.PutUint32(result[:4], val)
	return result
}

// deobfuscateBytes reverses obfuscateBytes (XOR is self-inverse)
var deobfuscateBytes = obfuscateBytes

// ObfuscateULID transforms a ULID string to its obfuscated (base64) form
func ObfuscateULID(id string) string {
	parsed, err := ulid.Parse(id)
	if err != nil {
		return id
	}
	obfuscated := obfuscateBytes(parsed[:])
	return base64.RawURLEncoding.EncodeToString(obfuscated)
}

// DeobfuscateULID reverses ULID obfuscation
func DeobfuscateULID(obfuscated string) (string, error) {
	data, err := base64.RawURLEncoding.DecodeString(obfuscated)
	if err != nil {
		return "", fmt.Errorf("invalid obfuscated ID encoding: %w", err)
	}
	if len(data) != 16 {
		return "", fmt.Errorf("invalid obfuscated ID length: %d", len(data))
	}
	deobfuscated := deobfuscateBytes(data)
	var id ulid.ULID
	copy(id[:], deobfuscated)
	return id.String(), nil
}

// ResolveID takes an ID string and returns the database ULID.
// It tries the raw input first, then attempts deobfuscation.
func ResolveID(id string) string {
	// First try: use as-is (plain ULID)
	if _, err := ulid.Parse(id); err == nil {
		return id
	}

	// Second try: deobfuscate
	if deobfuscated, err := DeobfuscateULID(id); err == nil {
		return deobfuscated
	}

	return id
}


