package generation

import (
	"context"
	"encoding/json"
	"errors"
)

// This file defines the provider-agnostic agent-loop contract used by every
// server-side AI feature (dashboard agent, contact research, automation AI
// nodes, inbox agent). Warmbly's hosted product runs on OpenAI; the same
// interface lets a self-hoster point at any OpenAI-compatible endpoint
// (Ollama, vLLM, LocalAI, OpenRouter) via an AI_PROVIDER preset or AI_BASE_URL, or use the
// Anthropic connector, without any caller change.

// RiskClass classifies what a tool can do, which drives the approval policy in
// the agent loop (read auto-runs; write pauses for approval; send always
// requires an explicit per-action human action).
type RiskClass string

const (
	RiskRead  RiskClass = "read"
	RiskWrite RiskClass = "write"
	RiskSend  RiskClass = "send"
)

// ToolDef is the LLM-facing definition of a tool the agent loop can call. It is
// the provider-level projection of a richer aitools.Tool (M2): only what the
// model needs (name, description, JSON-schema input) plus the Go handler the
// loop executes. Permission gating and approval happen in the caller/registry,
// not inside the provider.
type ToolDef struct {
	Name        string
	Description string
	// InputSchema is a JSON Schema object describing the tool's arguments.
	InputSchema map[string]any
	Risk        RiskClass
	// Handler executes the tool for the given JSON args and returns a result
	// string (usually JSON) fed back to the model as the tool result. An error
	// is surfaced to the model as a tool error so it can recover or stop.
	Handler func(ctx context.Context, args json.RawMessage) (string, error)
}

// ToolCall is one tool invocation the model requested, carried in an assistant
// message so the transcript is provider-agnostic and resumable (M3 persists it
// as agent_messages jsonb).
type ToolCall struct {
	ID   string          `json:"id"`
	Name string          `json:"name"`
	Args json.RawMessage `json:"args"`
}

// AgentMessage is one entry in the running transcript. Role is one of
// "user", "assistant", "tool". An assistant turn may carry ToolCalls; a tool
// turn carries ToolCallID + Content (the result).
type AgentMessage struct {
	Role       string     `json:"role"`
	Content    string     `json:"content,omitempty"`
	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
	ToolCallID string     `json:"tool_call_id,omitempty"`
}

// AgentEventType enumerates the streamed step kinds surfaced via OnEvent (the
// M3 SSE loop maps these to text deltas / tool_start / tool_result events).
type AgentEventType string

const (
	EventText AgentEventType = "text"
	// EventTextDelta is a partial text chunk streamed while the model is
	// still generating; a closing EventText with the full block always
	// follows, so consumers may treat deltas as purely cosmetic.
	EventTextDelta  AgentEventType = "text_delta"
	EventToolStart  AgentEventType = "tool_start"
	EventToolResult AgentEventType = "tool_result"
	EventIteration  AgentEventType = "iteration"
)

// AgentEvent is a single streamed step from RunAgent.
type AgentEvent struct {
	Type       AgentEventType
	Text       string
	ToolName   string
	ToolArgs   json.RawMessage
	ToolResult string
	Iteration  int
}

// AgentRequest is one RunAgent invocation.
type AgentRequest struct {
	// System is the system prompt.
	System string
	// Messages is the running transcript (may include prior tool turns for a
	// resumed run). At minimum one user message.
	Messages []AgentMessage
	// Tools available this run. May be empty for a plain completion.
	Tools []ToolDef
	// Model is the provider model id; empty means the provider's default for
	// the tier (see ModelForTier).
	Model string
	// MaxIterations bounds tool-use rounds (each model call is one iteration).
	// Zero uses the provider default.
	MaxIterations int
	// MaxTokens caps a single completion. Zero uses the provider default.
	MaxTokens int
	// OnEvent, if set, receives streamed step events. Optional.
	OnEvent func(AgentEvent)
	// Approve, if set, is called before executing a non-read tool. Returning
	// ErrApprovalRequired stops the loop and returns a result whose Pending
	// holds the tool call awaiting approval, so the caller can persist state
	// and resume. Returning any other error aborts the run.
	Approve func(ctx context.Context, tool ToolDef, call ToolCall) error
	// PreIteration, if set, is called at the start of each loop iteration
	// (before the model call). Returning an error stops the loop cleanly with
	// StopReason "stopped" and returns the transcript so far. Used to charge
	// per-iteration credits and enforce a per-run budget; the caller records
	// its own reason (e.g. out-of-credits) before returning.
	PreIteration func(ctx context.Context, iteration int) error
}

// PendingToolCall is the tool awaiting approval when a run stops with
// StopReason "approval_required".
type PendingToolCall struct {
	Call ToolCall
	Risk RiskClass
}

