package agent

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"strings"
	"time"

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

// State returns the current process state.
func (ap *Process) State() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return string(ap.state)
}

// DerivedTurnState returns the daemon-authoritative real Agent turn execution
// state used by Workflow callback recovery:
//
//   - active: a real turn is executing (isIdle=false or a live pending runtime
//     input exists);
//   - queued: the process is idle but a real delivered message is waiting in the
//     inbox for the runtime execution slot (a real queued turn);
//   - idle: the process is up and the session is live with neither an executing
//     turn nor a queued turn (turn ended, inbox empty) — recoverable;
//   - stopping/stopped: idle (the runtime stopped; recoverable while the machine
//     is still connected).
//
// It reads only real Process/dispatcher queue state under ap.mu, never status/
// activity/session presence. The empty string is reserved for "no session" by
// callers (the dispatcher reports unknown in that case).
func (ap *Process) DerivedTurnState() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.derivedTurnStateLocked()
}

func (ap *Process) derivedTurnStateLocked() string {
	if ap.stopping || ap.state == processStateStopped {
		return types.AgentTurnStateIdle
	}
	if !ap.isIdle {
		// A turn is executing (or a wake is actively running).
		return types.AgentTurnStateActive
	}
	if len(ap.inbox) > 0 || len(ap.pendingRuntimeInputs) > 0 {
		// A real delivered message is waiting for/inside the runtime execution
		// slot (accepted-unacked pending or inbox queued).
		return types.AgentTurnStateQueued
	}
	return types.AgentTurnStateIdle
}

// DriverName returns the name of the driver backing this process (e.g. "opencode", "claude").
func (ap *Process) DriverName() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.driver.Name()
}

// Stop requests graceful process termination.
func (ap *Process) Stop() {
	ap.stopWithDeliveryGate(false)
}

type processStopTransition struct {
	owner     bool
	terminate func()
	complete  <-chan struct{}
}

// stopWithDeliveryGate orders Process retirement after every delivery admission
// that already owns deliveryGate. Exactly one caller owns termination; every
// ordinary concurrent caller waits for that owner and then independently drains
// the gate, so public Stop/Sleep remain strong completion boundaries.
func (ap *Process) stopWithDeliveryGate(sleeping bool) {
	transition := ap.prepareStopTransition(sleeping)
	if !transition.owner {
		ap.drainLifecycleMutations()
		if transition.complete != nil {
			<-transition.complete
		}
		ap.drainDeliveryGate()
		return
	}
	defer ap.completeStopTransition()
	transition.terminate()
	ap.drainLifecycleMutations()
	ap.drainDeliveryGate()
}

// prepareStopTransition atomically makes the Process non-accepting and returns
// the runtime termination action. Separating the state transition from
// termination lets recovery mark the Process stopped before durable
// cleanup/session retirement, while delaying onExit until that finalizer has
// fenced the published session.
func (ap *Process) prepareStopTransition(sleeping bool) processStopTransition {
	ap.mu.Lock()
	if ap.stopping {
		complete := ap.stopComplete
		ap.mu.Unlock()
		return processStopTransition{complete: complete}
	}
	// Cancel every gate-held runtime mutation before changing or waiting on any
	// other lifecycle boundary. This unblocks Background callers as well as
	// caller-scoped RPCs; raw stdin writes are separately closed/terminated.
	ap.cancelDeliveryAdmissionLifetime()
	ap.state = processStateStopped
	ap.stopping = true
	ap.sleeping = sleeping
	ap.isIdle = false
	ap.stopInboxRetryLocked()
	ap.cancelReclaimedCorrelationLeaseSweepLocked()
	ap.advanceAckEpochLocked()
	if ap.stopComplete == nil {
		ap.stopComplete = make(chan struct{})
	}
	complete := ap.stopComplete
	proc := ap.cmd
	terminate := ap.terminateFunc
	cancel := ap.cancel
	ap.mu.Unlock()

	return processStopTransition{owner: true, complete: complete, terminate: func() {
		terminateAgentRuntime(proc, terminate)
		if cancel != nil {
			cancel()
		}
	}}
}

func (ap *Process) completeStopTransition() {
	ap.mu.Lock()
	complete := ap.stopComplete
	ap.mu.Unlock()
	if complete != nil {
		ap.stopCompleteOnce.Do(func() { close(complete) })
	}
}

func (ap *Process) drainDeliveryGate() {
	// Do not use acquireDeliveryGate: Stop has intentionally cancelled the
	// admission lifetime that ordinary acquisition observes.
	if err := ap.acquireDeliveryGateForDrain(context.Background()); err != nil {
		return
	}
	ap.releaseDeliveryGate()
}

