package main

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	runnerVersion  = "0.1.0-alpha"
	receiptSchema  = "org.arkhe.pars-lab.receipt.v1"
	benchmarkID    = "PARS-PILOT-2026-08-16-V2"
	upstreamCommit = "57ac9970fa324c7c2a6f613fe32f5cc0577d3c3e"
)

type condition struct {
	Label  string `json:"label"`
	Prompt string `json:"prompt"`
}

type conditionSet struct {
	BenchmarkID    string               `json:"benchmark_id"`
	UpstreamCommit string               `json:"upstream_commit"`
	Conditions     map[string]condition `json:"conditions"`
}

type answerValue struct {
	ClaimStatus string `json:"claim_status"`
	Answer      any    `json:"answer"`
}

type taskScore struct {
	ClaimStatusOK bool `json:"claim_status_ok"`
	AnswerOK      bool `json:"answer_ok"`
	Point         int  `json:"point"`
}

type score struct {
	AnswerFile         bool                 `json:"answer_file"`
	AnswerSHA256       *string              `json:"answer_sha256"`
	Parseable          bool                 `json:"parseable"`
	TaskPoints         int                  `json:"task_points"`
	ClaimStatusCorrect int                  `json:"claim_status_correct"`
	AnswerCorrect      int                  `json:"answer_correct"`
	Materialization    int                  `json:"materialization_point"`
	T5SHA256           *string              `json:"t5_sha256"`
	SevereFalseSupport []string             `json:"severe_false_supports"`
	TaskDetail         map[string]taskScore `json:"task_detail"`
	TotalPoints        int                  `json:"total_points"`
}

type armReceipt struct {
	Arm                string  `json:"arm"`
	Label              string  `json:"label"`
	Status             string  `json:"status"`
	ExitCode           int     `json:"exit_code"`
	DurationMS         int64   `json:"duration_ms"`
	ReportedTokens     *int64  `json:"reported_tokens"`
	ObservedModel      *string `json:"observed_model"`
	CombinedLogSHA256  string  `json:"combined_log_sha256"`
	FinalMessageSHA256 *string `json:"final_message_sha256"`
	Error              *string `json:"error"`
	Score              score   `json:"score"`
}

type receipt struct {
	Schema        string        `json:"schema"`
	Status        string        `json:"status"`
	RunnerVersion string        `json:"runner_version"`
	RunID         string        `json:"run_id"`
	StartedAt     string        `json:"started_at"`
	CompletedAt   *string       `json:"completed_at"`
	System        systemInfo    `json:"system"`
	Codex         codexInfo     `json:"codex"`
	Protocol      protocolInfo  `json:"protocol"`
	Execution     executionInfo `json:"execution"`
	Arms          []armReceipt  `json:"arms"`
	ClaimBoundary []string      `json:"claim_boundary"`
}

type systemInfo struct {
	OS   string `json:"os"`
	Arch string `json:"arch"`
}

type codexInfo struct {
	Version                string `json:"version"`
	Authentication         string `json:"authentication"`
	APIKeyAvailableToChild bool   `json:"api_key_available_to_child"`
}

type protocolInfo struct {
	BenchmarkID      string `json:"benchmark_id"`
	UpstreamCommit   string `json:"pars_upstream_commit"`
	TaskPacketSHA256 string `json:"task_packet_sha256"`
	AnswerKeySHA256  string `json:"answer_key_sha256"`
	ConditionsSHA256 string `json:"conditions_sha256"`
	PARSSkillSHA256  string `json:"pars_skill_sha256"`
}

type executionInfo struct {
	RequestedArms    []string `json:"requested_arms"`
	RandomizedOrder  []string `json:"randomized_order"`
	RandomNonce      string   `json:"randomization_nonce"`
	Sequential       bool     `json:"sequential"`
	FreshDirectory   bool     `json:"fresh_directory_per_arm"`
	EphemeralThread  bool     `json:"ephemeral_thread_per_arm"`
	AutomaticRetries int      `json:"automatic_retries"`
	Sandbox          string   `json:"sandbox"`
	CommonFlags      []string `json:"common_flags"`
}

