// supervisor.go 管理 run profile 子进程的内存运行态。每个 profile 同一时刻至多
// 一个运行进程。并发骨架照搬 acp/supervisor.go 已被测试验证的不变量：
//   - procs map 的写锁仅在 Spawn 插入 / monitor 删除时持有
//   - done channel 严格 close-once（monitorProcess 唯一持有）
//   - KilledByUs atomic 区分 stopped（我方终止）vs crashed
//   - onExit 不得重新 Lock s.mu（避免 monitor→Stop→onExit 链式死锁）
//
// 与 acp 的差异：无 JSON-RPC handshake / Relay 双向中继 / MCP token / worktree
// acquire；并补上经 internal/infra/proc 的整树终止（dev server 会派生子孙进程）。
package run

import (
	"bufio"
	"context"
	"io"
	"log/slog"
	"os/exec"
	"sync"
	"sync/atomic"
	"time"

	"github.com/google/uuid"

	procgrp "github.com/yan1h/agent-coding-workflow/internal/infra/proc"
)

// SupervisorConfig 是从 config.RunConfig 派生的 supervisor 子集。
type SupervisorConfig struct {
	KillGrace      time.Duration
	OutBufferLines int
	OutTailBytes   int
	WSSendBuffer   int
	ShutdownGrace  time.Duration
}

// RunProc 是单个运行进程的运行时句柄。
type RunProc struct {
	ProfileID uuid.UUID
	Cmd       *exec.Cmd
	group     procgrp.Group
	relay     *runRelay

	StartedAt  time.Time
	KilledByUs atomic.Bool   // true → stopped；false → crashed
	done       chan struct{} // monitor 退出时 close
}

// RunSupervisor 是 run profile 子进程总管。被动结构：Spawn 时起 goroutine，无主循环。
type RunSupervisor struct {
	mu       sync.RWMutex
	procs    map[uuid.UUID]*RunProc
	terminal map[uuid.UUID]RunStatus // 最近一次退出的终止态（crashed/stopped），重启即清空

	repo Repository
	cfg  SupervisorConfig
	log  *slog.Logger
}

// NewSupervisor 构造空 RunSupervisor。
func NewSupervisor(repo Repository, cfg SupervisorConfig, log *slog.Logger) *RunSupervisor {
	if log == nil {
		log = slog.Default()
	}
	return &RunSupervisor{
		procs:    map[uuid.UUID]*RunProc{},
		terminal: map[uuid.UUID]RunStatus{},
		repo:     repo,
		cfg:      cfg,
		log:      log,
	}
}

// SpawnParams 由 Service 构造：命令、参数、最终环境（宿主白名单+覆盖）、工作目录。
type SpawnParams struct {
	ProfileID uuid.UUID
	Command   string
	Args      []string
	Env       []string
	Dir       string
}

// ErrAlreadyRunning / ErrNotRunning 是 supervisor 层的哨兵，Service 翻译为 errs code。
var (
	ErrAlreadyRunning = errSentinel("run: profile already running")
	ErrNotRunning     = errSentinel("run: profile not running")
)

type errSentinel string

func (e errSentinel) Error() string { return string(e) }

// Spawn 启动一个子进程并托管。调用方（Service）负责前置鉴权与 env 构造。
// 同一 profile 已在运行时返回 ErrAlreadyRunning。
func (s *RunSupervisor) Spawn(p SpawnParams) (*RunProc, error) {
	s.mu.RLock()
	_, running := s.procs[p.ProfileID]
	s.mu.RUnlock()
	if running {
		return nil, ErrAlreadyRunning
	}

	cmd := exec.Command(p.Command, p.Args...)
	cmd.Dir = p.Dir
	cmd.Env = p.Env

	// 进程树管理：Prepare 须在 Start 前（Unix Setpgid），Adopt 须在 Start 后
	// （Windows Job Object）。终止时整树清理，避免 dev server 子孙进程残留。
	group := procgrp.NewGroup()
	group.Prepare(cmd)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}
	if err := cmd.Start(); err != nil {
		return nil, err
	}
	if err := group.Adopt(cmd); err != nil {
		_ = cmd.Process.Kill()
		return nil, err
	}

	proc := &RunProc{
		ProfileID: p.ProfileID,
		Cmd:       cmd,
		group:     group,
		relay:     newRunRelay(p.ProfileID, s.cfg.OutBufferLines, s.cfg.WSSendBuffer, s.log),
		StartedAt: time.Now(),
		done:      make(chan struct{}),
	}

	s.mu.Lock()
	s.procs[p.ProfileID] = proc
	delete(s.terminal, p.ProfileID) // 进入运行态，清除上一次终止态
	s.mu.Unlock()

	go s.drainPipe(proc, stdout, "info")
	go s.drainPipe(proc, stderr, "error")
	go s.monitorProcess(proc)

	return proc, nil
}

