// Package codex is the per-CLI adapter for OpenAI's `codex` CLI
// (subscription-billed via ChatGPT Plus/Pro). Same shape as the claudecode
// adapter: render the harness envelope as a codex-friendly prompt, drive
// the conversation through tmux.RealPane, estimate tokens from characters,
// detect rate-limit signatures.
//
// Calibration notes:
//   - The token-estimation constant (chars/4) is a baseline; codex's
//     responses tend to be slightly denser than claude's, so 3.6 chars/tok
//     is closer once measured. Update the EstimateTokens function if a
//     calibration corpus says otherwise.
//   - Rate-limit phrasing differs: codex tends to say "you've hit the rate
//     limit" or "429 - too many requests"; regex below covers both.
//   - The prompt prefix is "user> " in non-interactive mode and ">" in TUI
//     mode; we match either.
package codex

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"regexp"
	"strings"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/envelope"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime/tmux"
)

// CheckAuth verifies the codex CLI is on PATH. Minimal probe — extend
// this to mirror claudecode.CheckAuth when codex grows a JSON probe
// mode. Returns a descriptive error if the binary is missing.
func CheckAuth(ctx context.Context) error {
	if _, err := exec.LookPath("codex"); err != nil {
		return fmt.Errorf("codex CLI not found on PATH")
	}
	return nil
}

// PaneAdapter wraps a tmux.RealPane configured for `codex`.
type PaneAdapter struct {
	Pane *tmux.RealPane
}

// rateLimitSig matches codex-specific rate-limit output. Adjust as the CLI
// evolves.
var rateLimitSig = regexp.MustCompile(`(?i)rate limit|too many requests|429|usage limit|quota exceeded`)

// promptSig matches codex's prompt cue. Both `user> ` (REPL) and `> ` (TUI)
// shapes appear in the wild.
var promptSig = regexp.MustCompile(`(?m)^(user>|>) ?$`)

// NewPane constructs a RealPane configured for the codex CLI. The default
// command launches codex in interactive mode; callers can override
// `pane.CLICmd` if they need a different flag set.
func NewPane(socketName, sessionName, logPath string) *tmux.RealPane {
	return &tmux.RealPane{
		SocketName:     socketName,
		Session:        sessionName,
		LogPath:        logPath,
		CLICmd:         []string{"codex"},
		PromptRegex:    promptSig,
		RateLimitRegex: rateLimitSig,
	}
}

// Send renders the envelope-in payload as a codex-friendly prompt and
// forwards to the underlying pane.
func (a *PaneAdapter) Send(ctx context.Context, payload string) error {
	rendered := renderPrompt(payload)
	return a.Pane.Send(ctx, rendered)
}

// Receive proxies to the underlying pane.
func (a *PaneAdapter) Receive(ctx context.Context) (string, error) {
	return a.Pane.Receive(ctx)
}

// renderPrompt translates the harness envelope into a codex-friendly prompt.
// Same `harness-out` fenced-block contract as the other adapters — that's
// the deterministic turn-done signal.
func renderPrompt(payload string) string {
	var in struct {
		Prompt        string             `json:"prompt"`
		Incoming      *envelope.Envelope `json:"incoming"`
		AvailableTools []string          `json:"available_tools"`
		SystemPrompt  string             `json:"system_prompt"`
	}
	if err := json.Unmarshal([]byte(payload), &in); err != nil {
		return payload
	}
	var sb strings.Builder
	if in.SystemPrompt != "" {
		sb.WriteString("=== ROLE ===\n")
		sb.WriteString(in.SystemPrompt)
		sb.WriteString("\n=== END ROLE ===\n\n")
	}
	sb.WriteString("[harness-turn]\n")
	if in.Prompt != "" {
		sb.WriteString("Prompt: ")
		sb.WriteString(in.Prompt)
		sb.WriteString("\n")
	}
	if in.Incoming != nil {
		sb.WriteString("Inbox message:\n")
		sb.WriteString(fmt.Sprintf("  id: %s\n", in.Incoming.ID))
		sb.WriteString(fmt.Sprintf("  type: %s\n", in.Incoming.Type))
		sb.WriteString(fmt.Sprintf("  from: %s\n", in.Incoming.From))
		sb.WriteString(fmt.Sprintf("  task_id: %s\n", in.Incoming.TaskID))
		if in.Incoming.Payload.Intent != "" {
			sb.WriteString(fmt.Sprintf("  intent: %s\n", in.Incoming.Payload.Intent))
		}
		if in.Incoming.Payload.Expects != "" {
			sb.WriteString(fmt.Sprintf("  expects: %s\n", in.Incoming.Payload.Expects))
		}
		if len(in.Incoming.Payload.ContextRefs) > 0 {
			sb.WriteString("  context_refs:\n")
			for _, r := range in.Incoming.Payload.ContextRefs {
				sb.WriteString("    - ")
				sb.WriteString(r)
				sb.WriteString("\n")
			}
		}
	}
	if len(in.AvailableTools) > 0 {
		sb.WriteString("Available tools: ")
		sb.WriteString(strings.Join(in.AvailableTools, ", "))
		sb.WriteString("\n")
	}
	sb.WriteString("\nRespond ONLY with a fenced harness-out block of JSON. Schema:\n")
	sb.WriteString("```harness-out\n{\"text\":\"<short summary>\",\"tool_calls\":[{\"name\":\"<tool>\",\"args\":{...}}],\"tokens\":{\"prompt\":0,\"completion\":0}}\n```\n")
	sb.WriteString("\nAlways emit the block, even with empty tool_calls. Do not include any prose outside the block.\n")
	return sb.String()
}

// EstimateTokens is calibrated for codex output. Codex tends to produce
// slightly denser tokens than claude on the same prompt; 3.6 chars/token
// is closer than the universal 4. Replace the constant when you have a
// regression-fit value from your own corpus.
func EstimateTokens(prompt, completion string) runtime.TokenUsage {
	return runtime.TokenUsage{
		Prompt:     int(float64(len(prompt)) / 3.6),
		Completion: int(float64(len(completion)) / 3.6),
	}
}

// PaneFactory returns a factory suitable for tmux.New(...). chInfo is
// reserved for sidecar-channel wiring; the codex adapter does not yet
// support sidecar channels, so it is ignored here.
func PaneFactory(socketName, logsDir string) func(ctx context.Context, spec runtime.SpawnSpec, chInfo *tmux.ChannelPromptInfo) (tmux.PaneIO, error) {
	return func(ctx context.Context, spec runtime.SpawnSpec, chInfo *tmux.ChannelPromptInfo) (tmux.PaneIO, error) {
		_ = chInfo
		if !tmux.TmuxAvailable() {
			return nil, errors.New("codex: tmux not available on PATH")
		}
		pane := NewPane(socketName, "harness-"+spec.AgentID, logsDir+"/"+spec.AgentID+".log")
		if err := pane.Open(ctx); err != nil {
			return nil, err
		}
		return &PaneAdapter{Pane: pane}, nil
	}
}
