//go:build windows

package sboxrun

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"sync"
	"syscall"
	"time"
)

// Status is the engine's lifecycle state, parallel to what the GUI
// expects (idle/starting/active/failed).
type Status string

const (
	StatusIdle     Status = "idle"
	StatusStarting Status = "starting"
	StatusActive   Status = "active"
	StatusFailed   Status = "failed"
)

// Engine wraps a sing-box subprocess.
type Engine struct {
	cfg    Config
	assets *AssetPaths

	mu      sync.Mutex
	status  Status
	lastErr error
	cmd     *exec.Cmd
	cancel  context.CancelFunc

	// done is closed when the subprocess exits (whether by Stop or
	// crash). Lets Status() observers detect failure asynchronously.
	done chan struct{}
}

// New constructs an Engine. No I/O yet.
func New(cfg Config) (*Engine, error) {
	if cfg.ProxyHost == "" || cfg.ProxyPort == 0 {
		return nil, errors.New("ProxyHost and ProxyPort are required")
	}
	if len(cfg.TargetProcs) == 0 {
		cfg.TargetProcs = []string{
			"Discord.exe",
			"DiscordCanary.exe",
			"DiscordPTB.exe",
			"Update.exe",
		}
	}
	return &Engine{cfg: cfg, status: StatusIdle}, nil
}

// Status returns the current lifecycle state.
func (e *Engine) Status() Status {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.status
}

// LastError returns the last error pushed us to Failed (or nil).
func (e *Engine) LastError() error {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.lastErr
}

func (e *Engine) setStatus(s Status, err error) {
	e.mu.Lock()
	e.status = s
	if err != nil {
		e.lastErr = err
	} else if s == StatusActive || s == StatusIdle {
		e.lastErr = nil
	}
	e.mu.Unlock()
}

// Start brings the engine to Active. Generates the sing-box config,
// extracts assets, launches the subprocess. Returns when the process
// is running (or fails to start). The provided ctx is used only for
// the bring-up sequence; the running subprocess is governed by Stop.
func (e *Engine) Start(ctx context.Context) error {
	e.mu.Lock()
	if e.status != StatusIdle && e.status != StatusFailed {
		e.mu.Unlock()
		return fmt.Errorf("Start requires Idle or Failed; got %s", e.status)
	}
	e.status = StatusStarting
	e.mu.Unlock()

	if err := e.bringUp(); err != nil {
		e.setStatus(StatusFailed, err)
		return err
	}
	e.setStatus(StatusActive, nil)
	return nil
}

func (e *Engine) bringUp() error {
	// 1. Extract assets
	assets, err := InstallAssets()
	if err != nil {
		return fmt.Errorf("install assets: %w", err)
	}
	e.assets = assets

	// 2. Generate config (point sing-box log at the workdir log file
	// so admin-detached processes don't lose their output to nowhere).
	cfg := e.cfg
	cfg.LogPath = assets.LogPath
	configJSON, err := BuildSingBoxConfig(cfg)
	if err != nil {
		return fmt.Errorf("build config: %w", err)
	}
	if err := os.WriteFile(assets.ConfigPath, []byte(configJSON), 0644); err != nil {
		return fmt.Errorf("write config: %w", err)
	}

	// 3. Open log file (truncate; sing-box appends to its own stdout/
	// stderr handle so we direct both there).
	logFile, err := os.OpenFile(assets.LogPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("open log: %w", err)
	}

	// 4. Spawn sing-box subprocess.
	subCtx, cancel := context.WithCancel(context.Background())
	e.cancel = cancel
	cmd := exec.CommandContext(subCtx, assets.SingBoxExe,
		"run", "-c", assets.ConfigPath, "-D", assets.WorkDir)
	cmd.Stdout = logFile
	cmd.Stderr = logFile
	cmd.SysProcAttr = &syscall.SysProcAttr{
		// Don't show a console window for the child.
		HideWindow: true,
	}
	if err := cmd.Start(); err != nil {
		cancel()
		_ = logFile.Close()
		return fmt.Errorf("spawn sing-box: %w", err)
	}
	e.cmd = cmd
	e.done = make(chan struct{})

	// 5. Watch for unexpected exit.
	go func() {
		err := cmd.Wait()
		_ = logFile.Close()
		close(e.done)
		// If we didn't intend to stop (cancel hasn't fired), this is a
		// crash → mark Failed so the GUI surfaces it.
		select {
		case <-subCtx.Done():
			// expected — Stop() cancelled us
		default:
			e.setStatus(StatusFailed, fmt.Errorf("sing-box exited unexpectedly: %w", err))
		}
	}()

	// 6. Brief readiness probe — sing-box takes ~200-500ms to bind
	// the TUN. If the process dies in that window, surface the error.
	select {
	case <-e.done:
		return fmt.Errorf("sing-box exited during startup; see %s", assets.LogPath)
	case <-time.After(800 * time.Millisecond):
		// alive
	}
	return nil
}

// Stop terminates the sing-box subprocess gracefully and returns to
// Idle. Idempotent — second calls are no-op.
func (e *Engine) Stop() error {
	e.mu.Lock()
	if e.status == StatusIdle {
		e.mu.Unlock()
		return nil
	}
	cancel := e.cancel
	cmd := e.cmd
	done := e.done
	e.mu.Unlock()

	if cancel != nil {
		cancel()
	}
	if cmd != nil && cmd.Process != nil {
		// Give it 3s to exit cleanly, then force-kill.
		killTimer := time.AfterFunc(3*time.Second, func() {
			_ = cmd.Process.Kill()
		})
		if done != nil {
			<-done
		}
		killTimer.Stop()
	}
	e.setStatus(StatusIdle, nil)
	return nil
}

// LogPath returns the path of the sing-box stdout/stderr capture so
// the GUI's "Open log file" can pop it up.
func (e *Engine) LogPath() string {
	if e.assets == nil {
		return ""
	}
	return e.assets.LogPath
}

// ConfigPath returns the path of the generated sing-box config (for
// debugging — "View config" link in GUI).
func (e *Engine) ConfigPath() string {
	if e.assets == nil {
		return ""
	}
	return e.assets.ConfigPath
}