// stopWhileDeliveryGateHeld performs the complete stop transition. The caller
// holds deliveryGate; public callers must use Stop or Sleep instead.
func (ap *Process) stopWhileDeliveryGateHeld(sleeping bool) {
	transition := ap.prepareStopTransition(sleeping)
	if transition.owner {
		transition.terminate()
		ap.drainLifecycleMutations()
		ap.completeStopTransition()
	}
}

// beginLifecycleMutation admits a synchronous daemon-state mutation that may
// be reached reentrantly from a runtime server/tool request. Runtime callbacks
// must not acquire deliveryGate, because the outer request can already own
// that non-reentrant gate. Already-admitted delivery transport may hold the
// outer gate while taking this read side. In either case, Stop/onExit get the
// same lifecycle guarantee: no new mutation begins after the Process is
// non-accepting, and an admitted mutation is drained before retirement returns.
func (ap *Process) beginLifecycleMutation() bool {
	ap.lifecycleMutationGate.RLock()
	ap.mu.Lock()
	accepting := !ap.stopping && ap.state != processStateStopped
	ap.mu.Unlock()
	if !accepting {
		ap.lifecycleMutationGate.RUnlock()
		return false
	}
	return true
}

func (ap *Process) endLifecycleMutation() {
	ap.lifecycleMutationGate.RUnlock()
}

func (ap *Process) drainLifecycleMutations() {
	ap.lifecycleMutationGate.Lock()
	ap.lifecycleMutationGate.Unlock()
}

// Sleep puts the agent to sleep (graceful termination, preserving session state).
// Unlike Stop, the onExit callback should NOT remove the session from the manager.
func (ap *Process) Sleep() {
	ap.stopWithDeliveryGate(true)
}

// IsSleeping returns whether the process was explicitly put to sleep.
func (ap *Process) IsSleeping() bool {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.sleeping
}

// AcceptingDeliveries lets the dispatcher fence the Stop/Sleep-to-onExit
// window before treating a still-published Process pointer as a live target.
func (ap *Process) AcceptingDeliveries() bool {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return !ap.stopping && ap.state != processStateStopped
}

// ShouldAutoContinueAcceptedDeliveries is true for an unexpected runtime
// boundary and for the daemon's internal context-window recovery sleep.
// Explicit user Stop/Sleep preserves durable ownership for the next explicit
// wake and must not immediately respawn the process, but internal recovery may
// not strand accepted input behind its synthetic continuation prompt.
func (ap *Process) ShouldAutoContinueAcceptedDeliveries() bool {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.contextWindowExceeded && ap.sleeping {
		return true
	}
	return !ap.stopping && !ap.sleeping
}

// WaitForExit blocks until the underlying process exits or the timeout expires.
// Returns true if the process exited within the timeout.
func (ap *Process) WaitForExit(timeout time.Duration) bool {
	ap.mu.Lock()
	exited := ap.exited
	ap.mu.Unlock()
	if exited != nil {
		if timeout <= 0 {
			select {
			case <-exited:
				return true
			default:
				return false
			}
		}
		timer := time.NewTimer(timeout)
		defer timer.Stop()
		select {
		case <-exited:
			return true
		case <-timer.C:
			return false
		}
	}

	timer := time.NewTimer(timeout)
	defer timer.Stop()
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()
	for {
		ap.mu.Lock()
		proc := ap.cmd
		state := ap.state
		ap.mu.Unlock()

		if proc == nil {
			if state == processStateStopped {
				return true
			}
		} else if runtimeCommandExited(proc) {
			return true
		}
		select {
		case <-timer.C:
			return false
		case <-ticker.C:
		}
	}
}

// WaitForExitCallback waits until the dispatcher/user exit callback has
// returned. This fence is deliberately independent from WaitForExit: callers
// that only need child-process semantics must not inherit callback latency,
// while strong owners such as Dispatcher.Close can explicitly join every
// callback side effect before returning.
func (ap *Process) WaitForExitCallback(ctx context.Context) bool {
	if ap == nil {
		return true
	}
	ap.mu.Lock()
	done := ap.exitCallbackDone
	ap.mu.Unlock()
	// Preserve compatibility for test-only/manual Process values constructed
	// before this lifecycle fence existed. NewProcess always installs the
	// channel, so production never takes this branch.
	if done == nil {
		return true
	}
	if ctx == nil {
		ctx = context.Background()
	}
	select {
	case <-done:
		return true
	case <-ctx.Done():
		return false
	}
}

