package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/metrics"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const (
	idleCompactionTimeout = 5 * time.Second
	maxProcessLineBytes   = 32 * 1024 * 1024
	maxDeliveryReceipts   = 64
	maxTerminalMessages   = 512
)

var removeBuiltinSkillSource = os.RemoveAll

// WakeInfo holds the metadata for a acknowledged delivery and emits its bounded
// wake status updates.
type WakeInfo struct {
	MessageID string
	Target    string
	// TargetKey and TargetSeq are the server-authoritative identity of the
	// acknowledged message's resolved target. Visible-reply settlement uses these
	// fields rather than a local delivery token so one successful server send
	// can cover every acknowledged message in the same target through its frontier.
	TargetKey     string
	TargetSeq     int64
	MessageType   string
	DeliveryClass DeliveryClass
}

// DeliveryClass separates ordinary visible-reply obligations from signals that
// have their own lifecycle (for example Workflow callbacks, reactions, and
// system messages). It is delivery classification, not a compatibility or
// reply-state flag.
type DeliveryClass string

const (
	DeliveryClassVisibleReply DeliveryClass = "visible_reply"
	DeliveryClassSignalOnly   DeliveryClass = "signal_only"
)

// DeliveryClassForMessage derives the lightweight lifecycle category from
// trusted normalized delivery facts.
func DeliveryClassForMessage(msg types.InboxMessage) DeliveryClass {
	if types.MessageHasReplyAuthority(msg) {
		return DeliveryClassVisibleReply
	}
	return DeliveryClassSignalOnly
}

type pendingDeliveryStatus uint8

const (
	pendingDeliveryAwaitingCheck pendingDeliveryStatus = iota
	pendingDeliveryChecked
)

// pendingDelivery is the in-memory ownership/status row for one acknowledged
// message. Message identity remains per delivery for terminal wake events;
// final-check admission groups these rows by resolved TargetKey.
type pendingDelivery struct {
	Wake   WakeInfo
	Token  string
	Status pendingDeliveryStatus
}

// WakeStatusEvent carries wake status information emitted by Process
// to the caller (typically Dispatcher), which then sends it to the channel.
type WakeStatusEvent struct {
	MessageID      string
	WakeToken      string
	Target         string
	Status         string // received | reply_sent | completed_without_visible_reply | failed
	ReplyMessageID string
	Detail         string
}

// RuntimeEvent carries legacy process-to-dispatcher runtime diagnostic data.
// Dispatcher maps this bridge shape into runtimeevents.Event and eventually the
// channel-facing types.RuntimeEvent wire payload.
type RuntimeEvent struct {
	MessageID      string
	WakeToken      string
	EventClass     string
	Target         string
	ReplyMessageID string
	Detail         string
}

// ProcessOptions contains daemon-owned process state and lifecycle observers.
// Drivers receive only LaunchConfig.
type ProcessOptions struct {
	WorkflowCompaction *WorkflowCompactionGate
	Wake               *WakeInfo
	// InitialDelivery is the full wake message whose prompt the launch
	// surfaces when the initial turn is visible. It seeds the turn
	// message-send scope with the message's exact carried targets.
	InitialDelivery *types.InboxMessage
	// OnProcessConstructed runs after the Process is built and its initial
	// message-send scope is seeded but BEFORE driver.Spawn, so the launcher
	// can publish launch-window bindings that first-turn tool calls (which may
	// start inside Spawn) resolve against.
	OnProcessConstructed func(*Process)
	OnExit               func(*Process)
	OnEvent              func(drivercontract.ParsedEvent)
	OnWakeStatus         func(WakeStatusEvent)
	OnRuntimeEvent       func(RuntimeEvent)
}

// processState represents the lifecycle state of an interactive agent process.

// pendingRuntimeInput tracks active-turn direct input that the runtime transport
// accepted but has not yet acknowledged as model-visible. It is deliberately
// outside the daemon-local inbox so internal inbox readers do not return content
// that has already been handed to a runtime-specific pending queue.
type pendingRuntimeInput struct {
	Message                    types.InboxMessage
	Input                      drivercontract.RuntimeInput
	SessionID                  string
	WakeToken                  string
	CreatedAt                  time.Time
	InflightReason             DeliveryOwnershipInflightReason
	ReconcileAttempts          int
	Restored                   bool
	FinalHopDeferred           bool
	FinalHopRuntimeTransferred bool
	finalHopLeaseGeneration    uint64
	finalHopLeaseDeadline      time.Time
	finalHopLeaseCancel        chan struct{}
	finalHopLifecycleLease     *runtimeDeliveryLifecycleLease
	ackEpoch                   uint64
	sendScopeGen               uint64
	reconcileRunning           bool
	reconcileGeneration        uint64
	pendingStatusDone          chan struct{}
	pendingAckedEvent          *drivercontract.ParsedEvent
	reclaimed                  bool
	reclaimedAt                time.Time
}