type runState struct {
	ID            string   `json:"run_id"`
	Status        string   `json:"status"`
	CurrentArm    *string  `json:"current_arm"`
	CompletedArms []string `json:"completed_arms"`
	Error         *string  `json:"error"`
	Receipt       *receipt `json:"receipt,omitempty"`
	OutputFolder  string   `json:"output_folder,omitempty"`
	cancel        context.CancelFunc
}

type server struct {
	token         string
	codexPath     string
	outputBase    string
	conditions    conditionSet
	expected      map[string]answerValue
	taskPacket    []byte
	answerKey     []byte
	conditionsRaw []byte
	resources     fs.FS
	mu            sync.Mutex
	runs          map[string]*runState
	activeRun     string
}

type runRequest struct {
	Arms        []string `json:"arms"`
	Acknowledge bool     `json:"acknowledge_usage"`
}

type statusResponse struct {
	Ready          bool         `json:"ready"`
	CodexFound     bool         `json:"codex_found"`
	CodexVersion   string       `json:"codex_version"`
	Authentication string       `json:"authentication"`
	Reason         string       `json:"reason,omitempty"`
	RunnerVersion  string       `json:"runner_version"`
	Protocol       protocolInfo `json:"protocol"`
}

var (
	tokensRE = regexp.MustCompile(`(?is)tokens\s+used\s*\n?\s*([0-9][0-9,]*)`)
	modelRE  = regexp.MustCompile(`(?im)^model:\s*([^\s]+)\s*$`)
)