// CmdProcessState returns the process state of the underlying command.
// Returns nil if the command has not been set or has not exited yet.
func (ap *Process) CmdProcessState() *os.ProcessState {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.cmd == nil {
		return nil
	}
	return ap.cmd.ProcessState
}

// StartupFailure returns the first diagnostic observed when a runtime exits
// before Dispatcher can publish the Process generation. In particular, this
// preserves a Pi catalog/provider error instead of reducing it to a bare exit
// code that would cause durable continuation to retry or obscure the root
// cause.
func (ap *Process) StartupFailure() error {
	ap.mu.Lock()
	detail := strings.TrimSpace(ap.runtimeErrorDetail)
	stderrTail := ap.stderrTail
	exitCode := ap.exitCode
	driver := ap.driver
	ap.mu.Unlock()
	if detail == "" && stderrTail != nil {
		detail = strings.TrimSpace(stderrTail())
	}
	if detail == "" {
		detail = fmt.Sprintf("runtime process exited before launch registration (exit code %d)", exitCode)
	} else {
		detail = fmt.Sprintf("runtime process exited before launch registration: %s", detail)
	}
	if driver != nil && strings.EqualFold(strings.TrimSpace(driver.Name()), "pi") {
		return piStartupFailure(fmt.Errorf("%s", detail))
	}
	return fmt.Errorf("%s", detail)
}

// PID returns the operating-system PID of the runtime root process, or 0 when
// the process has not started a command-backed runtime.
func (ap *Process) PID() int {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.cmd == nil || ap.cmd.Process == nil {
		return 0
	}
	return ap.cmd.Process.Pid
}

// WorkDir returns the workspace directory for this agent process.
func (ap *Process) WorkDir() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.workDir
}

// SetRuntimeAttachmentResolver sets the optional resolver used for runtimes
// that can consume structured message attachments.
func (ap *Process) SetRuntimeAttachmentResolver(resolver types.RuntimeAttachmentResolver) {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	ap.attachmentResolver = resolver
}

// GetSessionID returns the current session ID for this agent process.
func (ap *Process) GetSessionID() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.SessionID
}

// PromptSessionFreshness returns whether the spawned runtime session was fresh
// or successfully resumed.
func (ap *Process) PromptSessionFreshness() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.sessionFreshness
}

