package service

import (
	"context"
	"slices"
	"strings"
	"time"

	iop "iop/proto/gen/iop"
)

type singleRequestWorkspaceToolRuntime interface {
	workspaceOpen(context.Context, *SingleRequestWorkspaceBinding, *iop.WorkspaceOpenRequest) (*iop.WorkspaceOpenResponse, error)
	workspaceTool(context.Context, *SingleRequestWorkspaceBinding, *iop.WorkspaceToolRequest) (*iop.WorkspaceToolResponse, error)
}

// SingleRequestWorkspaceLifecycle is optional for executors that never open a
// workspace. Once a workspace is open, terminal commit fails closed unless the
// lifecycle can complete one typed cleanup for the immutable request.
type SingleRequestWorkspaceLifecycle interface {
	CleanupWorkspace(context.Context, *SingleRequestWorkspaceBinding, string) error
}

type singleRequestToolUsage struct {
	iterations  int
	outputBytes int
}

type singleRequestToolLoopState struct {
	continuation       SingleRequestToolContinuation
	runtime            singleRequestWorkspaceToolRuntime
	lifecycle          SingleRequestWorkspaceLifecycle
	opened             bool
	openAttempted      bool
	opening            bool
	openDone           chan struct{}
	openErr            error
	seenCallIDs        map[string]struct{}
	usage              map[string]singleRequestToolUsage
	pendingCallID      string
	pendingResultReady bool
	stageID            string
	stageDeadline      time.Time
	stageEpoch         uint64
	stageTimer         *time.Timer
}

type singleRequestPendingTool struct {
	request  *iop.WorkspaceToolRequest
	stageID  string
	deadline time.Time
}

func (h *singleRequestHandle) prepareInternalWorkspaceToolLocked(call *InternalWorkspaceToolCall) (*singleRequestPendingTool, error, singleRequestErrorClass) {
	if h.toolLoop.continuation == nil || h.toolLoop.runtime == nil {
		return nil, ErrSingleRequestInternalToolUnavailable, ""
	}
	if h.toolLoop.pendingCallID != "" {
		return nil, ErrSingleRequestInternalToolInvalidCall, ""
	}
	cloned := call.Clone()
	request, err := decodeInternalWorkspaceToolCall(cloned)
	if err != nil {
		return nil, err, ""
	}
	expectedStageID := canonicalSingleRequestStageID(h.state)
	if expectedStageID == "" || request.GetRequestId() != h.req.RequestID || request.GetStageId() != expectedStageID {
		return nil, ErrSingleRequestIdentityMismatch, ""
	}
	if h.binding == nil || h.binding.Workspace == nil || !h.internalWorkspaceToolCapabilityAllowed(request) {
		return nil, ErrSingleRequestInternalToolDenied, ""
	}
	if _, duplicate := h.toolLoop.seenCallIDs[request.GetToolCallId()]; duplicate {
		return nil, ErrSingleRequestInternalToolInvalidCall, ""
	}
	usage := h.toolLoop.usage[expectedStageID]
	if usage.iterations >= h.binding.Limits.MaxToolIterations || h.toolLoop.stageDeadline.IsZero() {
		return nil, ErrSingleRequestInternalToolBudget, ""
	}
	if !time.Now().Before(h.toolLoop.stageDeadline) {
		_, errorClass := h.classifyChildOperationContext(nil, singleRequestErrorClassTimeout)
		return nil, ErrSingleRequestInternalToolBudget, errorClass
	}

	usage.iterations++
	h.toolLoop.usage[expectedStageID] = usage
	h.toolLoop.seenCallIDs[request.GetToolCallId()] = struct{}{}
	h.toolLoop.pendingCallID = request.GetToolCallId()
	h.toolLoop.pendingResultReady = false
	return &singleRequestPendingTool{
		request:  request,
		stageID:  expectedStageID,
		deadline: h.toolLoop.stageDeadline,
	}, nil, ""
}

func (h *singleRequestHandle) internalWorkspaceToolCapabilityAllowed(request *iop.WorkspaceToolRequest) bool {
	workspace := h.binding.Workspace
	operationID := ""
	switch request.GetOperation() {
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_READ:
		operationID = "read"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST:
		operationID = "list"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE:
		operationID = "write"
		if write := request.GetWrite(); write == nil || len(write.GetContent()) > workspace.Limits.MaxWriteBytes {
			return false
		}
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE:
		operationID = "delete"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND:
		operationID = "command"
		if _, allowed := slices.BinarySearch(workspace.CommandIDs, request.GetCommandId()); !allowed {
			return false
		}
		environmentBytes := 0
		for name, value := range request.GetEnvironment() {
			if _, allowed := slices.BinarySearch(workspace.EnvironmentNames, name); !allowed || strings.ContainsRune(value, 0) {
				return false
			}
			environmentBytes += len(name) + len(value)
			if environmentBytes > h.binding.Limits.MaxOutputBytes {
				return false
			}
		}
	default:
		return false
	}
	_, allowed := slices.BinarySearch(workspace.OperationIDs, operationID)
	return allowed
}