type processState string

const (
	processStateStarting processState = "starting"
	processStateRunning  processState = "running"
	processStateStopped  processState = "stopped"
)

type processRuntimeBinding struct {
	cmd             *exec.Cmd
	waitFunc        func() error
	terminateFunc   func()
	stdinPipe       io.WriteCloser
	stderrTail      func() string
	driver          drivercontract.Driver
	cancel          context.CancelFunc
	cleanup         func()
	analysisToken   string
	onExit          func(*Process)
	onEvent         func(drivercontract.ParsedEvent)       // called after each parsed event
	onRecoveryPoint func(types.RecoveryPointPreparedEvent) // called when a recovery point is prepared
	onWakeStatus    func(WakeStatusEvent)                  // callback to emit wake status to channel
	onRuntimeEvent  func(RuntimeEvent)
	onResumeSafety  func(version uint64, safe bool, sessionID string) error // callback to persist versioned resume safety state
}

type processLifecycleState struct {
	state                processState
	stopping             bool // true after Stop/Sleep is called; suppresses stale events
	sleeping             bool // true if process was explicitly put to sleep
	isIdle               bool // true after turn_end with empty inbox; waiting for stdin delivery
	exitCode             int
	exited               chan struct{}
	exitOnce             sync.Once
	exitCallbackDone     chan struct{}
	exitCallbackDoneOnce sync.Once
	stopComplete         chan struct{}
	stopCompleteOnce     sync.Once
}

type processDeliveryState struct {
	inbox                       []types.InboxMessage
	pendingRuntimeInputs        map[string]pendingRuntimeInput
	deliveryOwnershipStore      DeliveryOwnershipStore
	deliveryOwnershipStoreEpoch uint64
	deliveryOwnershipHandoffID  string
	// deliveryReconcileReservations prevents an inbox retry from claiming a
	// body while a reentrant MCP/read-history reconciliation is durably moving
	// that same owner to acked. Values are per-message mutation generations.
	deliveryReconcileReservations map[string]uint64
	deliveryReconcileGeneration   uint64
	ackedMessages                 []string
	ackedSet                      map[string]struct{}
	failedMessages                []string
	failedSet                     map[string]struct{}
	freshness                     types.SessionFreshnessState
	runtimeHandoffEpoch           uint64
	inboxRetryTimer               *time.Timer
	inboxRetryGeneration          uint64
	inboxRetryAttempt             int
	inboxRetryMessageID           string
	inboxRetryStartedAt           time.Time
	inboxRetryLastError           string
	inboxRetryLastClass           deliveryRetryClass
	inboxRetryMaxAttempts         int
	inboxRetryMaxElapsed          time.Duration
	inboxRetryBoundaryMaxElapsed  time.Duration
	inboxRetryRunning             bool
	inboxRetryImmediate           bool
	inboxRetryDelay               func(int) time.Duration
	inboxRetryCancel              context.CancelFunc
	pendingReconcileMaxAttempts   int
	pendingReconcileMaxElapsed    time.Duration
	pendingReconcileSuccessGrace  time.Duration
	// reclaimedCorrelationLeaseTTL overrides reclaimedCorrelationLeaseTTL when
	// positive; production leaves it zero to use the package default. The
	// reclaimed lease sweep timer (reclaimedLeaseSweep*) drives eviction on this
	// deadline even when no further reclaim admission arrives.
	reclaimedCorrelationLeaseTTL    time.Duration
	reclaimedLeaseSweepDeadline     time.Time
	reclaimedLeaseSweepCancel       chan struct{}
	reclaimedLeaseSweepGeneration   uint64
	runtimeDeliveryDeferredLease    time.Duration
	runtimeDeliveryReceiptGenerator func() (string, error)
	runtimeDeliveryReceipts         []string
	runtimeDeliveryReceiptSet       map[string]struct{}
	attachmentResolver              types.RuntimeAttachmentResolver
}

