package repl

import (
	"bytes"
	"fmt"
	"sync"
	"time"

	"github.com/sandbox0-ai/sandbox0/manager/procd/pkg/process"
)

// REPL implements a configurable REPL process.
// Design note: REPL output is intentionally passed through unchanged.
// Parsing interactive CLI output is brittle and lossy; the only reliable
// contract is readiness via a prompt token, so we never trim or reinterpret output.
// This also keeps REPL compatible with arbitrary interactive CLIs.
type REPL struct {
	*process.BaseProcess
	runner *process.PTYRunner
	config *REPLConfig

	// Ready detection state. Note: prompt token detection is per-chunk and can fire multiple times.
	mu          sync.Mutex
	readyToken  []byte
	readyBuffer []byte
}

var _ process.Process = (*REPL)(nil)

// New creates a new REPL process with the given configuration.
func New(id string, replConfig *REPLConfig, processConfig process.ProcessConfig) (*REPL, error) {
	if replConfig == nil {
		return nil, fmt.Errorf("REPL config is required")
	}
	config := replConfig.Clone()
	config.applyDefaults()
	if err := config.Validate(); err != nil {
		return nil, fmt.Errorf("invalid REPL config: %w", err)
	}

	bp := process.NewBaseProcess(id, process.ProcessTypeREPL, processConfig)

	repl := &REPL{
		BaseProcess: bp,
		config:      config,
	}

	if config.Ready.Mode == ReadyModePromptToken && config.Ready.Token != "" {
		repl.readyToken = []byte(config.Ready.Token)
		repl.readyBuffer = make([]byte, 0, len(repl.readyToken))
	}

	// Set up output filter if needed for readiness detection
	var outputFilter func([]byte) ([]byte, bool)
	if config.Ready.Mode == ReadyModePromptToken && len(repl.readyToken) > 0 {
		outputFilter = repl.filterOutput
	}

	repl.runner = process.NewPTYRunner(bp, outputFilter, nil)
	return repl, nil
}

// NewFromRegistry creates a REPL using a config from the registry.
func NewFromRegistry(id string, alias string, processConfig process.ProcessConfig) (*REPL, error) {
	replConfig, ok := DefaultRegistry.Get(alias)
	if !ok {
		return nil, fmt.Errorf("unknown REPL alias: %s", alias)
	}
	return New(id, replConfig, processConfig)
}

// Start starts the REPL process.
func (r *REPL) Start() error {
	if r.IsRunning() {
		return process.ErrProcessAlreadyRunning
	}

	processConfig := r.GetConfig()

	// Build exec candidates from REPL config
	candidates := make([]execCandidate, len(r.config.Candidates))
	for i, c := range r.config.Candidates {
		candidates[i] = execCandidate{
			name: c.Name,
			args: c.Args,
		}
	}

	// Build environment variables
	extraEnv := r.buildEnvVars(processConfig)
	if err := startWithCandidates(r.BaseProcess, r.runner, processConfig, candidates, extraEnv); err != nil {
		return err
	}
	r.scheduleStartupReady()
	return nil
}

// buildEnvVars builds the environment variables for the REPL.
func (r *REPL) buildEnvVars(processConfig process.ProcessConfig) map[string]string {
	envVars := map[string]string{}

	// Get TERM value
	term := processConfig.Term
	if term == "" && r.config.DefaultTerm != "" {
		term = r.config.DefaultTerm
	}

	// Get prompt value
	prompt := r.config.Prompt.CustomPrompt

	for _, env := range r.config.Env {
		var value string
		switch env.ValueFrom {
		case "term":
			value = term
		case "prompt":
			value = prompt
		default:
			value = env.Value
		}
		if value != "" {
			envVars[env.Name] = value
		}
	}

	if len(envVars) == 0 {
		return nil
	}
	return envVars
}

// Stop stops the REPL process.
func (r *REPL) Stop() error {
	return r.runner.Stop()
}
func (r *REPL) WriteInput(data []byte) error {
	return r.BaseProcess.WriteInput(data)
}

// Restart restarts the process.
func (r *REPL) Restart() error {
	if err := r.Stop(); err != nil {
		return err
	}
	time.Sleep(100 * time.Millisecond)
	return r.Start()
}

// ResizeTerminal resizes the PTY.
func (r *REPL) ResizeTerminal(size process.PTYSize) error {
	if !r.IsRunning() {
		return process.ErrProcessNotRunning
	}
	return r.ResizePTY(size)
}

// Config returns the REPL configuration.
func (r *REPL) Config() *REPLConfig {
	return r.config.Clone()
}

// filterOutput detects readiness without altering output.
// This is deliberately minimal: output must remain raw for AI agents.
func (r *REPL) filterOutput(data []byte) ([]byte, bool) {
	if len(data) == 0 {
		return data, false
	}
	promptDetected := r.detectReadyToken(data)
	return data, promptDetected
}

// Alias returns the REPL alias/name.
func (r *REPL) Alias() string {
	return r.config.Name
}

func (r *REPL) detectReadyToken(data []byte) bool {
	if len(r.readyToken) == 0 {
		return false
	}
	r.mu.Lock()
	defer r.mu.Unlock()

	if len(r.readyBuffer) == 0 {
		if bytes.Contains(data, r.readyToken) {
			return true
		}
		r.readyBuffer = append(r.readyBuffer[:0], tailForToken(data, len(r.readyToken))...)
		return false
	}

	combined := make([]byte, 0, len(r.readyBuffer)+len(data))
	combined = append(combined, r.readyBuffer...)
	combined = append(combined, data...)
	if bytes.Contains(combined, r.readyToken) {
		r.readyBuffer = r.readyBuffer[:0]
		return true
	}
	r.readyBuffer = append(r.readyBuffer[:0], tailForToken(combined, len(r.readyToken))...)
	return false
}

func tailForToken(data []byte, tokenLen int) []byte {
	if tokenLen <= 1 || len(data) < tokenLen-1 {
		return data
	}
	return data[len(data)-(tokenLen-1):]
}

func (r *REPL) scheduleStartupReady() {
	if r.config.Ready.Mode != ReadyModeStartupDelay {
		return
	}
	delay := time.Duration(r.config.Ready.StartupDelayMs) * time.Millisecond
	if delay <= 0 {
		delay = time.Duration(DefaultReadyStartupDelayMs) * time.Millisecond
	}
	go func() {
		time.Sleep(delay)
		if !r.IsRunning() {
			return
		}
		r.SignalInputReady()
		r.PublishOutput(process.ProcessOutput{
			Source: process.OutputSourcePrompt,
		})
	}()
}