func (h *singleRequestHandle) executeInternalWorkspaceTool(pending *singleRequestPendingTool) {
	outcome := singleRequestOutcomeSuccess
	errorClass := singleRequestErrorClass("")
	toolObserved := false
	observeTool := func(observedOutcome singleRequestOutcome, observedErrorClass singleRequestErrorClass) {
		if toolObserved {
			return
		}
		h.mu.Lock()
		h.timing.onToolExit(observedOutcome, observedErrorClass)
		h.mu.Unlock()
		toolObserved = true
	}
	defer func() {
		observeTool(outcome, errorClass)
	}()
	if pending == nil || pending.request == nil {
		outcome, errorClass = singleRequestOutcomeError, singleRequestErrorClassValidation
		h.failInternalWorkspaceTool(ErrSingleRequestInternalToolInvalidCall)
		return
	}
	ctx, cancel := context.WithDeadline(h.execCtx, pending.deadline)
	defer cancel()

	h.mu.Lock()
	runtime := h.toolLoop.runtime
	continuation := h.toolLoop.continuation
	binding := h.binding.Workspace.Clone()
	h.mu.Unlock()
	if runtime == nil || continuation == nil || binding == nil {
		outcome, errorClass = singleRequestOutcomeError, singleRequestErrorClassProvider
		h.failInternalWorkspaceTool(ErrSingleRequestInternalToolUnavailable)
		return
	}

	if err := h.ensureSingleRequestWorkspaceOpen(ctx, runtime, binding); err != nil {
		outcome, errorClass = h.failInternalWorkspaceToolOutcome(ctx, singleRequestErrorClassInternalToolFailed, ErrSingleRequestInternalToolBudget)
		return
	}
	h.mu.Lock()
	terminal := isTerminalState(h.state)
	h.mu.Unlock()
	if terminal || ctx.Err() != nil {
		outcome, errorClass = h.classifyChildOperationContext(ctx, singleRequestErrorClassInternalToolFailed)
		return
	}

	pending.request.TimeoutMs = 0
	if pending.request.GetOperation() == iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND {
		pending.request.TimeoutMs = internalToolCommandTimeoutMilliseconds(pending.deadline, binding.Limits.MaxCommandTimeoutMS)
	}
	response, err := runtime.workspaceTool(ctx, binding, pending.request)
	if err != nil {
		outcome, errorClass = h.failInternalWorkspaceToolOutcome(ctx, singleRequestErrorClassInternalToolFailed, ErrSingleRequestInternalToolBudget)
		return
	}
	if response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS &&
		response.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
		if response.GetStatus() == iop.WorkspaceStatus_WORKSPACE_STATUS_TIMEOUT || response.GetErrorCode() == iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_TIMEOUT {
			outcome, errorClass = h.failInternalWorkspaceToolOutcome(ctx, singleRequestErrorClassTimeout, ErrSingleRequestInternalToolFailed)
			return
		}
		outcome, errorClass = singleRequestOutcomeError, singleRequestErrorClassInternalToolFailed
		h.failInternalWorkspaceTool(ErrSingleRequestInternalToolFailed)
		return
	}
	result := internalWorkspaceToolResult(response)

	h.mu.Lock()
	if isTerminalState(h.state) {
		outcome, errorClass = h.classifyChildOperationContext(ctx, singleRequestErrorClassInternalToolFailed)
		h.mu.Unlock()
		return
	}
	if h.state != SingleRequestStateInternalTool || h.savedStage == "" || h.toolLoop.pendingCallID != result.ToolCallID ||
		canonicalSingleRequestStageID(h.savedStage) != pending.stageID || result.RequestID != h.req.RequestID || result.StageID != pending.stageID {
		outcome, errorClass = singleRequestOutcomeError, singleRequestErrorClassValidation
		h.failLocked(ErrSingleRequestIdentityMismatch)
		h.mu.Unlock()
		return
	}
	usage := h.toolLoop.usage[pending.stageID]
	outputBytes := internalWorkspaceToolOutputBytes(result)
	if outputBytes < 0 || outputBytes > h.binding.Limits.MaxOutputBytes-usage.outputBytes {
		outcome, errorClass = singleRequestOutcomeError, singleRequestErrorClassInternalToolBudget
		h.failLocked(ErrSingleRequestInternalToolBudget)
		h.mu.Unlock()
		return
	}
	usage.outputBytes += outputBytes
	h.toolLoop.usage[pending.stageID] = usage
	h.toolLoop.pendingResultReady = true
	h.mu.Unlock()

	// Complete the successful workspace-tool observation before external
	// continuation code can synchronously advance the resumed provider stages.
	// The deferred closer remains responsible for every earlier failure path.
	observeTool(singleRequestOutcomeSuccess, "")
	if err := continuation.ContinueInternalTool(ctx, result.Clone()); err != nil {
		outcome, errorClass = h.failInternalWorkspaceToolOutcome(ctx, singleRequestErrorClassInternalToolFailed, ErrSingleRequestInternalToolBudget)
	}
}

