// Package claudecode is the per-CLI adapter for Anthropic's `claude` CLI
// (Claude Code, subscription-billed). It wraps a tmux.RealPane and:
//   - renders our envelope into a claude-friendly prompt
//   - drives the conversation through tmux.RealPane.Send + Receive
//   - estimates tokens from character counts (~chars/4 baseline)
//   - detects rate-limit signatures
//
// One adapter = one tmux.RealPane = one persistent `claude` session.
package claudecode

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

	"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"
)

// authErrorSig matches the well-known claude CLI auth-failure phrases.
var authErrorSig = regexp.MustCompile(`(?i)not authenticated|please.*log\s*in|run\s+claude\s+login|unauthorized|invalid (api )?key|session expired|no api key`)

// CheckAuth verifies the claude CLI is on PATH AND authenticated. Runs
// a minimal `claude --print "ping"` subprocess with a short timeout.
// Returns nil on success, a descriptive error otherwise.
func CheckAuth(ctx context.Context) error {
	if _, err := exec.LookPath("claude"); err != nil {
		return fmt.Errorf("claude CLI not found on PATH (install: https://docs.claude.com/en/docs/claude-code/installation)")
	}
	probeCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()
	cmd := exec.CommandContext(probeCtx, "claude",
		"--print", "ping",
		"--output-format", "json",
		"--model", "claude-haiku-4-5-20251001",
	)
	out, err := cmd.CombinedOutput()
	if err == nil {
		var arr []struct {
			Type      string `json:"type"`
			IsError   bool   `json:"is_error"`
			Subtype   string `json:"subtype"`
			ErrorText string `json:"error"`
			Result    string `json:"result"`
		}
		if jerr := json.Unmarshal(out, &arr); jerr == nil {
			for _, ev := range arr {
				if ev.Type == "result" && ev.IsError {
					return fmt.Errorf("claude auth check rejected: %s (run `claude` interactively once to re-authenticate)", ev.ErrorText)
				}
			}
		}
		return nil
	}
	msg := strings.TrimSpace(string(out))
	if len(msg) > 400 {
		msg = msg[:400] + "…"
	}
	if probeCtx.Err() == context.DeadlineExceeded {
		return fmt.Errorf("claude auth check timed out after 15s — the CLI may be waiting on an interactive auth flow. Run `claude` once to complete login. (probe output: %s)", msg)
	}
	if authErrorSig.MatchString(msg) {
		return fmt.Errorf("claude not authenticated: %s — run `claude` interactively once to log in", msg)
	}
	return fmt.Errorf("claude probe failed: %w (output: %s)", err, msg)
}

// PaneAdapter implements tmux.PaneIO by wrapping a RealPane and adding
// envelope-in prompt rendering. It hides the raw send/receive of RealPane
// behind harness-envelope semantics.
//
// ChannelInstruction, when set, is woven into the rendered prompt so the
// model knows how to use the configured sidecar channel (run
// harness-send via Bash, or Write to the mailbox path). Empty in the
// default TUI-scrape mode.
type PaneAdapter struct {
	Pane               *tmux.RealPane
	ChannelInstruction string
}

// rateLimitSig matches common claude rate-limit phrases. Calibration is
// best-effort — adjust if the CLI changes its wording.
var rateLimitSig = regexp.MustCompile(`(?i)rate limit|you've reached your usage|429`)

// promptSig matches the claude prompt prefix ("> " at start of line or after
// the model finishes a turn). RealPane uses this to detect turn-done.
var promptSig = regexp.MustCompile(`(?m)^>\s*$`)

// NewPane constructs a RealPane configured for the claude CLI.
//
// CLI flags chosen for unattended harness use:
//   --dangerously-skip-permissions: the harness owns the sandbox; without
//     this, every tool call in auto-mode triggers a permission prompt and
//     the agent hangs waiting for input that will never come.
//   --include-hook-events: surface hook failures in the pane output stream
//     so the sidecar parser (and the L6 forensic trail) can SEE when a
//     user-defined Stop hook fails. Otherwise hook errors that prevent
//     turn-completion are invisible to the harness — see live-behavior
//     audit L1 (stop hooks failing on /dev/tty in detached tmux panes).
//
// User-side requirement (documented in live-behavior-audit.md L1):
// user-defined Stop hooks in ~/.claude/settings.json that write to
// /dev/tty MUST guard with `[ -t 1 ]` or `[ -c /dev/tty ]` because the
// harness spawns claude in a DETACHED tmux session (no controlling tty).
// Hooks that don't guard cause the agent to never finalize turns,
// burning ~25s per parse retry in real LLM cost.
func NewPane(socketName, sessionName, logPath string) *tmux.RealPane {
	return &tmux.RealPane{
		SocketName: socketName,
		Session:    sessionName,
		LogPath:    logPath,
		CLICmd: []string{
			"claude",
			"--dangerously-skip-permissions",
			"--include-hook-events",
		},
		PromptRegex:    promptSig,
		RateLimitRegex: rateLimitSig,
	}
}