type processRecoveryState struct {
	workDir                       string // workspace directory for local recovery state
	disableRecovery               bool
	resumeSafetyKnown             bool   // latest Codex daemon-restart safety transition is available
	resumeSafety                  bool   // latest Codex daemon-restart safety value
	resumeSafetyVersion           uint64 // orders callback replay against newer runtime transitions
	lastCheckpointKey             string // dedup: skip identical consecutive checkpoints
	pendingPreparedRecoveryPoints []types.RecoveryPointPreparedEvent
	contextWindowExceeded         bool       // set when the current turn exceeds the runtime context window
	contextRecoveryWakes          []WakeInfo // exact wake targets for dispatcher-managed recovery or same-session compaction continuation
	contextCompactionContinuation bool       // set after runtime-owned compaction starts and a retry prompt must be injected at the next idle boundary
	contextCompactionInProgress   bool       // set while waiting for the runtime-owned context compaction completion boundary
	outputTruncationDetected      bool       // set when the current response exhausts only its output-token budget
}

type processWakeState struct {
	pendingWake          *pendingDelivery  // nil if started without a wake message
	pendingWakeToken     string            // random 32-hex token for this wake delivery
	pendingDeliveryQueue []pendingDelivery // FIFO deliveries behind pendingWake
	currentTurnMessageID string            // message ID of the current turn's triggering delivery
}

type processUsageState struct {
	model                   string // model identifier (e.g., "claude-sonnet-4-20250514")
	providerAPIKey          string // provider API key, used as the apiKey dimension in metrics
	launchID                string
	launchStartedAt         time.Time
	runtime                 string
	sessionKind             string
	sessionFreshness        string
	pendingPromptMetrics    map[string]promptMetricsBoundary
	launchPromptMetrics     []metrics.PromptCompileRecord
	activePromptMetrics     map[string]metrics.InvocationExposureBlock
	promptMetricsBaselineOK bool
	contextExposureComplete bool
	launchMetricsWritten    bool
	tokenSourceState
	tokenSourceRecorder      tokenSourceRecorder
	tokenSourceAuditRecorder tokenSourceAuditRecorder
}

type processRuntimeFailureState struct {
	runtimeErrorDetail                    string
	runtimeErrorEventEmitted              bool
	runtimeErrorWakeFailed                bool
	transientRuntimeRetry                 transientRuntimeRetryState
	transientRuntimeRetryPending          bool
	transientRuntimeRetryAwaitingTurnEnd  bool
	transientRuntimeRetryRedelivered      bool
	transientRuntimeRetryToolCallObserved bool
}

// Process manages one interactive runtime process and its inbox/idle state.
type Process struct {
	mu sync.Mutex
	// deliveryOwnershipMutationMu serializes Process-local journal Put/Delete
	// operations that otherwise cross while ap.mu is intentionally released.
	// The durable dispatcher store additionally enforces terminal-monotonic
	// transitions across Process generations.
	deliveryOwnershipMutationMu sync.Mutex
	// lifecycleMutationGate admits synchronous daemon-state mutations initiated
	// by runtime tool callbacks and already-admitted runtime transport calls that
	// cannot safely acquire deliveryGate. Stop and unexpected exit take the
	// exclusive side after marking the Process non-accepting, which drains
	// already-entered mutations before session cleanup.
	lifecycleMutationGate sync.RWMutex
	// resumeSafetyNotifyMu serializes durable resume-safety callbacks without
	// holding ap.mu (the callback reads Process session state). Together with
	// resumeSafetyVersion it prevents a late callback-install replay from
	// overwriting a newer TurnEnd/TurnStarted transition.
	resumeSafetyNotifyMu sync.Mutex
	// deliveryGate serializes concrete AgentTeam message admission through rendering
	// and runtime transport. ap.mu protects the state containers themselves, but
	// it is intentionally released around bounded RPCs; without this outer
	// ordering gate, concurrent websocket/signal deliveries can both observe no
	// owner and issue out-of-order (or duplicate) runtime inputs. The channel
	// gate, unlike a bare mutex, lets handoff callers honor their context while
	// waiting behind an older delivery.
	deliveryGateOnce sync.Once
	deliveryGate     chan struct{}
	// deliveryAdmissionLifetime is cancelled before Stop waits for deliveryGate.
	// Bounded runtime transport mutations derive from it, so a caller that
	// supplied context.Background cannot leave retirement waiting forever on an
	// RPC; admitted compiler/attachment work uses its own local timeout so it can
	// persist the first owner before replacement exposure.
	deliveryAdmissionLifetimeOnce   sync.Once
	deliveryAdmissionLifetime       context.Context
	deliveryAdmissionLifetimeCancel context.CancelFunc

	AgentID   string
	SessionID string

	processRuntimeBinding
	processLifecycleState
	processDeliveryState
	processRecoveryState
	processWakeState
	processUsageState
	processRuntimeFailureState
	// ackEpoch fences late runtime acknowledgements from a prior
	// provider turn. It is delivery lifecycle state, not model-visible routing
	// authority.
	ackEpoch uint64
	// sendScope is the turn's message-send scope: the exact sendable targets
	// delivered in the current authoritative request. Produced only at trusted
	// runtime-delivery evidence points, retired at authoritative turn
	// boundaries, consumed only by the message_send guard.
	sendScope    sendScopeState
	turnDelivery turnDeliveryState

	workflowCompaction *WorkflowCompactionGate
	exec               *processExec
}