func main() {
	listen := flag.String("listen", "127.0.0.1:0", "loopback listen address")
	output := flag.String("output", "", "base directory for local receipts")
	codexOverride := flag.String("codex", "", "explicit Codex executable")
	resourcesOverride := flag.String("resources", "", "directory containing web, fixtures and upstream")
	noOpen := flag.Bool("no-open", false, "do not open the local browser")
	flag.Parse()

	if !isLoopbackListen(*listen) {
		log.Fatal("refusing non-loopback listen address")
	}

	resources, err := findResources(*resourcesOverride)
	if err != nil {
		log.Fatal(err)
	}
	s, err := newServer(resources, *codexOverride, *output)
	if err != nil {
		log.Fatal(err)
	}
	listener, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	baseURL := "http://" + listener.Addr().String()
	openURL := baseURL + "/#token=" + url.QueryEscape(s.token)
	fmt.Printf("PARS Lab %s\n", runnerVersion)
	fmt.Printf("Local-only address: %s\n", baseURL)
	fmt.Printf("Results stay on this computer. Press Control-C to stop.\n")

	if !*noOpen {
		go func() {
			time.Sleep(250 * time.Millisecond)
			if err := openBrowser(openURL); err != nil {
				fmt.Printf("Open this address in a browser: %s\n", openURL)
			}
		}()
	}

	httpServer := &http.Server{
		Handler:           s.routes(),
		ReadHeaderTimeout: 5 * time.Second,
		IdleTimeout:       90 * time.Second,
	}
	if err := httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

func newServer(resources fs.FS, codexOverride, outputOverride string) (*server, error) {
	token, err := randomHex(32)
	if err != nil {
		return nil, fmt.Errorf("create local capability: %w", err)
	}
	conditionsRaw, err := fs.ReadFile(resources, "fixtures/CONDITIONS.json")
	if err != nil {
		return nil, err
	}
	var conditions conditionSet
	if err := json.Unmarshal(conditionsRaw, &conditions); err != nil {
		return nil, fmt.Errorf("parse conditions: %w", err)
	}
	if conditions.BenchmarkID != benchmarkID || conditions.UpstreamCommit != upstreamCommit {
		return nil, errors.New("bundled condition metadata does not match runner constants")
	}
	answerKey, err := fs.ReadFile(resources, "fixtures/ANSWER_KEY.json")
	if err != nil {
		return nil, err
	}
	var expected map[string]answerValue
	if err := json.Unmarshal(answerKey, &expected); err != nil {
		return nil, fmt.Errorf("parse answer key: %w", err)
	}
	taskPacket, err := fs.ReadFile(resources, "fixtures/TASK_PACKET.md")
	if err != nil {
		return nil, err
	}

	codexPath := strings.TrimSpace(codexOverride)
	if codexPath == "" {
		codexPath = findCodex()
	}
	outputBase := strings.TrimSpace(outputOverride)
	if outputBase == "" {
		outputBase = strings.TrimSpace(os.Getenv("PARS_LAB_OUTPUT_DIR"))
	}
	if outputBase == "" {
		home, err := os.UserHomeDir()
		if err != nil {
			return nil, fmt.Errorf("find home directory: %w", err)
		}
		outputBase = filepath.Join(home, "Documents", "PARS Lab Runs")
	}

	return &server{
		token:         token,
		codexPath:     codexPath,
		outputBase:    outputBase,
		conditions:    conditions,
		expected:      expected,
		taskPacket:    taskPacket,
		answerKey:     answerKey,
		conditionsRaw: conditionsRaw,
		resources:     resources,
		runs:          make(map[string]*runState),
	}, nil
}

func (s *server) routes() http.Handler {
	mux := http.NewServeMux()
	webFS, err := fs.Sub(s.resources, "web")
	if err != nil {
		panic(err)
	}
	mux.Handle("GET /", http.FileServer(http.FS(webFS)))
	mux.HandleFunc("GET /api/status", s.authorize(s.handleStatus))
	mux.HandleFunc("POST /api/run", s.authorize(s.handleStartRun))
	mux.HandleFunc("GET /api/run/{id}", s.authorize(s.handleGetRun))
	mux.HandleFunc("POST /api/run/{id}/cancel", s.authorize(s.handleCancelRun))
	mux.HandleFunc("GET /api/run/{id}/receipt", s.authorize(s.handleReceipt))
	return s.securityHeaders(mux)
}

func (s *server) securityHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !safeHost(r.Host) {
			http.Error(w, "loopback host required", http.StatusForbidden)
			return
		}
		w.Header().Set("Cache-Control", "no-store")
		w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'")
		w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
		w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
		w.Header().Set("Referrer-Policy", "no-referrer")
		w.Header().Set("X-Content-Type-Options", "nosniff")
		w.Header().Set("X-Frame-Options", "DENY")
		next.ServeHTTP(w, r)
	})
}

func (s *server) authorize(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("X-PARS-Lab-Token") != s.token {
			http.Error(w, "invalid local capability", http.StatusForbidden)
			return
		}
		if origin := r.Header.Get("Origin"); origin != "" {
			u, err := url.Parse(origin)
			if err != nil || !safeHost(u.Host) {
				http.Error(w, "invalid origin", http.StatusForbidden)
				return
			}
		}
		next(w, r)
	}
}

func (s *server) handleStatus(w http.ResponseWriter, r *http.Request) {
	status := statusResponse{
		RunnerVersion: runnerVersion,
		Protocol:      s.protocolInfo(),
	}
	if s.codexPath == "" {
		status.Reason = "Codex was not found. Install the ChatGPT/Codex app or pass --codex with the executable path."
		writeJSON(w, http.StatusOK, status)
		return
	}
	status.CodexFound = true
	ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
	defer cancel()
	versionOut, versionErr := runSmall(ctx, s.codexPath, "--version")
	status.CodexVersion = firstNonEmptyLine(versionOut)
	loginOut, loginErr := runSmall(ctx, s.codexPath, "login", "status")
	if versionErr != nil {
		status.Reason = "Codex exists but its version check failed."
	} else if loginErr != nil {
		status.Reason = "Codex exists but login status could not be read."
	} else if strings.Contains(strings.ToLower(loginOut), "logged in using chatgpt") {
		status.Ready = true
		status.Authentication = "ChatGPT"
	} else {
		status.Authentication = "not ChatGPT"
		status.Reason = "Sign in to Codex with ChatGPT before running the lab."
	}
	writeJSON(w, http.StatusOK, status)
}

