// Package idempotency implements the idempotency pattern in Go.
//
// This package demonstrates the same invariant as the Effect.ts implementation:
// - Dedup by key + write-ahead ledger → execute once within TTL
package idempotency

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sync"
	"time"
)

// Context holds the dedup store and ledger for idempotent operations.
type Context struct {
	dedup      sync.Map // key -> *cacheEntry
	ledger     []map[string]any
	ledgerLock sync.Mutex
	keyLocks   sync.Map // key -> *sync.Mutex for per-key locking
}

type cacheEntry struct {
	result any
	expiry time.Time
}

// NewContext creates a new idempotency context.
func NewContext() *Context {
	return &Context{
		ledger: make([]map[string]any, 0),
	}
}

// Clear resets the context for testing.
func (c *Context) Clear() {
	c.dedup = sync.Map{}
	c.keyLocks = sync.Map{}
	c.ledgerLock.Lock()
	c.ledger = make([]map[string]any, 0)
	c.ledgerLock.Unlock()
}

// AppendLedger adds an entry to the ledger with thread safety.
func (c *Context) AppendLedger(entry map[string]any) {
	c.ledgerLock.Lock()
	defer c.ledgerLock.Unlock()
	entry["timestamp"] = time.Now().Unix()
	c.ledger = append(c.ledger, entry)
}

// LedgerCount counts ledger entries of a specific kind for a key.
func (c *Context) LedgerCount(kind, key string) int {
	c.ledgerLock.Lock()
	defer c.ledgerLock.Unlock()
	
	count := 0
	for _, entry := range c.ledger {
		if entry["kind"] == kind && entry["idempotencyKey"] == key {
			count++
		}
	}
	return count
}

// GetLedger returns a copy of the ledger for testing.
func (c *Context) GetLedger() []map[string]any {
	c.ledgerLock.Lock()
	defer c.ledgerLock.Unlock()
	
	result := make([]map[string]any, len(c.ledger))
	copy(result, c.ledger)
	return result
}

func digest(data any) string {
	jsonBytes, _ := json.Marshal(data)
	hash := sha256.Sum256(jsonBytes)
	return hex.EncodeToString(hash[:])
}

func processSideEffect(key string, args map[string]any) (map[string]any, error) {
	// Simulate async work
	time.Sleep(10 * time.Millisecond)
	
	// Include key in transaction ID to ensure uniqueness per key
	combined := fmt.Sprintf("%s%s", key, digest(args))
	txnID := fmt.Sprintf("txn_%s", digest(combined)[:8])
	
	amount := float64(0)
	if a, ok := args["amount"].(float64); ok {
		amount = a
	}
	
	currency := "USD"
	if c, ok := args["currency"].(string); ok {
		currency = c
	}
	
	return map[string]any{
		"transaction_id": txnID,
		"status":         "success",
		"amount":         amount,
		"currency":       currency,
	}, nil
}

// snippet-start: idempotency_pattern
// Charge processes a charge with idempotency guarantee.
//
// Invariant: Same key returns same result without re-execution.
func Charge(ctx *Context, key string, args map[string]any) (map[string]any, error) {
	// Get or create lock for this key
	lockInterface, _ := ctx.keyLocks.LoadOrStore(key, &sync.Mutex{})
	keyLock := lockInterface.(*sync.Mutex)
	
	// Lock for this specific key
	keyLock.Lock()
	defer keyLock.Unlock()
	
	// Check dedup store with TTL
	if cached, ok := ctx.dedup.Load(key); ok {
		entry := cached.(*cacheEntry)
		if time.Now().Before(entry.expiry) {
			// Record dedup hit in ledger
			ctx.AppendLedger(map[string]any{
				"kind":           "tool.result",
				"idempotencyKey": key,
				"dedup":          true,
			})
			return entry.result.(map[string]any), nil
		}
		// Expired, remove from store
		ctx.dedup.Delete(key)
	}
	
	// Write-ahead ledger: record the call BEFORE execution
	ctx.AppendLedger(map[string]any{
		"kind":           "tool.call",
		"idempotencyKey": key,
		"argsDigest":     digest(args),
	})
	
	// Execute the side effect exactly once
	result, err := processSideEffect(key, args)
	if err != nil {
		return nil, err
	}
	
	// Record the result in the ledger
	ctx.AppendLedger(map[string]any{
		"kind":           "tool.result",
		"idempotencyKey": key,
		"result":         result,
	})
	
	// Cache with TTL (24 hours)
	ctx.dedup.Store(key, &cacheEntry{
		result: result,
		expiry: time.Now().Add(24 * time.Hour),
	})
	
	return result, nil
}
// snippet-end: idempotency_pattern

// WithIdempotency is a standalone wrapper matching the Effect.ts pattern.
//
// Test assertion (identical across all languages):
// assert(process("key-1", args) == process("key-1", args))
func WithIdempotency(ctx *Context, key string, args map[string]any) (map[string]any, error) {
	return Charge(ctx, key, args)
}