package agent

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const processRuntimeDeliveryDeferredLease = 6 * time.Minute

// runtimeDeliveryLifecycleLease transfers the lifecycle read-side owner
// acquired by InboxMessagesForFreshnessPageForRuntime to the exact MCP result
// that contains the claimed bodies. Stop may mark the Process non-accepting,
// but it cannot finish retirement/replacement until every receipt in this
// claim batch has committed Delivered, become explicitly Uncertain, failed
// closed, or been settled after its owning transport is proven retired.
//
// One page can claim several bodies that cross the same HTTP response boundary,
// so they share one read lock and release it only after the final receipt is
// resolved. RWMutex read ownership is intentionally transferable between
// goroutines; Go locks are not goroutine-affine.
type runtimeDeliveryLifecycleLease struct {
	process *Process

	mu        sync.Mutex
	remaining map[string]struct{}
	released  bool
}

func newRuntimeDeliveryLifecycleLease(ap *Process, receipts []string) *runtimeDeliveryLifecycleLease {
	remaining := make(map[string]struct{}, len(receipts))
	for _, receipt := range receipts {
		receipt = strings.TrimSpace(receipt)
		if types.IsRuntimeDeliveryReceipt(receipt) {
			remaining[receipt] = struct{}{}
		}
	}
	return &runtimeDeliveryLifecycleLease{process: ap, remaining: remaining}
}

// AcknowledgeMCPToolResultDelivery implements types.MCPToolResultDeliveryOwner.
// It calls the exact Process captured by the claim rather than resolving the
// agent ID through Dispatcher after a concurrent Stop/replacement.
func (lease *runtimeDeliveryLifecycleLease) AcknowledgeMCPToolResultDelivery(receipts []string, outcome types.MCPToolDeliveryOutcome) {
	if lease == nil || lease.process == nil {
		return
	}
	lease.process.acknowledgeRuntimeMessageToolDeliveriesForOwner(receipts, outcome, "", lease)
}

func (lease *runtimeDeliveryLifecycleLease) resolve(receipt string) {
	if lease == nil {
		return
	}
	receipt = strings.TrimSpace(receipt)
	lease.mu.Lock()
	if lease.released {
		lease.mu.Unlock()
		return
	}
	delete(lease.remaining, receipt)
	if len(lease.remaining) != 0 {
		lease.mu.Unlock()
		return
	}
	lease.released = true
	process := lease.process
	lease.mu.Unlock()
	if process != nil {
		process.endLifecycleMutation()
	}
}

// cancelRuntimeDeliveryLeaseTimerLocked wakes a stale lease goroutine
// immediately instead of leaving one timer/goroutine asleep until the default
// six-minute deadline after a terminal response. The caller must hold ap.mu and
// must write the mutated pending value back (or remove it) before unlocking.
func cancelRuntimeDeliveryLeaseTimerLocked(pending *pendingRuntimeInput) {
	if pending == nil || pending.finalHopLeaseCancel == nil {
		return
	}
	close(pending.finalHopLeaseCancel)
	pending.finalHopLeaseCancel = nil
}

// settleTransferredRuntimeDeliveryLeasesAfterTransportRetired is the retirement
// catch-all for Deferred second hops. Call it only after waitFunc/cmd.Wait has
// proved the child generation and its output transport are gone. The durable
// owner is already Uncertain, so settlement only invalidates ephemeral lease
// tokens and releases lifecycle readers; it must not reconcile on the retired
// Process or touch direct first-hop HTTP owners whose writes are independent of
// the child transport.
func (ap *Process) settleTransferredRuntimeDeliveryLeasesAfterTransportRetired() {
	if ap == nil {
		return
	}
	type settlement struct {
		receipt string
		lease   *runtimeDeliveryLifecycleLease
	}
	var settlements []settlement
	ap.mu.Lock()
	for receipt, pending := range ap.pendingRuntimeInputs {
		if !pending.FinalHopDeferred || !pending.FinalHopRuntimeTransferred || pending.finalHopLifecycleLease == nil {
			continue
		}
		settlements = append(settlements, settlement{receipt: receipt, lease: pending.finalHopLifecycleLease})
		cancelRuntimeDeliveryLeaseTimerLocked(&pending)
		pending.FinalHopDeferred = false
		pending.FinalHopRuntimeTransferred = false
		pending.finalHopLeaseGeneration++
		pending.finalHopLeaseDeadline = time.Time{}
		pending.finalHopLifecycleLease = nil
		ap.pendingRuntimeInputs[receipt] = pending
	}
	ap.mu.Unlock()
	for _, settlement := range settlements {
		settlement.lease.resolve(settlement.receipt)
	}
}

func generateCryptographicRuntimeDeliveryReceipt() (string, error) {
	random := make([]byte, 16)
	if _, err := rand.Read(random); err != nil {
		return "", fmt.Errorf("generate runtime delivery receipt: %w", err)
	}
	receipt := types.NewRuntimeDeliveryReceipt(hex.EncodeToString(random))
	if !types.IsRuntimeDeliveryReceipt(receipt) {
		return "", fmt.Errorf("generate runtime delivery receipt: empty receipt")
	}
	return receipt, nil
}

func (ap *Process) runtimeDeliveryReceiptLocked() (string, error) {
	generator := ap.runtimeDeliveryReceiptGenerator
	if generator == nil {
		generator = generateCryptographicRuntimeDeliveryReceipt
	}
	receipt, err := generator()
	if err != nil {
		return "", err
	}
	receipt = strings.TrimSpace(receipt)
	if !types.IsRuntimeDeliveryReceipt(receipt) {
		return "", fmt.Errorf("runtime delivery receipt generator returned invalid receipt")
	}
	return receipt, nil
}