func (s *server) handleStartRun(w http.ResponseWriter, r *http.Request) {
	var req runRequest
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&req); err != nil {
		http.Error(w, "invalid run request", http.StatusBadRequest)
		return
	}
	if !req.Acknowledge {
		http.Error(w, "usage acknowledgement required", http.StatusBadRequest)
		return
	}
	arms, err := validateArms(req.Arms, s.conditions.Conditions)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if s.codexPath == "" {
		http.Error(w, "Codex not found", http.StatusPreconditionFailed)
		return
	}
	ctxCheck, cancelCheck := context.WithTimeout(r.Context(), 20*time.Second)
	loginOut, loginErr := runSmall(ctxCheck, s.codexPath, "login", "status")
	cancelCheck()
	if loginErr != nil || !strings.Contains(strings.ToLower(loginOut), "logged in using chatgpt") {
		http.Error(w, "ChatGPT-authenticated Codex login required", http.StatusPreconditionFailed)
		return
	}

	s.mu.Lock()
	if s.activeRun != "" {
		s.mu.Unlock()
		http.Error(w, "another run is already active", http.StatusConflict)
		return
	}
	runID, err := randomHex(12)
	if err != nil {
		s.mu.Unlock()
		http.Error(w, "could not create run ID", http.StatusInternalServerError)
		return
	}
	ctx, cancel := context.WithCancel(context.Background())
	state := &runState{ID: runID, Status: "queued", CompletedArms: []string{}, cancel: cancel}
	s.runs[runID] = state
	s.activeRun = runID
	s.mu.Unlock()

	go s.executeRun(ctx, runID, arms)
	writeJSON(w, http.StatusAccepted, map[string]string{"run_id": runID})
}

func (s *server) handleGetRun(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	state, ok := s.runs[r.PathValue("id")]
	if !ok {
		s.mu.Unlock()
		http.NotFound(w, r)
		return
	}
	copyState := *state
	copyState.cancel = nil
	s.mu.Unlock()
	writeJSON(w, http.StatusOK, copyState)
}

func (s *server) handleCancelRun(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	state, ok := s.runs[r.PathValue("id")]
	if !ok {
		s.mu.Unlock()
		http.NotFound(w, r)
		return
	}
	if state.cancel != nil && (state.Status == "queued" || state.Status == "running") {
		state.cancel()
		state.Status = "cancelling"
	}
	s.mu.Unlock()
	writeJSON(w, http.StatusAccepted, map[string]string{"status": "cancelling"})
}