// Stop 终止指定 profile 的子进程（整树）。幂等阻塞直到 done 关闭或 ctx 取消。
// profile 未运行时返回 ErrNotRunning。
func (s *RunSupervisor) Stop(ctx context.Context, profileID uuid.UUID, grace time.Duration) error {
	s.mu.RLock()
	proc, ok := s.procs[profileID]
	s.mu.RUnlock()
	if !ok {
		return ErrNotRunning
	}
	proc.KilledByUs.Store(true)
	proc.group.TerminateGroup(proc.done, grace)
	select {
	case <-proc.done:
	case <-ctx.Done():
	}
	return nil
}

// Get 返回指定 profile 当前运行进程（用于 status/logs/subscribe）；未运行返回 false。
func (s *RunSupervisor) Get(profileID uuid.UUID) (*RunProc, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	proc, ok := s.procs[profileID]
	return proc, ok
}

// StatusOf 返回内存运行态：在 procs 中 → running；否则取最近终止态（crashed/stopped），
// 无记录（含服务端重启后）→ stopped。
func (s *RunSupervisor) StatusOf(profileID uuid.UUID) RunStatus {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if _, ok := s.procs[profileID]; ok {
		return StatusRunning
	}
	if st, ok := s.terminal[profileID]; ok {
		return st
	}
	return StatusStopped
}

// Subscribe 为正在运行的 profile 注册一个日志订阅；未运行返回 (nil,false)。
func (s *RunSupervisor) Subscribe(profileID, userID uuid.UUID) (*Subscriber, *runRelay, bool) {
	s.mu.RLock()
	proc, ok := s.procs[profileID]
	s.mu.RUnlock()
	if !ok {
		return nil, nil, false
	}
	return proc.relay.Subscribe(userID), proc.relay, true
}

// LogsSince 返回正在运行进程的内存日志（ID > since）；未运行返回 nil。
func (s *RunSupervisor) LogsSince(profileID uuid.UUID, since int64, limit int) []*LogEnvelope {
	s.mu.RLock()
	proc, ok := s.procs[profileID]
	s.mu.RUnlock()
	if !ok {
		return nil
	}
	return proc.relay.since(since, limit)
}

// drainPipe 逐行读取管道并 emit 到 relay。EOF/读错误时安静退出（Wait 由 monitor 处理）。
func (s *RunSupervisor) drainPipe(proc *RunProc, pipe io.Reader, level string) {
	scanner := bufio.NewScanner(pipe)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	for scanner.Scan() {
		proc.relay.emit(level, scanner.Text())
	}
}

// monitorProcess 等待进程退出，更新 procs/terminal map，释放进程树句柄并触发 onExit。
// 这是 done channel 唯一的合法关闭路径。
func (s *RunSupervisor) monitorProcess(proc *RunProc) {
	defer close(proc.done)
	waitErr := proc.Cmd.Wait()

	exitCode := int32(0)
	if exitErr, ok := waitErr.(*exec.ExitError); ok {
		exitCode = int32(exitErr.ExitCode())
	}

	status := StatusStopped
	if !proc.KilledByUs.Load() {
		status = StatusCrashed
	}

	s.mu.Lock()
	delete(s.procs, proc.ProfileID)
	s.terminal[proc.ProfileID] = status
	s.mu.Unlock()

	// 释放进程树句柄（Windows 关闭 Job handle；Unix no-op）。
	proc.group.Close()

	s.onExit(context.Background(), proc, status, exitCode)
}

// onExit 把退出信息落库并广播终止帧。不得再 Lock s.mu（monitorProcess 已 delete
// map 后调用本函数；Stop 也持读锁等 done，重入会死锁）。
func (s *RunSupervisor) onExit(ctx context.Context, proc *RunProc, status RunStatus, exitCode int32) {
	lastErr := ""
	if status == StatusCrashed {
		lastErr = proc.relay.tailText(s.cfg.OutTailBytes)
	}
	ec := exitCode
	if s.repo != nil {
		if err := s.repo.UpdateRunProfileExit(ctx, proc.ProfileID, &ec, lastErr); err != nil {
			s.log.Error("run.on_exit.update_exit", "profile_id", proc.ProfileID, "err", err.Error())
		}
	}
	proc.relay.Close(string(status), &ec)
}

// ShutdownAll 并行终止所有运行进程，受 cfg.ShutdownGrace 总时限约束。
func (s *RunSupervisor) ShutdownAll(ctx context.Context) {
	s.mu.RLock()
	ids := make([]uuid.UUID, 0, len(s.procs))
	for id := range s.procs {
		ids = append(ids, id)
	}
	s.mu.RUnlock()

	shutdownCtx, cancel := context.WithTimeout(ctx, s.cfg.ShutdownGrace)
	defer cancel()

	var wg sync.WaitGroup
	for _, id := range ids {
		wg.Add(1)
		go func(pid uuid.UUID) {
			defer wg.Done()
			_ = s.Stop(shutdownCtx, pid, s.cfg.KillGrace)
		}(id)
	}

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()
	select {
	case <-done:
	case <-shutdownCtx.Done():
		s.log.Warn("run.shutdown_all.timeout")
	}
}