// Send renders the envelope-in payload as a human-readable prompt and forwards
// to the underlying pane. Payload format expected by RealPane.Send is the raw
// JSON the harness already produces in tmux.Runtime.CallLLM.
func (a *PaneAdapter) Send(ctx context.Context, payload string) error {
	rendered := renderPrompt(payload, a.ChannelInstruction)
	return a.Pane.Send(ctx, rendered)
}

// Receive proxies to the underlying pane's Receive. Output is left as the
// raw pane log so the sidecar parser sees the unmodified harness-out block.
func (a *PaneAdapter) Receive(ctx context.Context) (string, error) {
	return a.Pane.Receive(ctx)
}

// renderPrompt translates the harness's envelope-in payload into a
// claude-friendly prompt. The payload is the JSON {"prompt": "...",
// "incoming": <envelope>, "available_tools": [...]} that
// tmux.Runtime.CallLLM produces.
//
// The prompt instructs claude to ALWAYS emit a fenced `harness-out` block,
// even on a no-op turn. This is the deterministic turn-done signal.
//
// channelInstruction (optional) is the side-channel directive returned
// by tmux.SidecarChannel.Open(). When non-empty, it is woven into the
// output contract so the model uses the channel rather than relying on
// TUI scraping.
func renderPrompt(payload string, channelInstruction 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 {
		// Don't break — pass the raw payload through. Reformat-retry handles it.
		return payload
	}
	var sb strings.Builder
	if in.SystemPrompt != "" {
		// First turn: prepend the role md as a system-style preamble so claude
		// sees its identity + tools + storage conventions before the message.
		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("\n## Output contract (read carefully)\n\n")
	sb.WriteString("Respond ONLY with a fenced `harness-out` block. The block contains JSON matching:\n\n")
	sb.WriteString("```harness-out\n{\"text\":\"<short summary>\",\"tool_calls\":[{\"name\":\"<tool>\",\"args\":{...}}],\"tokens\":{\"prompt\":0,\"completion\":0}}\n```\n\n")
	sb.WriteString("Rules:\n")
	sb.WriteString("- Always emit the block, even on a no-op turn (empty tool_calls).\n")
	sb.WriteString("- No prose outside the block. The harness ignores anything else.\n")
	sb.WriteString("- If you need to ask the user a question, use `tool_calls: [{name:\"request_clarification\", args:{question:\"...\"}}]` rather than asking in prose.\n")
	if channelInstruction != "" {
		// A sidecar channel is active. The model should send its turn
		// output through the channel — the JSON above is the payload
		// shape. The harness reads the channel directly and bypasses
		// TUI scraping for this turn.
		sb.WriteString("\n## Side channel (preferred delivery)\n\n")
		sb.WriteString(channelInstruction)
		sb.WriteString("\n\nThe JSON payload is the same shape as the harness-out block above. ")
		sb.WriteString("Send it via the channel — that is the primary delivery path. ")
		sb.WriteString("The fenced block above is a backwards-compatible fallback for if the channel is unavailable.\n")
	}
	return sb.String()
}

// EstimateTokens is the calibration function for subscription mode. claude
// doesn't expose tokens via CLI, so we approximate from output length. The
// 4 chars/token ratio is a baseline; per-prompt-style calibration can replace
// this with a regressed constant once we measure.
func EstimateTokens(prompt, completion string) runtime.TokenUsage {
	return runtime.TokenUsage{
		Prompt:     len(prompt) / 4,
		Completion: len(completion) / 4,
	}
}

// PaneFactory returns a tmux.Runtime configured for the claude-code adapter.
// Each Spawn call creates a fresh tmux session and PaneAdapter for the agent.
//
// Logs are written under logsDir/<agentID>.log.
//
// When chInfo is non-nil (sidecar channel active), the Env vars are
// exported into the tmux session (so the model's Bash tool sees
// HARNESS_SEND_SOCKET / HARNESS_MAILBOX_PATH) and the Instruction is
// stashed on the adapter for renderPrompt to weave into every turn.
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) {
		if !tmux.TmuxAvailable() {
			return nil, errors.New("claude-code: tmux not available on PATH")
		}
		pane := NewPane(socketName, "harness-"+spec.AgentID, logsDir+"/"+spec.AgentID+".log")
		var instruction string
		if chInfo != nil {
			pane.Env = chInfo.Env
			instruction = chInfo.Instruction
		}
		if err := pane.Open(ctx); err != nil {
			return nil, err
		}
		return &PaneAdapter{Pane: pane, ChannelInstruction: instruction}, nil
	}
}