// CleanupWorkspace maps the private wire terminal to one safe coordinator
// outcome. Node error text and filesystem details never enter coordinator state.
func (s *Service) CleanupWorkspace(ctx context.Context, binding *SingleRequestWorkspaceBinding, requestID string) error {
	response, err := s.workspaceCleanup(ctx, binding, &iop.WorkspaceCleanupRequest{RequestId: requestID})
	if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		return ErrSingleRequestWorkspaceCleanup
	}
	return nil
}

func (h *singleRequestHandle) failInternalWorkspaceToolOutcome(ctx context.Context, fallback singleRequestErrorClass, timeoutErr error) (singleRequestOutcome, singleRequestErrorClass) {
	outcome, errorClass := h.classifyChildOperationContext(ctx, fallback)
	h.mu.Lock()
	defer h.mu.Unlock()
	if isTerminalState(h.state) {
		return outcome, errorClass
	}
	switch {
	case outcome == singleRequestOutcomeCancel:
		h.cancelLocked()
	case errorClass == singleRequestErrorClassInternalToolBudget:
		h.failLockedWithErrorClass(ErrSingleRequestInternalToolBudget, singleRequestErrorClassInternalToolBudget)
	case errorClass == singleRequestErrorClassTimeout:
		if timeoutErr == nil {
			timeoutErr = ErrSingleRequestInternalToolBudget
		}
		h.failLockedWithErrorClass(timeoutErr, singleRequestErrorClassTimeout)
	default:
		h.failLockedWithErrorClass(ErrSingleRequestInternalToolFailed, errorClass)
	}
	return outcome, errorClass
}

func (h *singleRequestHandle) failInternalWorkspaceTool(err error) {
	h.failInternalWorkspaceToolWithErrorClass(err, "")
}

func (h *singleRequestHandle) failInternalWorkspaceToolWithErrorClass(err error, errorClass singleRequestErrorClass) {
	h.mu.Lock()
	if !isTerminalState(h.state) {
		h.failLockedWithErrorClass(err, errorClass)
	}
	h.mu.Unlock()
}

func (h *singleRequestHandle) activeStageIDLocked() string {
	if h.state == SingleRequestStateInternalTool {
		return canonicalSingleRequestStageID(h.savedStage)
	}
	return canonicalSingleRequestStageID(h.state)
}

func canonicalSingleRequestStageID(state SingleRequestState) string {
	switch state {
	case SingleRequestStatePlanning:
		return "plan"
	case SingleRequestStateWorking:
		return "work"
	case SingleRequestStateReviewing, SingleRequestStateRepairing:
		return "review"
	default:
		return ""
	}
}

func (h *singleRequestHandle) updateStageBudgetLocked(previousStageID string) {
	nextStageID := h.activeStageIDLocked()
	if previousStageID == nextStageID {
		return
	}
	h.stopStageBudgetLocked()
	h.toolLoop.stageID = nextStageID
	if nextStageID == "" {
		h.toolLoop.stageDeadline = time.Time{}
		return
	}
	h.toolLoop.stageDeadline = time.Now().Add(time.Duration(h.binding.Limits.StageTimeoutMS) * time.Millisecond)
	h.toolLoop.stageEpoch++
	epoch := h.toolLoop.stageEpoch
	deadline := h.toolLoop.stageDeadline
	h.toolLoop.stageTimer = time.AfterFunc(time.Until(deadline), func() {
		h.mu.Lock()
		defer h.mu.Unlock()
		if isTerminalState(h.state) || h.toolLoop.stageEpoch != epoch || h.activeStageIDLocked() != nextStageID {
			return
		}
		h.failLockedWithErrorClass(ErrSingleRequestInternalToolBudget, singleRequestErrorClassTimeout)
	})
}

func (h *singleRequestHandle) stopStageBudgetLocked() {
	if h.toolLoop.stageTimer != nil {
		h.toolLoop.stageTimer.Stop()
		h.toolLoop.stageTimer = nil
	}
}

func internalToolRemainingMilliseconds(deadline time.Time) int64 {
	remaining := time.Until(deadline)
	if remaining <= time.Millisecond {
		return 1
	}
	return int64((remaining + time.Millisecond - 1) / time.Millisecond)
}

func internalToolCommandTimeoutMilliseconds(deadline time.Time, workspaceMaximum int) int64 {
	remaining := internalToolRemainingMilliseconds(deadline)
	if workspaceMaximum > 0 && int64(workspaceMaximum) < remaining {
		return int64(workspaceMaximum)
	}
	return remaining
}
