// Package sandbox — sandboxed Starlark interpreter.
//
// v0.29.0 CS1: Provides execution with timeout, step limits,
// captured output, and injectable predeclared modules.
//
// The sandbox is intentionally restrictive:
//   - No file I/O
//   - No network access
//   - No os/env access
//   - No load() statements (modules injected via predeclared)
//   - Step limit prevents infinite loops
//   - Context-based timeout prevents runaway wall-clock
//
// Extensions interact with the platform through injected Go builtins
// (e.g., secrets.get, notifications.send) — never through Starlark's
// own I/O. This is the eval loop; modules are added in CS3.
package sandbox

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"go.starlark.net/starlark"
	"go.starlark.net/starlarkstruct"
	starlarkjson "go.starlark.net/lib/json"
)

// ─── Configuration ───────────────────────────

// Config controls sandbox resource limits.
type Config struct {
	// MaxSteps limits total bytecode operations. 0 = unlimited.
	// Default: 1_000_000 (~1M ops, covers most scripts in <100ms).
	MaxSteps uint64

	// Name identifies this sandbox in logs and error messages.
	Name string
}

// DefaultConfig returns production defaults.
func DefaultConfig() Config {
	return Config{
		MaxSteps: 1_000_000,
		Name:     "extension",
	}
}

// ─── Result ──────────────────────────────────

// Result holds the output of a sandbox execution.
type Result struct {
	// Globals contains the top-level names defined by the script.
	Globals map[string]starlark.Value

	// Output captures all print() calls made during execution.
	Output string

	// Steps is the number of bytecode operations executed.
	Steps uint64
}

// ─── Sandbox ─────────────────────────────────

// Sandbox executes Starlark scripts with resource limits and
// injected modules. Safe for concurrent use — each Exec call
// gets its own thread.
type Sandbox struct {
	config Config
}

// New creates a sandbox with the given configuration.
// Set MaxSteps to 0 for unlimited (not recommended in production).
func New(cfg Config) *Sandbox {
	if cfg.Name == "" {
		cfg.Name = "extension"
	}
	return &Sandbox{config: cfg}
}

// LoadFunc resolves load("path") calls to Starlark source.
// Returns the module's globals. The sandbox caches results per
// thread (Starlark handles this via thread.Load dedup).
type LoadFunc func(thread *starlark.Thread, module string) (starlark.StringDict, error)

// Exec executes a Starlark script within the sandbox.
// load() is disabled — all dependencies must be provided via modules.
// For load() support, use ExecWithLoader.
func (s *Sandbox) Exec(ctx context.Context, filename, source string, modules map[string]starlark.Value) (*Result, error) {
	return s.ExecWithLoader(ctx, filename, source, modules, nil)
}

// ExecWithLoader executes a Starlark script within the sandbox with
// an optional load callback. If loader is nil, load() returns an error.
//
// Parameters:
//   - ctx: context for timeout/cancellation
//   - filename: used in error messages and stack traces
//   - source: Starlark source code
//   - modules: predeclared modules injected into the script's namespace
//   - loader: resolves load() calls; nil = load() disabled
func (s *Sandbox) ExecWithLoader(ctx context.Context, filename, source string, modules map[string]starlark.Value, loader LoadFunc) (*Result, error) {
	var output strings.Builder
	var outputMu sync.Mutex

	predeclared := make(starlark.StringDict, len(modules)+1)
	for k, v := range modules {
		predeclared[k] = v
	}
	// json.encode / json.decode — universal, no permissions required (pure computation)
	predeclared["json"] = starlarkjson.Module

	// Default: load() disabled. If loader provided, delegate to it.
	loadFn := func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
		return nil, fmt.Errorf("load() is disabled in sandbox (requested %q)", module)
	}
	if loader != nil {
		loadFn = loader
	}

	thread := &starlark.Thread{
		Name: s.config.Name,
		Print: func(_ *starlark.Thread, msg string) {
			outputMu.Lock()
			defer outputMu.Unlock()
			output.WriteString(msg)
			output.WriteByte('\n')
		},
		Load: loadFn,
	}

	if s.config.MaxSteps > 0 {
		thread.SetMaxExecutionSteps(s.config.MaxSteps)
	}

	// Wire context cancellation into the thread.
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-ctx.Done():
			thread.Cancel(ctx.Err().Error())
		case <-done:
		}
	}()

	globals, err := starlark.ExecFile(thread, filename, source, predeclared)
	if err != nil {
		return nil, wrapError(err)
	}

	result := &Result{
		Globals: make(map[string]starlark.Value, len(globals)),
		Output:  output.String(),
		Steps:   thread.ExecutionSteps(),
	}
	for k, v := range globals {
		result.Globals[k] = v
	}
	return result, nil
}

// Call invokes a Starlark function within the sandbox.
// Used by the task executor and filter runner to call specific
// entry points (e.g., "on_pre_completion", "on_run").
func (s *Sandbox) Call(ctx context.Context, fn starlark.Callable, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, string, error) {
	var output strings.Builder
	var outputMu sync.Mutex

	thread := &starlark.Thread{
		Name: s.config.Name + "/call",
		Print: func(_ *starlark.Thread, msg string) {
			outputMu.Lock()
			defer outputMu.Unlock()
			output.WriteString(msg)
			output.WriteByte('\n')
		},
		Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
			return nil, fmt.Errorf("load() is disabled in sandbox (requested %q)", module)
		},
	}

	if s.config.MaxSteps > 0 {
		thread.SetMaxExecutionSteps(s.config.MaxSteps)
	}

	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-ctx.Done():
			thread.Cancel(ctx.Err().Error())
		case <-done:
		}
	}()

	val, err := starlark.Call(thread, fn, args, kwargs)
	if err != nil {
		return nil, output.String(), wrapError(err)
	}
	return val, output.String(), nil
}

// ─── Module Helper ───────────────────────────

// MakeModule creates a starlarkstruct.Module with the given name and
// members. Standard way to expose Go functions to Starlark.
//
// Usage:
//
//	mod := sandbox.MakeModule("secrets", starlark.StringDict{
//	    "get": starlark.NewBuiltin("secrets.get", secretsGet),
//	})
func MakeModule(name string, members starlark.StringDict) *starlarkstruct.Module {
	return &starlarkstruct.Module{
		Name:    name,
		Members: members,
	}
}

// ─── Error Wrapping ──────────────────────────

// SandboxError wraps a Starlark execution error with context.
type SandboxError struct {
	Err       error
	Backtrace string
}

func (e *SandboxError) Error() string {
	if e.Backtrace != "" {
		return fmt.Sprintf("%s\n%s", e.Err.Error(), e.Backtrace)
	}
	return e.Err.Error()
}

func (e *SandboxError) Unwrap() error { return e.Err }

func wrapError(err error) error {
	if err == nil {
		return nil
	}
	se := &SandboxError{Err: err}
	if evalErr, ok := err.(*starlark.EvalError); ok {
		se.Backtrace = evalErr.Backtrace()
	}
	return se
}