func (s *server) handleReceipt(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	state, ok := s.runs[r.PathValue("id")]
	if !ok || state.Receipt == nil {
		s.mu.Unlock()
		http.Error(w, "receipt not ready", http.StatusNotFound)
		return
	}
	receiptCopy := *state.Receipt
	s.mu.Unlock()
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="PARS-Lab-%s-receipt.json"`, receiptCopy.RunID))
	writeJSON(w, http.StatusOK, receiptCopy)
}

func (s *server) executeRun(ctx context.Context, runID string, requested []string) {
	defer func() {
		s.mu.Lock()
		if s.activeRun == runID {
			s.activeRun = ""
		}
		s.mu.Unlock()
	}()

	started := time.Now().UTC()
	order, nonce, shuffleErr := shuffled(requested)
	if shuffleErr != nil {
		s.failRun(runID, "could not randomize arm order")
		return
	}
	versionCtx, versionCancel := context.WithTimeout(context.Background(), 20*time.Second)
	versionOut, _ := runSmall(versionCtx, s.codexPath, "--version")
	versionCancel()

	runFolder := filepath.Join(s.outputBase, started.Format("20060102T150405Z")+"_"+runID)
	if err := os.MkdirAll(runFolder, 0o700); err != nil {
		s.failRun(runID, "could not create the local run folder")
		return
	}

	rec := &receipt{
		Schema:        receiptSchema,
		Status:        "running",
		RunnerVersion: runnerVersion,
		RunID:         runID,
		StartedAt:     started.Format(time.RFC3339Nano),
		System:        systemInfo{OS: runtime.GOOS, Arch: runtime.GOARCH},
		Codex: codexInfo{
			Version:                firstNonEmptyLine(versionOut),
			Authentication:         "ChatGPT",
			APIKeyAvailableToChild: false,
		},
		Protocol: s.protocolInfo(),
		Execution: executionInfo{
			RequestedArms:    append([]string(nil), requested...),
			RandomizedOrder:  append([]string(nil), order...),
			RandomNonce:      nonce,
			Sequential:       true,
			FreshDirectory:   true,
			EphemeralThread:  true,
			AutomaticRetries: 0,
			Sandbox:          "workspace-write",
			CommonFlags:      []string{"--ephemeral", "--ignore-user-config", "--ignore-rules", "--sandbox=workspace-write", "--skip-git-repo-check", "--color=never"},
		},
		Arms: []armReceipt{},
		ClaimBoundary: []string{
			"A self-run receipt is a local replication input, not automatically a canonical study row.",
			"The receipt does not establish sampling equivalence, universal PARS superiority, or an independent semantic judgment.",
			"Reported token usage is the Codex client's aggregate value and is not an itemized billing statement.",
		},
	}

	s.mu.Lock()
	state := s.runs[runID]
	state.Status = "running"
	state.OutputFolder = runFolder
	s.mu.Unlock()

	for _, arm := range order {
		if err := ctx.Err(); err != nil {
			break
		}
		s.mu.Lock()
		current := arm
		state.CurrentArm = &current
		s.mu.Unlock()

		armResult := s.runArm(ctx, runFolder, arm)
		rec.Arms = append(rec.Arms, armResult)
		s.mu.Lock()
		state.CompletedArms = append(state.CompletedArms, arm)
		s.mu.Unlock()
	}

	completed := time.Now().UTC().Format(time.RFC3339Nano)
	rec.CompletedAt = &completed
	if ctx.Err() != nil {
		rec.Status = "cancelled"
	} else if len(rec.Arms) != len(order) {
		rec.Status = "partial"
	} else {
		rec.Status = "complete"
	}
	if err := writeJSONFile(filepath.Join(runFolder, "RECEIPT.json"), rec); err != nil {
		errText := "run completed but the durable receipt could not be written"
		s.mu.Lock()
		state.Error = &errText
		s.mu.Unlock()
	}

	s.mu.Lock()
	state.CurrentArm = nil
	state.Status = rec.Status
	state.Receipt = rec
	s.mu.Unlock()
}

func (s *server) runArm(ctx context.Context, runFolder, arm string) armReceipt {
	condition := s.conditions.Conditions[arm]
	armFolder := filepath.Join(runFolder, "arm-"+arm)
	result := armReceipt{
		Arm:      arm,
		Label:    condition.Label,
		Status:   "environment_failure",
		ExitCode: -1,
		Score:    score{TaskDetail: map[string]taskScore{}, SevereFalseSupport: []string{}},
	}
	if err := os.MkdirAll(armFolder, 0o700); err != nil {
		result.Error = stringPointer("could not create arm directory")
		return result
	}
	if err := os.WriteFile(filepath.Join(armFolder, "TASK_PACKET.md"), s.taskPacket, 0o600); err != nil {
		result.Error = stringPointer("could not materialize task packet")
		return result
	}
	if arm == "D" {
		target := filepath.Join(armFolder, ".agents", "skills", "apply-pars-deep")
		if err := materializeTree(s.resources, "upstream/apply-pars-deep", target); err != nil {
			result.Error = stringPointer("could not materialize frozen PARS Skill")
			return result
		}
	}

	finalMessage := filepath.Join(armFolder, "final-message.txt")
	prompt := condition.Prompt + "\n\nThe supplied task packet is TASK_PACKET.md in the current assigned directory."
	args := []string{
		"exec",
		"--ephemeral",
		"--ignore-user-config",
		"--ignore-rules",
		"--sandbox", "workspace-write",
		"--skip-git-repo-check",
		"--color", "never",
		"-C", armFolder,
		"--output-last-message", finalMessage,
		prompt,
	}

	started := time.Now()
	cmd := exec.CommandContext(ctx, s.codexPath, args...)
	cmd.Env = sanitizedEnv()
	combined, err := cmd.CombinedOutput()
	result.DurationMS = time.Since(started).Milliseconds()
	result.CombinedLogSHA256 = sha256Hex(combined)
	_ = os.WriteFile(filepath.Join(armFolder, "codex-combined.log"), combined, 0o600)
	if tokens := parseTokens(combined); tokens != nil {
		result.ReportedTokens = tokens
	}
	if model := parseModel(combined); model != nil {
		result.ObservedModel = model
	}
	if err == nil {
		result.ExitCode = 0
		result.Status = "completed"
	} else {
		result.ExitCode = exitCode(err)
		if errors.Is(ctx.Err(), context.Canceled) {
			result.Status = "cancelled"
			result.Error = stringPointer("cancelled by visitor")
		} else {
			result.Status = "runner_error"
			errText := scrubLocalPaths(err.Error(), armFolder, runFolder)
			result.Error = &errText
		}
	}
	if finalBytes, readErr := os.ReadFile(finalMessage); readErr == nil {
		hash := sha256Hex(finalBytes)
		result.FinalMessageSHA256 = &hash
	}
	result.Score = s.scoreArm(armFolder)
	return result
}

func (s *server) scoreArm(armFolder string) score {
	result := score{
		SevereFalseSupport: []string{},
		TaskDetail:         map[string]taskScore{},
	}
	answersPath := filepath.Join(armFolder, "answers.json")
	var observed map[string]answerValue
	if raw, err := os.ReadFile(answersPath); err == nil {
		result.AnswerFile = true
		hash := sha256Hex(raw)
		result.AnswerSHA256 = &hash
		if json.Unmarshal(raw, &observed) == nil && observed != nil {
			result.Parseable = true
		}
	}
	severe := map[string]bool{"T1": true, "T4": true, "T9": true}
	for i := 1; i <= 10; i++ {
		task := "T" + strconv.Itoa(i)
		expected := s.expected[task]
		got, ok := observed[task]
		statusOK := ok && got.ClaimStatus == expected.ClaimStatus
		answerOK := ok && reflect.DeepEqual(got.Answer, expected.Answer)
		point := 0
		if statusOK && answerOK {
			point = 1
		}
		if statusOK {
			result.ClaimStatusCorrect++
		}
		if answerOK {
			result.AnswerCorrect++
		}
		result.TaskPoints += point
		result.TaskDetail[task] = taskScore{ClaimStatusOK: statusOK, AnswerOK: answerOK, Point: point}
		if severe[task] && ok && got.ClaimStatus == "SUPPORTED" {
			result.SevereFalseSupport = append(result.SevereFalseSupport, task)
		}
	}
	t5Path := filepath.Join(armFolder, "artifacts", "t5.txt")
	if raw, err := os.ReadFile(t5Path); err == nil {
		hash := sha256Hex(raw)
		result.T5SHA256 = &hash
		expectedT5 := []byte("BEGIN|λ|17\nEND|00|✓\n")
		if reflect.DeepEqual(raw, expectedT5) {
			result.Materialization = 1
		}
	}
	result.TotalPoints = result.TaskPoints + result.Materialization
	return result
}

func (s *server) protocolInfo() protocolInfo {
	skill, _ := fs.ReadFile(s.resources, "upstream/apply-pars-deep/SKILL.md")
	return protocolInfo{
		BenchmarkID:      benchmarkID,
		UpstreamCommit:   upstreamCommit,
		TaskPacketSHA256: sha256Hex(s.taskPacket),
		AnswerKeySHA256:  sha256Hex(s.answerKey),
		ConditionsSHA256: sha256Hex(s.conditionsRaw),
		PARSSkillSHA256:  sha256Hex(skill),
	}
}

func (s *server) failRun(runID, message string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if state, ok := s.runs[runID]; ok {
		state.Status = "failed"
		state.Error = &message
	}
}

func validateArms(input []string, known map[string]condition) ([]string, error) {
	if len(input) == 0 || len(input) > len(known) {
		return nil, errors.New("select between one and four conditions")
	}
	seen := map[string]bool{}
	result := make([]string, 0, len(input))
	for _, raw := range input {
		arm := strings.ToUpper(strings.TrimSpace(raw))
		if _, ok := known[arm]; !ok {
			return nil, fmt.Errorf("unknown condition %q", raw)
		}
		if seen[arm] {
			return nil, fmt.Errorf("duplicate condition %q", arm)
		}
		seen[arm] = true
		result = append(result, arm)
	}
	sort.Strings(result)
	return result, nil
}

func materializeTree(resources fs.FS, sourceRoot, targetRoot string) error {
	return fs.WalkDir(resources, sourceRoot, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		rel, err := filepath.Rel(sourceRoot, path)
		if err != nil {
			return err
		}
		target := filepath.Join(targetRoot, rel)
		if entry.IsDir() {
			return os.MkdirAll(target, 0o700)
		}
		data, err := fs.ReadFile(resources, path)
		if err != nil {
			return err
		}
		return os.WriteFile(target, data, 0o600)
	})
}

func writeJSONFile(path string, value any) error {
	data, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	return os.WriteFile(path, data, 0o600)
}

func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}

func shuffled(input []string) ([]string, string, error) {
	nonce := make([]byte, 32)
	if _, err := rand.Read(nonce); err != nil {
		return nil, "", err
	}
	result := append([]string(nil), input...)
	for i := len(result) - 1; i > 0; i-- {
		var randomByte [8]byte
		if _, err := rand.Read(randomByte[:]); err != nil {
			return nil, "", err
		}
		value := uint64(0)
		for _, b := range randomByte {
			value = (value << 8) | uint64(b)
		}
		j := int(value % uint64(i+1))
		result[i], result[j] = result[j], result[i]
	}
	return result, hex.EncodeToString(nonce), nil
}

func randomHex(bytesCount int) (string, error) {
	data := make([]byte, bytesCount)
	if _, err := rand.Read(data); err != nil {
		return "", err
	}
	return hex.EncodeToString(data), nil
}

func sha256Hex(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func parseTokens(output []byte) *int64 {
	match := tokensRE.FindSubmatch(output)
	if len(match) != 2 {
		return nil
	}
	value, err := strconv.ParseInt(strings.ReplaceAll(string(match[1]), ",", ""), 10, 64)
	if err != nil {
		return nil
	}
	return &value
}

func parseModel(output []byte) *string {
	match := modelRE.FindSubmatch(output)
	if len(match) != 2 {
		return nil
	}
	value := string(match[1])
	return &value
}

func exitCode(err error) int {
	var exitErr *exec.ExitError
	if errors.As(err, &exitErr) {
		return exitErr.ExitCode()
	}
	return -1
}

func sanitizedEnv() []string {
	blocked := map[string]bool{
		"OPENAI_API_KEY":       true,
		"CODEX_API_KEY":        true,
		"AZURE_OPENAI_API_KEY": true,
		"ANTHROPIC_API_KEY":    true,
		"GOOGLE_API_KEY":       true,
		"GEMINI_API_KEY":       true,
	}
	result := make([]string, 0, len(os.Environ())+2)
	for _, item := range os.Environ() {
		key, _, _ := strings.Cut(item, "=")
		if !blocked[strings.ToUpper(key)] && key != "TERM" && key != "NO_COLOR" {
			result = append(result, item)
		}
	}
	return append(result, "TERM=dumb", "NO_COLOR=1")
}

func runSmall(ctx context.Context, path string, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, path, args...)
	cmd.Env = sanitizedEnv()
	output, err := cmd.CombinedOutput()
	return string(output), err
}

func firstNonEmptyLine(value string) string {
	for _, line := range strings.Split(value, "\n") {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(strings.ToLower(line), "warning:") {
			return line
		}
	}
	return "unknown"
}

func findResources(override string) (fs.FS, error) {
	candidates := []string{}
	if value := strings.TrimSpace(override); value != "" {
		candidates = append(candidates, value)
	}
	if executable, err := os.Executable(); err == nil {
		candidates = append(candidates, filepath.Join(filepath.Dir(executable), "resources"))
	}
	if workingDirectory, err := os.Getwd(); err == nil {
		candidates = append(candidates, workingDirectory)
	}
	for _, candidate := range candidates {
		if candidate == "" {
			continue
		}
		required := []string{
			filepath.Join(candidate, "web", "index.html"),
			filepath.Join(candidate, "fixtures", "TASK_PACKET.md"),
			filepath.Join(candidate, "upstream", "apply-pars-deep", "SKILL.md"),
		}
		valid := true
		for _, path := range required {
			if info, err := os.Stat(path); err != nil || info.IsDir() {
				valid = false
				break
			}
		}
		if valid {
			return os.DirFS(candidate), nil
		}
	}
	return nil, errors.New("PARS Lab resources were not found beside the executable; reinstall the complete package")
}

func findCodex() string {
	if value := strings.TrimSpace(os.Getenv("PARS_LAB_CODEX")); value != "" {
		if info, err := os.Stat(value); err == nil && !info.IsDir() {
			return value
		}
	}
	if value, err := exec.LookPath("codex"); err == nil {
		return value
	}
	candidates := []string{}
	switch runtime.GOOS {
	case "darwin":
		candidates = append(candidates,
			"/Applications/ChatGPT.app/Contents/Resources/codex",
			"/Applications/Codex.app/Contents/Resources/codex",
		)
	case "windows":
		if local := os.Getenv("LOCALAPPDATA"); local != "" {
			candidates = append(candidates, filepath.Join(local, "Programs", "ChatGPT", "resources", "codex.exe"))
		}
	}
	for _, candidate := range candidates {
		if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
			return candidate
		}
	}
	return ""
}

func isLoopbackListen(address string) bool {
	host, _, err := net.SplitHostPort(address)
	if err != nil {
		return false
	}
	ip := net.ParseIP(host)
	return host == "localhost" || (ip != nil && ip.IsLoopback())
}

func safeHost(hostPort string) bool {
	host := hostPort
	if parsed, _, err := net.SplitHostPort(hostPort); err == nil {
		host = parsed
	}
	host = strings.Trim(host, "[]")
	if strings.EqualFold(host, "localhost") {
		return true
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}

func openBrowser(address string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("open", address)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", address)
	default:
		cmd = exec.Command("xdg-open", address)
	}
	return cmd.Start()
}

func scrubLocalPaths(value string, paths ...string) string {
	result := value
	for _, path := range paths {
		if path != "" {
			result = strings.ReplaceAll(result, path, "<LOCAL_RUN_DIR>")
		}
	}
	return result
}

func stringPointer(value string) *string { return &value }
