package workspace

import (
	"context"
	"errors"
	"io/fs"
	"path"
	"sort"
	"strings"
	"time"

	iop "iop/proto/gen/iop"
)

var errCleanupUnsupported = errors.New("workspace cleanup is unsupported on this platform")

// ReadInternalArtifact reads one inventoried Node-owned request artifact. The
// caller supplies only a path relative to the immutable request namespace; the
// public workspace tool surface cannot invoke this helper or name .iop.
func (r *Runtime) ReadInternalArtifact(requestID, relativePath string) ([]byte, error) {
	req, err := r.Request(requestID)
	if err != nil {
		return nil, err
	}
	name, err := internalArtifactPath(relativePath)
	if err != nil {
		return nil, ErrInvalidRequest
	}
	req.mu.Lock()
	defer req.mu.Unlock()
	if req.cleaning {
		return nil, ErrClosed
	}
	content, err := readOwnedArtifact(req.entry, req.internalPrefix, name, req.artifacts, maxInternalArtifactSize)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, fs.ErrNotExist
	}
	return content, err
}

// WriteInternalArtifact creates a new Node-owned request artifact. The caller
// supplies only a path relative to its immutable request namespace; the public
// workspace tool surface cannot invoke this helper or name .iop directly.
func (r *Runtime) WriteInternalArtifact(requestID, relativePath string, content []byte) error {
	if len(content) > maxInternalArtifactSize {
		return ErrInvalidRequest
	}
	req, err := r.Request(requestID)
	if err != nil {
		return err
	}
	name, err := internalArtifactPath(relativePath)
	if err != nil {
		return ErrInvalidRequest
	}
	req.mu.Lock()
	defer req.mu.Unlock()
	if req.cleaning {
		return ErrClosed
	}
	created, err := createOwnedArtifact(req.entry, req.internalPrefix, name, content, req.artifacts)
	if err != nil {
		return err
	}
	if len(req.artifacts)+len(created) > maxCleanupArtifacts {
		rollbackOwnedArtifacts(req.entry, created)
		return ErrInvalidRequest
	}
	for _, artifact := range created {
		req.artifacts[artifact.relative] = artifact
	}
	return nil
}

func internalArtifactPath(value string) (string, error) {
	if value == "" || len(value) > 1024 || value == "." || path.IsAbs(value) || path.Clean(value) != value || strings.Contains(value, "\\") || strings.ContainsRune(value, 0) || strings.HasPrefix(value, "../") || value == ".." {
		return "", errInvalidPath
	}
	return value, nil
}

// Cleanup elects one result owner for a request, cancels all of its command
// groups, validates the exact request tree against the in-memory ownership
// inventory, and removes only matching entries with non-recursive operations.
func (r *Runtime) Cleanup(ctx context.Context, requestID string) CleanupResult {
	if !validRequestID(requestID) {
		return cleanupFailure(iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST)
	}
	if ctx == nil {
		ctx = context.Background()
	}

	r.cleanupMu.Lock()
	if existing := r.cleanupCalls[requestID]; existing != nil {
		r.cleanupMu.Unlock()
		<-existing.done
		return existing.result
	}
	r.mu.RLock()
	req := r.requests[requestID]
	r.mu.RUnlock()
	if req == nil {
		r.cleanupMu.Unlock()
		return cleanupFailure(iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND)
	}
	call := &cleanupCall{done: make(chan struct{})}
	r.cleanupCalls[requestID] = call
	r.cleanupMu.Unlock()

	startedAt := time.Now()
	call.result = r.performCleanup(ctx, req)
	r.observeCleanup(req, call.result, time.Since(startedAt).Milliseconds())
	close(call.done)
	r.cleanupMu.Lock()
	r.cleanupOrder = append(r.cleanupOrder, requestID)
	for len(r.cleanupOrder) > completedCleanupLimit {
		evicted := r.cleanupOrder[0]
		r.cleanupOrder = r.cleanupOrder[1:]
		delete(r.cleanupCalls, evicted)
	}
	r.cleanupMu.Unlock()
	return call.result
}

