// Package gemini is the per-CLI adapter for Google's `gemini` CLI. Either
// AI Studio's free tier or a paid AI Pro subscription works — both run
// the same CLI binary.
//
// Calibration notes:
//   - Gemini's tokenizer is byte-pair similar to Claude's; chars/4 is fine
//     as a first approximation. Calibrate if your domain corpus says
//     otherwise.
//   - Rate-limit phrases the CLI uses include "RESOURCE_EXHAUSTED",
//     "quota exceeded", and "429"; regex below catches all three.
//   - Prompt cue is `> ` (REPL) or `$ ` (script-mode echo); regex handles
//     both.
package gemini

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 gemini CLI is on PATH. Returns a descriptive
// error if the binary is missing.
func CheckAuth(ctx context.Context) error {
	if _, err := exec.LookPath("gemini"); err != nil {
		return fmt.Errorf("gemini CLI not found on PATH")
	}
	return nil
}

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

var rateLimitSig = regexp.MustCompile(`(?i)RESOURCE_EXHAUSTED|quota exceeded|rate limit|429`)
var promptSig = regexp.MustCompile(`(?m)^(>|\$) ?$`)

// NewPane constructs a RealPane configured for the gemini CLI.
func NewPane(socketName, sessionName, logPath string) *tmux.RealPane {
	return &tmux.RealPane{
		SocketName:     socketName,
		Session:        sessionName,
		LogPath:        logPath,
		CLICmd:         []string{"gemini"},
		PromptRegex:    promptSig,
		RateLimitRegex: rateLimitSig,
	}
}

// Send renders the envelope-in payload as a gemini-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 gemini-friendly prompt.
// Gemini prefers concise system instructions; the boilerplate here is
// intentionally shorter than the claudecode version.
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 != "" {
		// Gemini prefers brief; we still include role md but trim leading
		// whitespace.
		sb.WriteString("=== ROLE ===\n")
		sb.WriteString(strings.TrimSpace(in.SystemPrompt))
		sb.WriteString("\n=== END ROLE ===\n\n")
	}
	sb.WriteString("[harness-turn]\n")
	if in.Prompt != "" {
		sb.WriteString(in.Prompt)
		sb.WriteString("\n")
	}
	if in.Incoming != nil {
		sb.WriteString(fmt.Sprintf("\n[message] type=%s from=%s task=%s\n",
			in.Incoming.Type, in.Incoming.From, in.Incoming.TaskID))
		if in.Incoming.Payload.Intent != "" {
			sb.WriteString("intent: ")
			sb.WriteString(in.Incoming.Payload.Intent)
			sb.WriteString("\n")
		}
		if in.Incoming.Payload.Expects != "" {
			sb.WriteString("expects: ")
			sb.WriteString(string(in.Incoming.Payload.Expects))
			sb.WriteString("\n")
		}
		for _, r := range in.Incoming.Payload.ContextRefs {
			sb.WriteString("ref: ")
			sb.WriteString(r)
			sb.WriteString("\n")
		}
	}
	if len(in.AvailableTools) > 0 {
		sb.WriteString("\ntools: ")
		sb.WriteString(strings.Join(in.AvailableTools, ", "))
		sb.WriteString("\n")
	}
	// Gemini prefers terse output schemas without preamble.
	sb.WriteString("\nReply ONLY:\n```harness-out\n{\"text\":\"<short>\",\"tool_calls\":[],\"tokens\":{\"prompt\":0,\"completion\":0}}\n```\n")
	return sb.String()
}

// EstimateTokens is calibrated for gemini output. Default 4 chars/token;
// adjust if your measurements differ.
func EstimateTokens(prompt, completion string) runtime.TokenUsage {
	return runtime.TokenUsage{
		Prompt:     len(prompt) / 4,
		Completion: len(completion) / 4,
	}
}

// PaneFactory returns a factory suitable for tmux.New(...). chInfo is
// reserved for sidecar-channel wiring; the gemini 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("gemini: 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
	}
}