type processExec struct {
	authorizationID string
}

// ExecAuthorizationID returns the daemon-only identifier for this Process's
// immutable Exec runtime authorization.
func (ap *Process) ExecAuthorizationID() string {
	if ap == nil || ap.exec == nil {
		return ""
	}
	return strings.TrimSpace(ap.exec.authorizationID)
}

func launchConfigStartsTurn(cfg drivercontract.LaunchConfig) bool {
	// A launch starts a turn only when the runtime will actually write a first
	// input frame. Claude enforces the authoritative no-prompt gate: a true
	// no-text/no-attachment launch does not start a turn, so the Process is idle
	// and the first durable handoff writes the first real frame. This mirrors the
	// ClaudeDriver.Spawn gate so idle resume-safety stays correct.
	if len(cfg.PromptAttachments) > 0 {
		return true
	}
	if cfg.CompiledPrompt == nil || !cfg.CompiledPrompt.HasLaunchCarrier() {
		return false
	}
	return strings.TrimSpace(cfg.CompiledPrompt.LaunchSurface("prompt")) != "" ||
		strings.TrimSpace(cfg.CompiledPrompt.LaunchSurface("freshPrompt")) != "" ||
		strings.TrimSpace(cfg.CompiledPrompt.LaunchSurface("resumePrompt")) != ""
}

