package openai

import (
	"context"
	"sort"
	"strings"
	"time"

	"go.uber.org/zap"
)

const (
	defaultLogicalRequestSweepLimit = 64
	// Retained as test/source compatibility names; observePossibleWorkspaceOrphan
	// no longer emits the legacy message or reason fields.
	hotPathOrphanObservationMessage = "hot_path_workspace_orphan"
	hotPathOrphanReasonTTL          = "logical_request_ttl_expired"
)

type logicalRequestExpirySnapshot struct {
	RequestID     string
	OwnerEdgeID   string
	PriorState    logicalRequestState
	Stage         string
	TerminalClass string
	UpdatedAt     time.Time
}

func (c *logicalRequestCoordinator) expiredForSweepLocked(record *logicalRequestRecord, now time.Time) bool {
	if record == nil || now.Sub(record.updatedAt) <= c.ttl {
		return false
	}
	// Active work is protected even when a caller-visible TTL elapses. A
	// cancelled/disconnected owner explicitly changes the state to detached.
	return record.state != logicalRequestStateActive
}

func (c *logicalRequestCoordinator) sweepExpired(now time.Time, maxSweep int) []logicalRequestExpirySnapshot {
	if c == nil {
		return nil
	}
	if maxSweep <= 0 {
		maxSweep = defaultLogicalRequestSweepLimit
	}
	c.mu.Lock()
	defer c.mu.Unlock()

	candidates := make([]*logicalRequestRecord, 0)
	for _, record := range c.requests {
		if c.expiredForSweepLocked(record, now) {
			candidates = append(candidates, record)
		}
	}
	sort.Slice(candidates, func(i, j int) bool {
		if candidates[i].updatedAt.Equal(candidates[j].updatedAt) {
			return candidates[i].id < candidates[j].id
		}
		return candidates[i].updatedAt.Before(candidates[j].updatedAt)
	})
	if len(candidates) > maxSweep {
		candidates = candidates[:maxSweep]
	}
	out := make([]logicalRequestExpirySnapshot, 0, len(candidates))
	for _, record := range candidates {
		stage := strings.TrimSpace(record.activeStageID)
		if stage == "" {
			stage = string(record.state)
		}
		out = append(out, logicalRequestExpirySnapshot{
			RequestID: record.id, OwnerEdgeID: record.ownerEdgeID, PriorState: record.state,
			Stage: stage, TerminalClass: record.terminalClass, UpdatedAt: record.updatedAt,
		})
		delete(c.requests, record.id)
	}
	return out
}

// sweepLogicalRequestTTL runs only at deterministic preset ingress boundaries.
// It releases the coordinator lock before touching sibling stores or logging.
func (s *Server) sweepLogicalRequestTTL() {
	if s == nil || s.requestCoordinator == nil {
		return
	}
	expired := s.requestCoordinator.sweepExpired(s.requestCoordinator.now(), defaultLogicalRequestSweepLimit)
	for _, item := range expired {
		hadLight := s.lightFlows != nil && s.lightFlows.has(item.RequestID, item.OwnerEdgeID)
		hadArtifact := s.artifactFrontiers != nil && s.artifactFrontiers.has(item.RequestID, item.OwnerEdgeID)
		if s.lightFlows != nil {
			s.lightFlows.remove(item.RequestID, item.OwnerEdgeID)
		}
		if s.artifactFrontiers != nil {
			s.artifactFrontiers.remove(item.RequestID, item.OwnerEdgeID)
		}
		if hadLight || hadArtifact {
			s.observePossibleWorkspaceOrphan(item)
		}
	}
}

func (s *Server) observePossibleWorkspaceOrphan(item logicalRequestExpirySnapshot) {
	if s == nil {
		return
	}
	s.observeHotPathOrphan(context.Background(), hotPathOrphanOutcomeTTLExpired, item.RequestID, item.Stage)
	// Retain the existing redacted diagnostic projection while the closed Hot
	// Path observation is the lifecycle/metric owner.
	if s.logger != nil {
		terminalClass := strings.TrimSpace(item.TerminalClass)
		if terminalClass == "" {
			terminalClass = "inactive"
		}
		s.logger.Info(hotPathOrphanObservationMessage,
			zap.String("request_id", item.RequestID),
			zap.String("workspace_path", newReservedPaths(item.RequestID).JobDir+"/"),
			zap.String("prior_state", string(item.PriorState)),
			zap.String("stage", item.Stage),
			zap.String("terminal_class", terminalClass),
			zap.String("reason", hotPathOrphanReasonTTL),
		)
	}
}