// ActiveTokenAnalysisLaunchID returns the body-free launch identity while this
// Process is still active. It is used only to label point-in-time diagnostics;
// portable reports remove Run detail.
func (ap *Process) ActiveTokenAnalysisLaunchID() string {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.stopping || ap.state == processStateStopped {
		return ""
	}
	return strings.TrimSpace(ap.launchID)
}
func (ap *Process) waitProcess() {
	exitCode := -1
	var err error

	ap.mu.Lock()
	waitFunc := ap.waitFunc
	cmd := ap.cmd
	ap.mu.Unlock()

	if waitFunc != nil {
		err = waitFunc()
		if cmd != nil && cmd.ProcessState != nil {
			exitCode = cmd.ProcessState.ExitCode()
		} else if err == nil {
			exitCode = 0
		}
	} else if cmd != nil {
		err = cmd.Wait()
		ps := cmd.ProcessState
		if ps != nil {
			exitCode = ps.ExitCode()
		}
	} else {
		exitCode = 0
	}

	// waitFunc/cmd.Wait is the proof boundary that the runtime generation and
	// its final child-response transport can no longer complete a Deferred MCP
	// second hop. Release those exact lifecycle owners before the retirement
	// drain below; direct HTTP first hops are intentionally not swept here.
	ap.settleTransferredRuntimeDeliveryLeasesAfterTransportRetired()

	// Fence new admission immediately after the runtime boundary, then drain the
	// admission that may already own deliveryGate before exposing onExit. An
	// unexpected child exit can race a DeliverForHandoff that is still rendering
	// attachments or fsyncing its first owner; starting a replacement from onExit
	// before that Put completes would attach the new Process to an empty journal
	// and strand the later owner. Do not set stopping here: unexpected exits still
	// need ShouldAutoContinueAcceptedDeliveries to launch their continuation.
	ap.cancelDeliveryAdmissionLifetime()
	ap.mu.Lock()
	ap.state = processStateStopped
	ap.advanceAckEpochLocked()
	ap.mu.Unlock()
	ap.drainLifecycleMutations()
	gateAcquired := ap.acquireDeliveryGateForDrain(context.Background()) == nil
	slog.Info("interactive agent exited", "agent_id", ap.AgentID, "error", err, "exit_code", exitCode)

	// Emit "failed" wake status if the agent exited with an error while a
	// delivery obligation is still pending.
	ap.mu.Lock()
	wake := ap.pendingWake
	token := ap.pendingWakeToken
	stopping := ap.stopping
	runtimeErrorDetail := ap.runtimeErrorDetail
	runtimeErrorEventEmitted := ap.runtimeErrorEventEmitted
	runtimeErrorWakeFailed := ap.runtimeErrorWakeFailed
	ap.mu.Unlock()

	exitDetail := fmt.Sprintf("process exited with code %d", exitCode)
	if err != nil {
		exitDetail = err.Error()
	}
	if runtimeErrorDetail != "" {
		exitDetail = runtimeErrorDetail
	} else {
		exitDetail = ap.detailWithStderr(exitDetail)
	}

	failedWakeExit := wake != nil && !runtimeErrorWakeFailed && !stopping
	if failedWakeExit {
		detail := exitDetail
		ap.mu.Lock()
		events := ap.terminalAllWakeStatusesLocked(consts.WakeStatusFailed, detail)
		ap.mu.Unlock()
		for _, event := range events {
			ap.emitWakeStatus(event)
		}
	}

	if !stopping && (exitCode != 0 || failedWakeExit || runtimeErrorDetail != "") && !runtimeErrorEventEmitted {
		ap.emitRuntimeEvent(RuntimeEvent{
			MessageID: func() string {
				if wake != nil {
					return wake.Wake.MessageID
				}
				return ""
			}(),
			WakeToken:  token,
			EventClass: consts.RuntimeEventClassRuntimeExitedUnexpectedly,
			Detail:     exitDetail,
		})
	}

	ap.mu.Lock()
	stdinPipe := ap.stdinPipe
	ap.stdinPipe = nil
	ap.stopInboxRetryLocked()
	onExit := ap.onExit
	cleanup := ap.cleanup
	ap.cleanup = nil
	ap.pendingPromptMetrics = nil
	ap.launchPromptMetrics = nil
	analysisToken := ap.analysisToken
	ap.analysisToken = ""
	ap.exitCode = exitCode
	var stopComplete <-chan struct{}
	if ap.stopping {
		stopComplete = ap.stopComplete
	}
	ap.mu.Unlock()
	// state=stopped prevents any later caller from entering runtime mutation, so
	// the gate can be released after the drain and before invoking cleanup or
	// dispatcher/user callbacks. This also avoids making a defensive Stop from an
	// external callback re-enter a non-reentrant gate.
	if gateAcquired {
		ap.releaseDeliveryGate()
	}
	// A public Stop may have captured the exact callback invalidator before
	// waitProcess observed the child exit. In that interleaving waitProcess has
	// no invalidator of its own, so publishing onExit now could replace the
	// session/Exec pointer before Stop clears the old generation. Release the
	// non-reentrant delivery gate first (the Stop owner needs it), then join the
	// owner's post-drain invalidation boundary before exposing a replacement.
	if stopComplete != nil {
		<-stopComplete
	}

	if stdinPipe != nil {
		logIgnoredCleanupError("failed to close runtime stdin pipe after exit", stdinPipe.Close(), "agent_id", ap.AgentID)
	}
	if cleanup != nil {
		cleanup()
	}
	if analysisToken != "" && revokeRuntimeAnalysisToken != nil {
		revokeRuntimeAnalysisToken(analysisToken)
	}
	ap.signalExited()
	defer ap.signalExitCallbackDone()
	if onExit != nil {
		onExit(ap)
	}
}

func terminateAgentRuntime(cmd *exec.Cmd, terminate func()) {
	if terminate != nil {
		terminate()
		return
	}
	terminateRuntimeCommand(cmd)
}

// ExitSummary returns the exit code and retains the existing dispatcher
// boundary. Visible-reply settlement is owned by delivery status rows rather
// than a process-wide scalar.
func (ap *Process) ExitSummary() (int, bool) {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.exitCode, false
}

func (ap *Process) signalExited() {
	ap.mu.Lock()
	exited := ap.exited
	ap.mu.Unlock()
	if exited == nil {
		return
	}
	ap.exitOnce.Do(func() {
		close(exited)
	})
}

func (ap *Process) signalExitCallbackDone() {
	ap.mu.Lock()
	done := ap.exitCallbackDone
	ap.mu.Unlock()
	if done == nil {
		return
	}
	ap.exitCallbackDoneOnce.Do(func() {
		close(done)
	})
}
