package workspace

import (
	"container/heap"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"sort"
	"time"

	iop "iop/proto/gen/iop"
)

const maxListEntries = 1024
const listBatchSize = 128

type listMaxHeap []string

func (h listMaxHeap) Len() int           { return len(h) }
func (h listMaxHeap) Less(i, j int) bool { return h[i] > h[j] }
func (h listMaxHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *listMaxHeap) Push(value any)    { *h = append(*h, value.(string)) }
func (h *listMaxHeap) Pop() any {
	old := *h
	last := old[len(old)-1]
	*h = old[:len(old)-1]
	return last
}

// Result is intentionally content-free on failure. The Node handler maps it to
// the typed wire response without returning filesystem paths or OS errors.
type Result struct {
	Status     iop.WorkspaceStatus
	Code       iop.WorkspaceErrorCode
	Content    []byte
	Entries    []string
	Stdout     []byte
	Stderr     []byte
	ExitCode   int32
	Truncated  bool
	DurationMS int64
}

func (r *Runtime) Read(requestID, relativePath string) (result Result) {
	startedAt := time.Now()
	var correlation string
	defer func() {
		result.DurationMS = time.Since(startedAt).Milliseconds()
		r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, result)
	}()
	return r.withRequest(requestID, func(req *Request) Result {
		correlation = req.correlation
		if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_READ); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			return result
		}
		name, err := userPath(relativePath)
		if err != nil {
			return failureFor(err)
		}
		info, err := checkedExisting(req.entry, name, false, false)
		if err != nil || !info.Mode().IsRegular() {
			if err == nil {
				err = errUnsafePath
			}
			return failureFor(err)
		}
		file, err := req.entry.root.Open(name)
		if err != nil {
			return failureFor(err)
		}
		defer file.Close()
		opened, err := file.Stat()
		if err != nil || !opened.Mode().IsRegular() {
			return failureFor(errUnsafePath)
		}
		if device, _, ok := fileIdentity(opened); !ok || device != req.entry.device {
			return failureFor(errUnsafePath)
		}
		data, err := io.ReadAll(io.LimitReader(file, req.maxRead+1))
		if err != nil {
			return failureFor(err)
		}
		if int64(len(data)) > req.maxRead {
			return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Content: data[:req.maxRead], Truncated: true}
		}
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Content: data}
	})
}

func (r *Runtime) List(requestID, relativePath string) (result Result) {
	startedAt := time.Now()
	var correlation string
	defer func() {
		result.DurationMS = time.Since(startedAt).Milliseconds()
		r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST, result)
	}()
	return r.withRequest(requestID, func(req *Request) Result {
		correlation = req.correlation
		if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			return result
		}
		name, err := userPath(relativePath)
		if err != nil {
			return failureFor(err)
		}
		if _, err := checkedExisting(req.entry, name, true, false); err != nil {
			return failureFor(err)
		}
		file, err := req.entry.root.Open(name)
		if err != nil {
			return failureFor(err)
		}
		defer file.Close()
		opened, err := file.Stat()
		if err != nil || !opened.IsDir() {
			return failureFor(errUnsafePath)
		}
		if device, _, ok := fileIdentity(opened); !ok || device != req.entry.device {
			return failureFor(errUnsafePath)
		}
		retained := make(listMaxHeap, 0, maxListEntries)
		heap.Init(&retained)
		truncated := false
		for {
			batch, readErr := file.ReadDir(listBatchSize)
			for _, item := range batch {
				candidate := item.Name()
				if name == "." && candidate == ".iop" {
					continue
				}
				if retained.Len() < maxListEntries {
					heap.Push(&retained, candidate)
					continue
				}
				truncated = true
				if candidate < retained[0] {
					retained[0] = candidate
					heap.Fix(&retained, 0)
				}
			}
			if errors.Is(readErr, io.EOF) {
				break
			}
			if readErr != nil {
				return failureFor(readErr)
			}
		}
		entries := []string(retained)
		sort.Strings(entries)
		result := Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Truncated: truncated}
		var bytes int64
		for _, item := range entries {
			child := item
			if name != "." {
				child = path.Join(name, child)
			}
			info, err := checkedExisting(req.entry, child, false, false)
			if err != nil {
				return failureFor(err)
			}
			if !info.IsDir() && !info.Mode().IsRegular() {
				return failureFor(errUnsafePath)
			}
			encoded := item + "\t" + entryType(info)
			if bytes+int64(len(encoded)) > req.maxOutput {
				result.Truncated = true
				break
			}
			bytes += int64(len(encoded))
			result.Entries = append(result.Entries, encoded)
		}
		return result
	})
}