// AgentResult is the final output of a RunAgent run.
type AgentResult struct {
	// Text is the model's final assistant text (empty when the run stopped for
	// approval before producing final text).
	Text string
	// Messages is the full transcript including this run's new turns, so a
	// caller can persist it and resume.
	Messages []AgentMessage
	Model    string
	// TokensUsed is the summed input+output tokens across all iterations.
	TokensUsed int
	Iterations int
	// StopReason is "stop" (final text), "max_iterations", or
	// "approval_required".
	StopReason string
	// Pending is set only when StopReason == "approval_required".
	Pending *PendingToolCall
}

// Compile-time guarantees that both providers satisfy Provider, and that the
// OpenAI provider also serves the writing assistant.
var (
	_ Provider         = (*openAIProvider)(nil)
	_ Provider         = (*anthropicProvider)(nil)
	_ WritingGenerator = (*openAIProvider)(nil)
	_ WritingGenerator = (*AnthropicClient)(nil)
	_ WritingGenerator = (*GenerationClient)(nil)
)

// CompletionRequest is a single, tool-less generation with an explicit system
// prompt. Used by reply drafts (M4) and the automation ai_step (generate mode).
type CompletionRequest struct {
	System    string
	Prompt    string
	Model     string
	MaxTokens int
	// Temperature optionally pins sampling. nil leaves it to the provider default
	// (creative writing); a pointer to 0 forces deterministic output (the reply
	// classifier's Layer 3 needs a stable single-label verdict).
	Temperature *float64
}

// Deterministic returns a *float64 pointing at 0, for
// CompletionRequest.Temperature when a caller needs a stable, non-sampled result
// (e.g. single-label classification). Each call returns a fresh pointer.
func Deterministic() *float64 {
	z := 0.0
	return &z
}

// Provider is a pluggable LLM backend that can run a tool-use agent loop.
type Provider interface {
	// RunAgent executes the tool-use loop and returns the final result.
	RunAgent(ctx context.Context, req AgentRequest) (*AgentResult, error)
	// Complete runs a single completion (no tools) with an explicit system
	// prompt, returning the text + token usage.
	Complete(ctx context.Context, req CompletionRequest) (*WritingResult, error)
	// ModelForTier returns the provider's model id for the org tier (paid gets
	// the stronger model).
	ModelForTier(paid bool) string
	// Name identifies the provider ("openai" or "anthropic"). Stays stable for
	// accounting even when pointed at a local/self-hosted endpoint.
	Name() string
	// IsLocal reports whether this provider is an explicitly-declared free/local
	// backend (AI_FREE). Callers use it to warn the user and to skip
	// credit charges; it is never inferred from the base URL (OpenRouter/Azure
	// also set one), so it stays a deliberate operator opt-in.
	IsLocal() bool
}

// ErrApprovalRequired is returned by an AgentRequest.Approve hook to pause the
// loop for human approval of a write/send tool. RunAgent turns it into a
// result with StopReason "approval_required".
var ErrApprovalRequired = errors.New("tool approval required")

// ErrProviderNotConfigured is returned by NewProvider when neither an OpenAI
// nor an Anthropic key is configured.
var ErrProviderNotConfigured = errors.New("no LLM provider configured: set AI_PROVIDER and AI_API_KEY")

// Default agent-loop bounds, applied when a request leaves them zero.
const (
	defaultMaxIterations = 12
	defaultAgentTokens   = 2048
)

// ProviderConfig configures provider construction. Only the keys and optional
// overrides that a self-hoster sets; everything else defaults sensibly.
type ProviderConfig struct {
	// OpenAIAPIKey enables the OpenAI provider (the preferred/default backend).
	OpenAIAPIKey string
	// OpenAIBaseURL overrides the OpenAI API base (e.g. an OpenAI-compatible
	// self-hosted endpoint). Empty uses the public OpenAI API.
	OpenAIBaseURL string
	// OpenAIModelTrial / OpenAIModelPaid override the per-tier model ids (so a
	// self-hoster can route to whatever their endpoint serves). Empty uses the
	// built-in gpt-4o-mini / gpt-4o defaults (or the local default when Local).
	OpenAIModelTrial string
	OpenAIModelPaid  string

	// Local marks this as a free/local model backend (AI_FREE). It flips
	// the empty-model default to a local-friendly tag and lets the agent warn
	// the user and skip credit charges. Set deliberately by the operator; never
	// inferred from OpenAIBaseURL.
	Local bool

	// AnthropicAPIKey enables the Anthropic connector (used only when no OpenAI
	// key is set).
	AnthropicAPIKey string

	// Search is the pluggable web-search client used by the search_web tool
	// (M2). Optional; when nil the tool returns a clean not-configured error.
	Search SearchClient
}

// NewProvider selects and constructs the active provider. OpenAI is preferred
// (Warmbly's hosted default and the pluggable self-host path); the Anthropic
// connector is used only when no OpenAI key is present. Returns
// ErrProviderNotConfigured when neither is set.
func NewProvider(cfg ProviderConfig) (Provider, error) {
	if cfg.OpenAIAPIKey != "" {
		return newOpenAIProvider(cfg), nil
	}
	if cfg.AnthropicAPIKey != "" {
		return newAnthropicProvider(cfg), nil
	}
	return nil, ErrProviderNotConfigured
}