// NewProcess launches a new interactive agent runtime.
// If wake is non-nil, the process tracks the wake-to-reply lifecycle and
// emits wake status events via onWakeStatus.
func NewProcess(
	ctx context.Context,
	driver drivercontract.Driver,
	cfg drivercontract.LaunchConfig,
	options ProcessOptions,
) (*Process, error) {
	agentCtx, cancel := context.WithCancel(ctx)
	ap := &Process{
		AgentID: cfg.AgentID,
		processRuntimeBinding: processRuntimeBinding{
			driver:         driver,
			cancel:         cancel,
			onExit:         options.OnExit,
			onEvent:        options.OnEvent,
			onWakeStatus:   options.OnWakeStatus,
			onRuntimeEvent: options.OnRuntimeEvent,
			analysisToken:  strings.TrimSpace(cfg.AnalysisToken),
		},
		processLifecycleState: processLifecycleState{
			state:            processStateStarting,
			exitCode:         -1,
			exited:           make(chan struct{}),
			exitCallbackDone: make(chan struct{}),
		},
		processRecoveryState: processRecoveryState{
			workDir:         cfg.WorkDir,
			disableRecovery: cfg.DisableRecovery,
		},
		processWakeState: processWakeState{},
		processUsageState: processUsageState{
			model:                    strings.TrimSpace(cfg.Model),
			launchID:                 strings.TrimSpace(cfg.LaunchID),
			launchStartedAt:          time.Now().UTC(),
			runtime:                  string(runtimeconfig.EffectiveRuntimeType(cfg.RuntimeType)),
			sessionKind:              "interactive",
			sessionFreshness:         sessionFreshnessForLaunch(cfg),
			tokenSourceRecorder:      defaultTokenSourceRecord,
			tokenSourceAuditRecorder: defaultTokenSourceAuditRecord,
		},
	}
	processOwnsAnalysisToken := false
	defer ap.revokeUnownedAnalysisToken(&processOwnsAnalysisToken)
	if cfg.Exec != nil {
		ap.exec = &processExec{authorizationID: cfg.Exec.AuthorizationID}
	}
	if cfg.Provider != nil {
		ap.providerAPIKey = strings.TrimSpace(cfg.Provider.APIKey)
		if metricsKey := strings.TrimSpace(cfg.Provider.MetricsAPIKey); metricsKey != "" {
			ap.providerAPIKey = metricsKey
		}
	}

	ap.beginInitialWake(options.Wake)
	ap.seedInitialDeliveryScope(options.InitialDelivery)
	ap.seedInitialTurnDeliveryLocked(options.InitialDelivery)
	ap.workflowCompaction = options.WorkflowCompaction
	if ap.workflowCompaction == nil {
		ap.workflowCompaction = NewWorkflowCompactionGate()
	}
	if err := materializeSkillsForLaunch(cfg); err != nil {
		cancel()
		return nil, err
	}
	if options.OnProcessConstructed != nil {
		options.OnProcessConstructed(ap)
	}

	spawned, err := driver.Spawn(agentCtx, cfg)
	if err != nil {
		cancel()
		return nil, err
	}

	ap.cmd = spawned.Cmd
	ap.waitFunc = spawned.Wait
	ap.terminateFunc = spawned.Terminate
	ap.stdinPipe = spawned.Stdin
	ap.stderrTail = spawned.StderrTail
	ap.cleanup = spawned.Cleanup
	ap.state = processStateRunning
	ap.isIdle = !launchConfigStartsTurn(cfg)
	ap.SessionID = strings.TrimSpace(spawned.SessionID)
	if ap.SessionID != "" {
		if spawned.SessionResumed {
			ap.sessionFreshness = metrics.PromptSessionFreshnessResume
		} else {
			ap.sessionFreshness = metrics.PromptSessionFreshnessFresh
		}
	}
	if fallback := spawned.FreshSessionFallback; fallback != nil {
		// Drivers perform this cutover exactly once, only after an authoritative
		// invalid/not-found response. Transport-ambiguous input remains under
		// runtime delivery ownership and never reaches this event.
		ap.emitRuntimeEvent(RuntimeEvent{
			EventClass: consts.RuntimeEventClassRuntimeRetrying,
			Detail:     sessionResumeFallbackDetail(cfg, *fallback),
		})
	}
	ap.recordSpawnTransientRuntimeRetryPoint(spawned.PreparedRecoveryPoint)
	ap.rememberTransientRuntimeRetryInput(cfg.InitialDeliveryMessageID, initialRuntimeInputForTransientRetry(cfg), launchSessionIDForTransientRetry(cfg, spawned.PreparedRecoveryPoint))
	if id := strings.TrimSpace(cfg.InitialDeliveryMessageID); id != "" && strings.TrimSpace(ap.currentTurnMessageID) == "" {
		ap.currentTurnMessageID = id
	}
	ap.recordSpawnPreparedRecoveryPoint(spawned.PreparedRecoveryPoint)

	slog.Info("agent process started",
		"agent_id", cfg.AgentID,
		"mcp_count", len(cfg.MCPs),
		"work_dir", cfg.WorkDir,
		"model", cfg.Model,
	)

	processOwnsAnalysisToken = true
	go ap.parseStdout(spawned.Stdout)
	go ap.waitProcess()

	return ap, nil
}

func sessionFreshnessForLaunch(cfg drivercontract.LaunchConfig) string {
	if strings.TrimSpace(cfg.SessionID) != "" {
		return metrics.PromptSessionFreshnessResume
	}
	return metrics.PromptSessionFreshnessFresh
}

func sessionResumeFallbackDetail(cfg drivercontract.LaunchConfig, fallback drivercontract.SessionResumeFallback) string {
	reason := strings.TrimSpace(fallback.Reason)
	if reason == "" {
		reason = "invalid_or_not_found_session"
	}
	operation := strings.TrimSpace(fallback.Operation)
	payload := map[string]any{
		"code":      "provider_session_fresh_fallback",
		"attempt":   1,
		"operation": operation,
		"reason":    reason,
		"runtime":   strings.TrimSpace(string(runtimeconfig.EffectiveRuntimeType(cfg.RuntimeType))),
	}
	data, err := json.Marshal(payload)
	if err != nil {
		return fmt.Sprintf("provider_session_fresh_fallback runtime=%s reason=%s operation=%s attempt=1",
			payload["runtime"], reason, operation)
	}
	return string(data)
}