func (r *Runtime) Write(requestID, relativePath string, content []byte) (result Result) {
	startedAt := time.Now()
	var correlation string
	defer func() {
		result.DurationMS = time.Since(startedAt).Milliseconds()
		r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE, result)
	}()
	return r.withRequest(requestID, func(req *Request) Result {
		correlation = req.correlation
		if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			return result
		}
		if int64(len(content)) > req.maxWrite {
			return failureFor(errInvalidPath)
		}
		name, err := userPath(relativePath)
		if err != nil || name == "." {
			if err == nil {
				err = errInvalidPath
			}
			return failureFor(err)
		}
		parent, base, err := openOrCreateParentNoFollow(req.entry, name)
		if err != nil {
			return failureFor(err)
		}
		defer parent.close()
		initialTarget, err := parent.targetIdentity(base)
		if err != nil || initialTarget.exists && (!initialTarget.mode.IsRegular() || initialTarget.device != req.entry.device) {
			return failureFor(errUnsafePath)
		}
		tmpBase, err := randomTempBase()
		if err != nil {
			return failureFor(err)
		}
		file, err := parent.createTemp(tmpBase)
		if err != nil {
			return failureFor(err)
		}
		ok := false
		defer func() {
			if !ok {
				_ = parent.remove(tmpBase)
			}
		}()
		if _, err := file.Write(content); err != nil {
			_ = file.Close()
			return failureFor(err)
		}
		if err := file.Sync(); err != nil {
			_ = file.Close()
			return failureFor(err)
		}
		if err := file.Close(); err != nil {
			return failureFor(err)
		}
		if req.entry.beforeRename != nil {
			if err := req.entry.beforeRename(); err != nil {
				return failureFor(err)
			}
		}
		if err := parent.revalidate(); err != nil {
			return failureFor(err)
		}
		currentTarget, err := parent.targetIdentity(base)
		if err != nil || currentTarget != initialTarget {
			return failureFor(errUnsafePath)
		}
		if err := parent.rename(tmpBase, base); err != nil {
			return failureFor(err)
		}
		ok = true
		// The atomic replacement is already committed. Directory sync is best
		// effort because reporting a post-effect failure would violate the
		// executor's failure-preserves-target contract.
		_ = parent.sync()
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS}
	})
}

func (r *Runtime) Delete(requestID, relativePath string) (result Result) {
	startedAt := time.Now()
	var correlation string
	defer func() {
		result.DurationMS = time.Since(startedAt).Milliseconds()
		r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE, result)
	}()
	return r.withRequest(requestID, func(req *Request) Result {
		correlation = req.correlation
		if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
			return result
		}
		name, err := userPath(relativePath)
		if err != nil || name == "." {
			if err == nil {
				err = errInvalidPath
			}
			return failureFor(err)
		}
		info, err := checkedExisting(req.entry, name, false, true)
		if err != nil {
			return failureFor(err)
		}
		if info.Mode()&os.ModeSymlink == 0 && !info.Mode().IsRegular() && !info.IsDir() {
			return failureFor(errUnsafePath)
		}
		if err := req.entry.root.Remove(name); err != nil {
			return failureFor(err)
		}
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS}
	})
}

func entryType(info os.FileInfo) string {
	switch {
	case info.IsDir():
		return "dir"
	case info.Mode().IsRegular():
		return "file"
	default:
		return "other"
	}
}

func randomTempBase() (string, error) {
	var token [12]byte
	if _, err := rand.Read(token[:]); err != nil {
		return "", err
	}
	return fmt.Sprintf(".iop-write-%x", token), nil
}

func failureFor(err error) Result {
	if errors.Is(err, errNotFound) || errors.Is(err, os.ErrNotExist) {
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND}
	}
	if errors.Is(err, ErrClosed) {
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY}
	}
	if errors.Is(err, ErrInvalidRequest) || errors.Is(err, ErrRequestConflict) || errors.Is(err, ErrUnknownWorkspace) || errors.Is(err, errInvalidPath) || errors.Is(err, errReservedPath) {
		return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST}
	}
	return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL}
}