func (r *Runtime) performCleanup(ctx context.Context, req *Request) CleanupResult {
	req.mu.Lock()
	req.cleaning = true
	artifacts := make(map[string]ownedArtifact, len(req.artifacts))
	for relative, artifact := range req.artifacts {
		artifacts[relative] = artifact
	}
	ownedParents := append([]ownedArtifact(nil), req.ownedParents...)
	req.mu.Unlock()

	executions := r.cancelRequestCommands(req.id)
	cleanupCtx, cancel := boundedCleanupContext(ctx)
	defer cancel()
	if !waitForCommandCleanup(cleanupCtx, executions) {
		r.closeRequestAuthority(req)
		return CleanupResult{
			Status:           iop.WorkspaceStatus_WORKSPACE_STATUS_TIMEOUT,
			Code:             iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_TIMEOUT,
			CleanedProcesses: int32(len(executions)),
		}
	}
	cleanedProcesses := int32(len(executions))
	cleanedArtifacts, err := validateAndRemoveOwnedArtifacts(req.entry, req.internalPrefix, artifacts, ownedParents)
	r.closeRequestAuthority(req)
	if err != nil {
		if errors.Is(err, errCleanupUnsupported) {
			return CleanupResult{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSUPPORTED, CleanedProcesses: cleanedProcesses}
		}
		return CleanupResult{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL, CleanedProcesses: cleanedProcesses}
	}
	return CleanupResult{
		Status:           iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS,
		Code:             iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSPECIFIED,
		CleanedProcesses: cleanedProcesses,
		CleanedArtifacts: int32(cleanedArtifacts),
	}
}

func (r *Runtime) cancelRequestCommands(requestID string) []*commandExecution {
	r.commandsMu.Lock()
	defer r.commandsMu.Unlock()
	executions := make([]*commandExecution, 0)
	for key, execution := range r.activeCommands {
		if key.requestID != requestID || execution == nil {
			continue
		}
		if execution.requestCancel() {
			executions = append(executions, execution)
		}
	}
	return executions
}

func boundedCleanupContext(parent context.Context) (context.Context, context.CancelFunc) {
	if deadline, ok := parent.Deadline(); ok && time.Until(deadline) <= defaultCleanupTimeout {
		return context.WithCancel(parent)
	}
	return context.WithTimeout(parent, defaultCleanupTimeout)
}

func waitForCommandCleanup(ctx context.Context, executions []*commandExecution) bool {
	for _, execution := range executions {
		select {
		case <-execution.done:
		case <-ctx.Done():
			return false
		}
	}
	return true
}

func (r *Runtime) closeRequestAuthority(req *Request) {
	r.mu.Lock()
	if r.requests[req.id] == req {
		delete(r.requests, req.id)
	}
	r.mu.Unlock()
	r.commandsMu.Lock()
	for key := range r.cancelledCommands {
		if key.requestID == req.id {
			delete(r.cancelledCommands, key)
		}
	}
	r.commandsMu.Unlock()
}

func cleanupFailure(status iop.WorkspaceStatus, code iop.WorkspaceErrorCode) CleanupResult {
	return CleanupResult{Status: status, Code: code}
}

func sortedArtifactsDeepestFirst(artifacts map[string]ownedArtifact) []ownedArtifact {
	ordered := make([]ownedArtifact, 0, len(artifacts))
	for _, artifact := range artifacts {
		ordered = append(ordered, artifact)
	}
	sort.Slice(ordered, func(i, j int) bool {
		leftDepth := strings.Count(ordered[i].relative, "/")
		rightDepth := strings.Count(ordered[j].relative, "/")
		if leftDepth != rightDepth {
			return leftDepth > rightDepth
		}
		return ordered[i].relative > ordered[j].relative
	})
	return ordered
}
