package model

import (
	"context"

	"github.com/ardanlabs/kronk/sdk/kronk/applog"
)

// =============================================================================
// Parser Plugin Contract
//
// A parser plugin teaches the model server how a particular model lineage
// (Qwen, GPT-OSS, Mistral, Gemma, GLM, …) emits two kinds of structured
// content during streaming generation:
//
//  1. Reasoning vs answer vs tool-call content (per-token classification),
//     surfaced through the StateMachine interface.
//
//  2. Final tool-call payloads (parsed once at end-of-stream from the
//     accumulated tool-call buffer), surfaced through ToolCall.
//
// A single Parser implementation is selected at Model.Load() based on
// the chat template, architecture, and model name. The selected plugin
// lives on *Model for the lifetime of the model and is safe for concurrent
// use across slots — except for NewStateMachine, which returns a fresh,
// slot-owned state machine on every call.
//
// Plugins live under sdk/kronk/parsers/<name>/ and are wired into the
// model package via RegisterParser at server bootstrap (no init()
// registration — every parser is explicitly enumerated at startup).
// =============================================================================

// Channel labels the semantic class of an emitted token, mapping 1:1 to the
// OpenAI chat/completions delta fields produced by the SSE writer.
type Channel uint8

const (
	// ChannelNone indicates a structural marker the caller should not
	// surface in either content or tool-call output. The token is still
	// counted toward the slot's output-token total.
	ChannelNone Channel = iota

	// ChannelReasoning content is accumulated into the response's
	// reasoning_content field and streamed as reasoning deltas.
	ChannelReasoning

	// ChannelAnswer content is accumulated into the response's content
	// field and streamed as content deltas.
	ChannelAnswer

	// ChannelTool content is accumulated into the slot's tool-call buffer
	// and parsed once via ToolCall when generation finishes.
	ChannelTool
)

// Result is the per-token outcome returned by StateMachine.Classify.
//
// Content may be empty when the token is a structural marker that has been
// fully consumed by the state machine (e.g. <think>, <tool_call>). When
// Content is non-empty, it is routed to the appropriate accumulator based on
// Channel.
type Result struct {
	Channel Channel
	Content string
}

// StateMachine is the per-request, per-slot streaming state machine. One
// instance is created per slot via Parser.NewStateMachine and reused
// across requests on that slot via Reset.
//
// Behavior is undefined if Classify is called after a previous call returned
// eog=true. Callers must invoke Reset before reusing the state machine.
type StateMachine interface {
	// Classify classifies a single decoded token's content and returns the
	// Result plus whether the model has signaled end-of-generation.
	Classify(content string) (r Result, eog bool)

	// Reset returns the state machine to its initial state for reuse.
	Reset()
}

// Parser is the plugin interface implemented by each model lineage.
// Implementations live in sdk/kronk/parsers/<name>/ and are registered
// at startup via RegisterParser.
type Parser interface {
	// Name returns the parser identifier (e.g. "standard", "gpt-oss").
	// Used for logging and as the override key in model configs.
	Name() string

	// NewStateMachine returns a fresh per-slot state machine. Callers must
	// not share StateMachine instances across slots.
	NewStateMachine() StateMachine

	// ToolCall parses the accumulated tool-call buffer into structured
	// tool calls. Called once when generation finishes, never on the hot
	// per-token path. The logger is used for repair/parse failures; tests
	// may pass a no-op logger.
	ToolCall(ctx context.Context, log applog.Logger, buf string) []ResponseToolCall
}

// ParamsAdjuster is an optional interface a Parser may implement to coerce
// request Params into values its model lineage's chat template will accept.
// It is invoked at the end of Model.adjustParams, after global defaults have
// been applied. Use cases include clamping reasoning_effort to the subset of
// values a strict template (e.g. Mistral Medium 3.5) will validate.
type ParamsAdjuster interface {
	AdjustParams(p Params) Params
}

// ReasoningNormalizer is an optional interface a Parser may implement to
// remove model-specific reasoning markup that destabilizes the incremental
// message cache (IMC).
//
// Reasoning is ephemeral: replaying it on prior assistant turns adds prompt
// tokens that do not improve generation, and the markup is rendered
// inconsistently across turns (a turn that emitted reasoning as the "current"
// turn re-renders without it once it becomes history), shifting the tokenized
// prefix and forcing full cache rebuilds. When IMC is on and preserve_thinking
// is off, the engine drops the reasoning / reasoning_content fields from
// assistant history (family-agnostic) and then invokes the normalizer for the
// lineage-specific markup the field-drop cannot reach.
//
// Both methods must be deterministic and idempotent: applying them twice
// yields the same result as applying them once.
type ReasoningNormalizer interface {
	// StripReasoningContent removes closed reasoning spans embedded directly
	// in an assistant message's content (e.g. <think>…</think> for Qwen,
	// <|channel>thought…<channel|> for Gemma). Text outside the spans is
	// preserved. Invoked on history before the Jinja render.
	StripReasoningContent(content string) string

	// StripEmptyReasoning removes empty reasoning spans from a fully rendered
	// prompt (e.g. "<think>\n\n</think>"), leaving a trailing span — the
	// generation-prompt marker that primes the model to answer — intact.
	// Used as a post-render pass so position-dependent template emission of
	// empty reasoning blocks does not shift the tokenized prefix across turns.
	StripEmptyReasoning(rendered string) string
}

// Fingerprint carries the model metadata that parser selection logic
// inspects at Model.Load time.
type Fingerprint struct {
	ChatTemplate string // raw jinja chat template
	Architecture string // gguf "general.architecture" (e.g. "llama", "qwen2")
	ModelName    string // gguf "general.name" (e.g. "Qwen3-Coder-30B-A3B")
}