// CurrentTurnMessageID returns the message ID of the current turn's triggering
// delivery. It is the daemon-authoritative source for human-attested upstream
// calls (e.g. KB retrieval via /agent-thread/upstream). The fallback chain is:
// dedicated currentTurnMessageID field (set at launch and wake-cycle
// activation) -> pendingWake.Wake.MessageID (mid-session) -> transientRuntimeRetry
// .MessageID (initial start). Returns "" when no turn is active.
func (ap *Process) CurrentTurnMessageID() string {
	if ap == nil {
		return ""
	}
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if id := strings.TrimSpace(ap.currentTurnMessageID); id != "" {
		return id
	}
	if ap.pendingWake != nil {
		if id := strings.TrimSpace(ap.pendingWake.Wake.MessageID); id != "" {
			return id
		}
	}
	return strings.TrimSpace(ap.transientRuntimeRetry.MessageID)
}

// materializeSkillsForLaunch builds the runtime skill sources from the launch
// config and materializes them into the runtime's workdir-specific skill
// location. The source list is a snapshot taken at launch time; later changes
// to the on-disk vault set do not affect the running process until the next
// launch (next-start semantics).
func materializeSkillsForLaunch(cfg drivercontract.LaunchConfig) error {
	var sources []runtimeconfig.RuntimeSkillSource
	if len(cfg.SkillPlan.SourceSlots) > 0 {
		for _, slot := range cfg.SkillPlan.SourceSlots {
			sources = append(sources, runtimeconfig.RuntimeSkillSource{
				Dir:      slot.Dir,
				Priority: slot.Priority,
				Origin:   slot.Origin,
			})
		}
	} else {
		sources = []runtimeconfig.RuntimeSkillSource{{
			Dir:      cfg.SkillDir,
			Priority: runtimeconfig.RuntimeSkillPriorityProfile,
			Origin:   runtimeconfig.RuntimeSkillOriginProfile,
		}}
		for _, dir := range cfg.ManagedSkillDirs {
			sources = append(sources, runtimeconfig.RuntimeSkillSource{
				Dir:      dir,
				Priority: runtimeconfig.RuntimeSkillPriorityManaged,
				Origin:   runtimeconfig.RuntimeSkillOriginManaged,
			})
		}
	}
	builtinDir, cleanup, err := writeBuiltinSkillSource(cfg.BuiltinSkills)
	if err != nil {
		return err
	}
	if cleanup != nil {
		defer cleanup()
	}
	if builtinDir != "" {
		sources = append(sources, runtimeconfig.RuntimeSkillSource{
			Dir:      builtinDir,
			Priority: runtimeconfig.RuntimeSkillPriorityBuiltin,
			Origin:   runtimeconfig.RuntimeSkillOriginBuiltin,
		})
	}
	return runtimeconfig.MaterializeRuntimeSkillsFromSources(cfg.RuntimeType, cfg.WorkDir, sources)
}

func writeBuiltinSkillSource(skills []drivercontract.BuiltinSkill) (string, func(), error) {
	if len(skills) == 0 {
		return "", nil, nil
	}
	root, err := os.MkdirTemp("", "agentteam-builtin-skills-*")
	if err != nil {
		return "", nil, fmt.Errorf("create builtin skill source: %w", err)
	}
	cleanup := func() {
		if err := removeBuiltinSkillSource(root); err != nil {
			slog.Warn("failed to remove builtin skill temporary source", "path", root, "error", err)
		}
	}
	for _, skill := range skills {
		name := strings.TrimSpace(skill.Name)
		if name == "" || filepath.Base(name) != name || name == "." {
			cleanup()
			return "", nil, fmt.Errorf("invalid builtin skill name %q", skill.Name)
		}
		dir := filepath.Join(root, "skills", name)
		if err := os.MkdirAll(dir, 0o700); err != nil {
			cleanup()
			return "", nil, fmt.Errorf("create builtin skill %s: %w", name, err)
		}
		if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(skill.Content), 0o600); err != nil {
			cleanup()
			return "", nil, fmt.Errorf("write builtin skill %s: %w", name, err)
		}
	}
	return root, cleanup, nil
}
