package runtime

import (
	"github.com/tidbcloud/AgentTeam/internal/knowledgeqa/protocol"

	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"
	"unicode"
)

const (
	RuntimeVersion        = "knowledge-qa-v18"
	PromptVersion         = "native-tool-calling-v16"
	ContextSummaryVersion = "knowledge-qa-context-v1"

	accountingCheckpointTTL          = 48 * time.Hour
	accountingCheckpointWriteTimeout = 5 * time.Second
)

const (
	// InferenceModeExploration selects the native repository exploration tools.
	InferenceModeExploration = "exploration"
	// InferenceModeContextSummary selects the isolated conversation-summary tool.
	InferenceModeContextSummary = "context_summary"
)

type InferenceRequest struct {
	OrganizationID  string
	WorkspaceID     string
	RequesterID     string
	TurnID          string
	Model           string
	ReasonLevel     string
	Capability      string
	IdempotencyKey  string
	Mode            string
	System          string
	Messages        []InferenceMessage
	MaxOutputTokens int
	Profile         ChatCompletionsCompatibilityProfile
	// DebugModelOutput asks the transport to retain the complete raw response
	// within the MaaS response-read boundary. It never changes the model request payload.
	DebugModelOutput bool

	WebSearchEnabled bool
}

// InferenceMessage is one OpenAI-compatible Chat Completions message. Assistant
// tool calls and their matching tool results are preserved as structured
// messages; repository data is never converted back into a synthetic user turn.
type InferenceMessage struct {
	Role        string
	Content     string
	ContentNull bool
	// ReasoningContent is present only when the provider returned a string
	// reasoning_content field. Its exact value is replayed before tool results.
	ReasoningContent *string
	ToolCalls        []InferenceToolCall
	ToolCallID       string
}

// InferenceToolCall is one native function call in an assistant message.
type InferenceToolCall struct {
	ID       string
	Type     string
	Function InferenceFunctionCall
}

// InferenceFunctionCall carries a function name and its raw JSON arguments.
type InferenceFunctionCall struct {
	Name      string
	Arguments string
}

type ConversationHistoryTurn struct {
	TurnID     string `json:"turnId"`
	Status     string `json:"status"`
	Question   string `json:"question"`
	Answer     string `json:"answer,omitempty"`
	Provenance string `json:"provenance"`
	Evidence   bool   `json:"evidence"`
}

type RuntimeContext struct {
	Generation             int64                     `json:"generation"`
	Summary                string                    `json:"summary,omitempty"`
	CompactedThroughTurnID string                    `json:"compactedThroughTurnId,omitempty"`
	SummaryVersion         string                    `json:"summaryVersion,omitempty"`
	Turns                  []ConversationHistoryTurn `json:"turns,omitempty"`
}

type RuntimeContextUpdate struct {
	ExpectedGeneration     int64  `json:"expectedGeneration"`
	Summary                string `json:"summary"`
	CompactedThroughTurnID string `json:"compactedThroughTurnId"`
	SummaryVersion         string `json:"summaryVersion"`
	CompactedTurnCount     int    `json:"compactedTurnCount"`
}

type InferenceResult struct {
	Message       InferenceMessage
	Usage         map[string]any
	SettlementRef string
	// RawResponse is populated only when InferenceRequest.DebugModelOutput is
	// true and is never replayed into model history.
	RawResponse string
}

type InferenceClient interface {
	ResolveChatCompletionsProfile(context.Context, string, string) (ChatCompletionsCompatibilityProfile, error)
	Generate(context.Context, InferenceRequest) (InferenceResult, error)
}

type Evidence struct {
	ID       string           `json:"id"`
	Content  string           `json:"content"`
	Citation CitationMetadata `json:"citation"`
}

type GroundedClaim struct {
	Text        string   `json:"text"`
	EvidenceIDs []string `json:"evidenceIds"`
}

// EvidenceReference keeps one attempt-local evidence ID associated with the
// citation metadata needed by AgentTeam. Repository content never crosses this
// boundary, and AgentTeam remains responsible for allocating durable citation IDs.
type EvidenceReference struct {
	ID       string           `json:"id"`
	Citation CitationMetadata `json:"citation"`
}

type GroundedAnswer struct {
	NoAnswer bool            `json:"noAnswer"`
	Claims   []GroundedClaim `json:"claims"`
}

type ExecuteRequest struct {
	Principal        protocol.Principal
	KnowledgeBaseIDs []string
	SourceIDs        []string
	RootHandleIDs    []string
	Question         string
	Model            string
	ReasonLevel      string
	Capability       string
	IdempotencyKey   string
	RuntimeContext   RuntimeContext
	WebSearchEnabled bool
}

type ExecuteResult struct {
	Status             string                      `json:"status"`
	ErrorCode          string                      `json:"errorCode,omitempty"`
	Claims             []GroundedClaim             `json:"claims,omitempty"`
	Evidence           []EvidenceReference         `json:"evidence,omitempty"`
	Diagnostics        []RuntimeDiagnostic         `json:"diagnostics,omitempty"`
	Roots              []RootHandleView            `json:"roots"`
	SearchedRoots      int                         `json:"searchedRoots"`
	Usage              map[string]any              `json:"usage,omitempty"`
	SettlementRef      string                      `json:"settlementRef,omitempty"`
	SettlementRefs     []string                    `json:"settlementRefs,omitempty"`
	RuntimeVersion     string                      `json:"runtimeVersion"`
	PromptVersion      string                      `json:"promptVersion"`
	ToolUsage          ToolUsage                   `json:"toolUsage"`
	RuntimeContext     *RuntimeContextUpdate       `json:"runtimeContext,omitempty"`
	SourceBranchChange *SourceBranchChangeProposal `json:"sourceBranchChange,omitempty"`
	// DebugModelOutput is present only on failed results when the explicit
	// server debug switch is enabled. It may be a structured invalid-response
	// projection containing bounded validation issues and the last raw response.
	DebugModelOutput string `json:"debugModelOutput,omitempty"`
}

// SourceBranchChangeProposal is a non-mutating, user-confirmable action
// derived from an exact branch snapshot opened during the current turn.
type SourceBranchChangeProposal struct {
	KnowledgeBaseID string `json:"knowledgeBaseId"`
	SourceID        string `json:"sourceId"`
	CurrentRef      string `json:"currentRef,omitempty"`
	TargetRef       string `json:"targetRef"`
}

// RuntimeDiagnostic is a bounded, content-free condition discovered while
// rendering an otherwise usable model answer.
type RuntimeDiagnostic struct {
	Code     string `json:"code"`
	Severity string `json:"severity"`
}

// RecoverAccountingRequest identifies one exact attempt under the same owner
// authority that executed it.
type RecoverAccountingRequest struct {
	Principal protocol.Principal
	AttemptID string
}

// RecoverAccountingResult exposes only aggregate usage and settlement lineage;
// no prompt, answer, evidence, repository, or credential data is recoverable.
type RecoverAccountingResult struct {
	Found          bool           `json:"found"`
	Terminal       bool           `json:"terminal,omitempty"`
	Generation     int            `json:"generation,omitempty"`
	Usage          map[string]any `json:"usage,omitempty"`
	SettlementRef  string         `json:"settlementRef,omitempty"`
	SettlementRefs []string       `json:"settlementRefs,omitempty"`
}

type PrepareRequest struct {
	Principal        protocol.Principal
	KnowledgeBaseIDs []string
	SourceIDs        []string
}

type PrepareResult struct {
	Roots            []RootHandleView `json:"roots"`
	ScopeDiagnostics ScopeDiagnostics `json:"scopeDiagnostics"`
	RuntimeVersion   string           `json:"runtimeVersion"`
	PromptVersion    string           `json:"promptVersion"`
}

type ToolUsage struct {
	GrepCalls   int `json:"grepCalls"`
	ListCalls   int `json:"listCalls"`
	ReadCalls   int `json:"readCalls"`
	RefCalls    int `json:"refCalls"`
	ModelCalls  int `json:"modelCalls"`
	ResultBytes int `json:"resultBytes"`

	WebSearchCalls int `json:"webSearchCalls"`
	WebReadCalls   int `json:"webReadCalls"`
	WebSources     int `json:"webSources"`
}

type OpenCitationRequest struct {
	Principal               protocol.Principal
	Citation                CitationMetadata
	AllowRestrictedEvidence bool
}

type Runtime struct {
	Resolver        *Resolver
	Tools           *Service
	Inference       InferenceClient
	Checkpoints     AccountingCheckpointStore
	MaxContextBytes int
	Clock           func() time.Time
	Logger          *slog.Logger
	modelRetryWait  func(context.Context, time.Duration) error
	WebReadClient   *http.Client

	WebSearchProvider WebSearchProvider
	// DebugModelOutput is disabled by default because its projection may contain
	// a raw response with reasoning, tool arguments, or knowledge-derived text.
	DebugModelOutput bool
}

// RecoverAccounting reads one owner-bound, unexpired accounting checkpoint.
// Reads do not delete or extend the checkpoint TTL.
func (runtime *Runtime) RecoverAccounting(ctx context.Context, request RecoverAccountingRequest) (RecoverAccountingResult, error) {
	if runtime == nil || runtime.Checkpoints == nil {
		return RecoverAccountingResult{}, repositoryFailure("Knowledge Q&A accounting recovery is unavailable", nil)
	}
	request.Principal = normalizePrincipal(request.Principal)
	request.AttemptID = strings.TrimSpace(request.AttemptID)
	if err := validatePrincipal(request.Principal); err != nil {
		return RecoverAccountingResult{}, err
	}
	if request.AttemptID == "" {
		return RecoverAccountingResult{}, invalidRequest("Knowledge Q&A accounting attempt is required")
	}
	checkpoint, found, err := runtime.Checkpoints.LoadAccountingCheckpoint(
		ctx, request.Principal, request.AttemptID, runtime.now(),
	)
	if err != nil {
		return RecoverAccountingResult{}, repositoryFailure("Knowledge Q&A accounting checkpoint is unavailable", err)
	}
	if !found {
		return RecoverAccountingResult{Found: false}, nil
	}
	settlementRef := ""
	if len(checkpoint.SettlementRefs) > 0 {
		settlementRef = checkpoint.SettlementRefs[len(checkpoint.SettlementRefs)-1]
	}
	return RecoverAccountingResult{
		Found: true, Terminal: checkpoint.State == protocol.AccountingCheckpointTerminal,
		Generation: checkpoint.Generation, Usage: checkpoint.Usage,
		SettlementRef: settlementRef, SettlementRefs: checkpoint.SettlementRefs,
	}, nil
}

// Prepare resolves current requester authorization and persists immutable,
// opaque root handles without starting retrieval or model inference. The AgentTeam
// worker durably records this result as the turn snapshot before Execute.
func (runtime *Runtime) Prepare(ctx context.Context, request PrepareRequest) (PrepareResult, error) {
	if runtime == nil || runtime.Resolver == nil {
		return PrepareResult{}, repositoryFailure("Knowledge Q&A runtime is unavailable", nil)
	}
	request.Principal = normalizePrincipal(request.Principal)
	if err := validatePrincipal(request.Principal); err != nil {
		return PrepareResult{}, err
	}
	roots, diagnostics, err := runtime.Resolver.ResolveWithDiagnostics(ctx, ScopeRequest{
		Principal: request.Principal, KnowledgeBaseIDs: request.KnowledgeBaseIDs, SourceIDs: request.SourceIDs,
	})
	if err != nil {
		return PrepareResult{}, err
	}
	return PrepareResult{
		Roots: roots, ScopeDiagnostics: diagnostics,
		RuntimeVersion: RuntimeVersion, PromptVersion: PromptVersion,
	}, nil
}

// OpenCitation performs live requester authorization and then reads the exact
// immutable revision recorded on the historical citation. It never reuses an
// expired turn handle and never substitutes the current repository revision.
func (runtime *Runtime) OpenCitation(ctx context.Context, request OpenCitationRequest) (CitationOpenResult, error) {
	if runtime == nil {
		return CitationOpenResult{}, repositoryFailure("Knowledge Q&A citation reader is unavailable", nil)
	}
	request.Principal = normalizePrincipal(request.Principal)
	if err := validatePrincipal(request.Principal); err != nil {
		return CitationOpenResult{}, err
	}
	citationKind := strings.TrimSpace(request.Citation.CitationKind)
	if citationKind == "" {
		citationKind = CitationKindKnowledge
	}
	if citationKind == CitationKindWeb {
		targetURL := strings.TrimSpace(request.Citation.URL)
		parsedURL, err := url.Parse(targetURL)
		if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" {
			return CitationOpenResult{}, invalidRequest("Knowledge web citation URL is invalid")
		}
		return CitationOpenResult{Status: CitationStatusAvailable, TargetURL: targetURL}, nil
	}
	if citationKind != CitationKindKnowledge {
		return CitationOpenResult{}, invalidRequest("Knowledge citation kind is invalid")
	}
	if runtime.Resolver == nil || runtime.Tools == nil || runtime.Tools.Repository == nil {
		return CitationOpenResult{}, repositoryFailure("Knowledge Q&A citation reader is unavailable", nil)
	}
	request.Citation.Revision = strings.TrimSpace(request.Citation.Revision)
	if request.Citation.Revision == "" {
		return CitationOpenResult{}, invalidRequest("Knowledge citation revision is required")
	}
	filePath, err := normalizeRelativePath(request.Citation.Path)
	if err != nil || request.Citation.LineStart < 1 || request.Citation.LineEnd < request.Citation.LineStart ||
		request.Citation.LineEnd-request.Citation.LineStart+1 > maxReadLines {
		return CitationOpenResult{}, invalidRequest("Knowledge citation range is invalid")
	}
	catalog, ok := runtime.Resolver.Catalog.(interface {
		ResolveCitationRoot(context.Context, protocol.Principal, CitationMetadata) (AuthorizedRootCandidate, string, string, error)
	})
	if !ok {
		return CitationOpenResult{}, repositoryFailure("Knowledge Q&A citation authorization is unavailable", nil)
	}
	root, status, openMode, err := catalog.ResolveCitationRoot(ctx, request.Principal, request.Citation)
	if err != nil {
		return CitationOpenResult{}, err
	}
	restrictedEvidence := false
	if status == CitationStatusForbidden && request.AllowRestrictedEvidence {
		sharedCatalog, ok := runtime.Resolver.Catalog.(interface {
			ResolveSharedCitationRoot(context.Context, protocol.Principal, CitationMetadata) (AuthorizedRootCandidate, string, error)
		})
		if !ok {
			return CitationOpenResult{}, repositoryFailure("Knowledge Q&A shared citation authorization is unavailable", nil)
		}
		root, status, err = sharedCatalog.ResolveSharedCitationRoot(ctx, request.Principal, request.Citation)
		if err != nil {
			return CitationOpenResult{}, err
		}
		if status == CitationStatusAvailable {
			openMode = CitationOpenModeEvidence
			restrictedEvidence = true
		}
	}
	if status != CitationStatusAvailable {
		return CitationOpenResult{Status: status}, nil
	}
	var content strings.Builder
	truncated := false
	err = runtime.Tools.Repository.Read(ctx, RepositorySnapshot{
		RepositoryRef: root.RepositoryRef, Revision: request.Citation.Revision,
		WorkspaceID: request.Principal.WorkspaceID, RequesterID: request.Principal.RequesterID, SourceID: root.SourceID,
		RestrictedEvidence: restrictedEvidence,
	}, RepositoryReadRequest{
		Path: filePath, LineStart: request.Citation.LineStart, LineEnd: request.Citation.LineEnd,
	}, func(chunk RepositoryReadChunk) error {
		if chunk.LineStart < request.Citation.LineStart || chunk.LineEnd < chunk.LineStart || chunk.LineEnd > request.Citation.LineEnd {
			return repositoryFailure("Repository primitive returned an invalid citation range", nil)
		}
		remaining := maxReadResultBytes - content.Len()
		if remaining <= 0 {
			truncated = true
			return errStopRepositoryStream
		}
		if len(chunk.Content) > remaining {
			content.Write(chunk.Content[:remaining])
			truncated = true
			return errStopRepositoryStream
		}
		content.Write(chunk.Content)
		return nil
	})
	if errors.Is(err, errStopRepositoryStream) {
		err = nil
	}
	if err != nil {
		if ctx.Err() != nil {
			return CitationOpenResult{}, contextError(ctx)
		}
		var runtimeErr *Error
		if errors.As(err, &runtimeErr) && runtimeErr.Code == CodeResourceExhausted {
			return CitationOpenResult{}, runtimeErr
		}
		return CitationOpenResult{Status: CitationStatusRevisionUnavailable}, nil
	}
	if content.Len() == 0 {
		return CitationOpenResult{Status: CitationStatusRevisionUnavailable}, nil
	}
	return CitationOpenResult{
		Status: CitationStatusAvailable, OpenMode: openMode, Content: content.String(), Path: filePath,
		Revision: request.Citation.Revision, LineStart: request.Citation.LineStart, LineEnd: request.Citation.LineEnd,
		Truncated: truncated,
	}, nil
}

func (runtime *Runtime) Execute(ctx context.Context, request ExecuteRequest) (ExecuteResult, error) {
	if runtime == nil || runtime.Resolver == nil || runtime.Tools == nil || runtime.Inference == nil || runtime.Checkpoints == nil {
		return ExecuteResult{}, repositoryFailure("Knowledge Q&A runtime is unavailable", nil)
	}
	request.Principal = normalizePrincipal(request.Principal)
	if err := validatePrincipal(request.Principal); err != nil {
		return ExecuteResult{}, err
	}
	// Root handles are execution capabilities, not history. Revoke only the
	// handles consumed by this execution: a retry reuses the turn ID and may
	// prepare its own handles while a canceled prior attempt is still unwinding.
	// Completed citations retain their own revision metadata and are reopened
	// under live authority.
	cleanupHandleIDs := normalizeUniqueStrings(request.RootHandleIDs)
	defer func() {
		if runtime.Tools.Handles == nil || len(cleanupHandleIDs) == 0 {
			return
		}
		cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = runtime.Tools.Handles.DeleteRootHandles(cleanupCtx, request.Principal, cleanupHandleIDs)
	}()
	request.Question = strings.TrimSpace(request.Question)
	request.Model = strings.TrimSpace(request.Model)
	request.ReasonLevel = strings.TrimSpace(request.ReasonLevel)
	request.Capability = strings.TrimSpace(request.Capability)
	request.IdempotencyKey = strings.TrimSpace(request.IdempotencyKey)
	if request.Question == "" || request.Model == "" || request.ReasonLevel == "" ||
		request.Capability == "" || request.IdempotencyKey == "" {
		return ExecuteResult{}, invalidRequest("Knowledge Q&A execution request is incomplete")
	}
	checkpointExpiresAt := runtime.now().Add(accountingCheckpointTTL)
	accounting := &modelCallAccounting{
		runtime: runtime, request: request, expiresAt: checkpointExpiresAt,
	}
	if err := runtime.putAccountingCheckpoint(request, 0, protocol.AccountingCheckpointRunning, nil, nil, checkpointExpiresAt); err != nil {
		return ExecuteResult{}, repositoryFailure("Knowledge Q&A accounting checkpoint could not be started", err)
	}
	defer func() {
		_ = runtime.putAccountingCheckpoint(
			request, accounting.generation, protocol.AccountingCheckpointTerminal,
			accounting.usage, accounting.settlementRefs, checkpointExpiresAt,
		)
	}()
	runtimeContext, err := normalizeRuntimeContext(request.RuntimeContext)
	if err != nil {
		return ExecuteResult{}, err
	}

	repository, roots, err := runtime.authorizeExecutionRoots(ctx, request)
	if len(cleanupHandleIDs) == 0 {
		cleanupHandleIDs = rootHandleIDs(roots)
	}
	if err != nil {
		return ExecuteResult{}, err
	}
	result := ExecuteResult{
		Roots: roots, SearchedRoots: len(roots),
		RuntimeVersion: RuntimeVersion, PromptVersion: PromptVersion,
	}
	if len(roots) == 0 {
		result.Status = "no_answer"
		return result, nil
	}
	profile, err := runtime.Inference.ResolveChatCompletionsProfile(ctx, request.Model, request.Capability)
	if err != nil {
		return ExecuteResult{}, err
	}
	if err := validateChatCompletionsProfile(profile); err != nil {
		return ExecuteResult{}, &Error{
			Code:    "knowledge_qa.compatibility_profile_unavailable",
			Message: "Knowledge Q&A model compatibility is unavailable",
			Cause:   err,
		}
	}
	accounting.profile = cloneChatCompletionsProfile(profile)

	outcome, runtimeContextUpdate, err := runtime.runExplorationWithContext(
		ctx, request, runtimeContext, roots, repository, accounting,
	)
	result.RuntimeContext = runtimeContextUpdate
	return renderExplorationResult(result, outcome, err)
}

func rootHandleIDs(roots []RootHandleView) []string {
	result := make([]string, 0, len(roots))
	for _, root := range roots {
		if handleID := strings.TrimSpace(root.HandleID); handleID != "" {
			result = append(result, handleID)
		}
	}
	return result
}

func (runtime *Runtime) authorizeExecutionRoots(
	ctx context.Context,
	request ExecuteRequest,
) (*ExecutionRepository, []RootHandleView, error) {
	if len(normalizeUniqueStrings(request.RootHandleIDs)) > 0 {
		return runtime.Tools.AuthorizeExecution(ctx, request.Principal, request.RootHandleIDs)
	}
	// Keep the in-process API usable for callers that do not need the
	// cross-service durability boundary. The hosted AgentTeam worker always
	// prepares first and supplies RootHandleIDs.
	roots, err := runtime.Resolver.Resolve(ctx, ScopeRequest{
		Principal: request.Principal, KnowledgeBaseIDs: request.KnowledgeBaseIDs, SourceIDs: request.SourceIDs,
	})
	if err != nil || len(roots) == 0 {
		return nil, roots, err
	}
	handleIDs := make([]string, 0, len(roots))
	for _, root := range roots {
		handleIDs = append(handleIDs, root.HandleID)
	}
	repository, authorizedRoots, err := runtime.Tools.AuthorizeExecution(ctx, request.Principal, handleIDs)
	if err != nil {
		// Preserve the freshly minted views so Execute can revoke their exact
		// handles even when execution-session construction fails.
		return nil, roots, err
	}
	return repository, authorizedRoots, nil
}

func renderExplorationResult(result ExecuteResult, outcome explorationOutcome, explorationErr error) (ExecuteResult, error) {
	if explorationErr != nil {
		if len(outcome.Usage) > 0 || strings.TrimSpace(outcome.SettlementRef) != "" ||
			strings.TrimSpace(outcome.DebugModelOutput) != "" {
			result.Status = "failed"
			result.ErrorCode = explorationTerminalErrorCode(explorationErr)
			result.ToolUsage = outcome.ToolUsage
			result.Usage = outcome.Usage
			result.SettlementRef = outcome.SettlementRef
			result.SettlementRefs = outcome.SettlementRefs
			result.DebugModelOutput = outcome.DebugModelOutput
			return result, nil
		}
		return ExecuteResult{}, explorationErr
	}
	result.ToolUsage = outcome.ToolUsage
	result.Usage = outcome.Usage
	result.SettlementRef = outcome.SettlementRef
	result.SettlementRefs = outcome.SettlementRefs
	if outcome.ErrorCode != "" {
		result.Status = "failed"
		result.ErrorCode = outcome.ErrorCode
		result.DebugModelOutput = outcome.DebugModelOutput
		return result, nil
	}
	answer, err := normalizeGroundedAnswer(outcome.Answer)
	if err != nil {
		result.Status = "failed"
		result.ErrorCode = CodeResponseInvalid
		result.DebugModelOutput = outcome.DebugModelOutput
		return result, nil
	}
	if answer.NoAnswer {
		result.Status = "no_answer"
		return result, nil
	}
	evidence, diagnostics := diagnoseGroundedClaims(answer, outcome.Evidence)
	result.Status = "completed"
	result.Claims = make([]GroundedClaim, 0, len(answer.Claims))
	for _, claim := range answer.Claims {
		claim.Text = strings.TrimSpace(claim.Text)
		claim.EvidenceIDs = normalizeEvidenceIDs(claim.EvidenceIDs)
		if strings.TrimSpace(evidenceMarkerPattern.ReplaceAllString(claim.Text, "")) == "" {
			continue
		}
		result.Claims = append(result.Claims, claim)
	}
	if len(result.Claims) == 0 {
		result.Status = "failed"
		result.ErrorCode = CodeResponseInvalid
		result.Claims = nil
		return result, nil
	}
	result.Evidence = evidence
	result.Diagnostics = diagnostics
	result.SourceBranchChange = outcome.SourceBranchChange
	return result, nil
}

func (runtime *Runtime) putAccountingCheckpoint(
	request ExecuteRequest,
	generation int,
	state string,
	usage map[string]any,
	settlementRefs []string,
	expiresAt time.Time,
) error {
	checkpointCtx, cancel := context.WithTimeout(context.Background(), accountingCheckpointWriteTimeout)
	defer cancel()
	err := runtime.Checkpoints.PutAccountingCheckpoint(checkpointCtx, protocol.AccountingCheckpoint{
		Principal: request.Principal, AttemptID: request.IdempotencyKey,
		Generation: generation, State: state, Usage: usage,
		SettlementRefs: settlementRefs, ExpiresAt: expiresAt, UpdatedAt: runtime.now(),
	})
	if err == nil {
		return nil
	}
	logger := runtime.Logger
	if logger == nil {
		logger = slog.Default()
	}
	logger.ErrorContext(
		checkpointCtx,
		"Knowledge Q&A accounting checkpoint write failed",
		"turn_id", request.Principal.TurnID,
		"attempt_id", request.IdempotencyKey,
		"generation", generation,
		"state", state,
		"error", err,
	)
	return err
}

func (runtime *Runtime) now() time.Time {
	if runtime != nil && runtime.Clock != nil {
		return runtime.Clock().UTC()
	}
	return time.Now().UTC()
}

func explorationTerminalErrorCode(err error) string {
	var runtimeErr *Error
	if errors.As(err, &runtimeErr) && strings.TrimSpace(runtimeErr.Code) != "" {
		return strings.TrimSpace(runtimeErr.Code)
	}
	return "knowledge_qa.inference_failed"
}

func normalizeRuntimeContext(input RuntimeContext) (RuntimeContext, error) {
	if input.Generation < 0 {
		return RuntimeContext{}, invalidRequest("Knowledge Q&A runtime context generation is invalid")
	}
	input.Summary = strings.TrimSpace(input.Summary)
	input.CompactedThroughTurnID = strings.TrimSpace(input.CompactedThroughTurnID)
	input.SummaryVersion = strings.TrimSpace(input.SummaryVersion)
	if input.Summary != "" || input.CompactedThroughTurnID != "" || input.SummaryVersion != "" {
		if input.Generation == 0 || input.Summary == "" || input.CompactedThroughTurnID == "" || input.SummaryVersion != ContextSummaryVersion {
			return RuntimeContext{}, invalidRequest("Knowledge Q&A runtime context summary state is invalid")
		}
	}
	result := make([]ConversationHistoryTurn, 0, len(input.Turns))
	seen := make(map[string]struct{}, len(input.Turns))
	for _, item := range input.Turns {
		item.TurnID = strings.TrimSpace(item.TurnID)
		item.Status = strings.TrimSpace(item.Status)
		item.Question = strings.TrimSpace(item.Question)
		item.Answer = strings.TrimSpace(item.Answer)
		item.Provenance = strings.TrimSpace(item.Provenance)
		if item.TurnID == "" || item.Question == "" || item.Provenance != "conversation_history" || item.Evidence {
			return RuntimeContext{}, invalidRequest("Knowledge Q&A conversation history provenance is invalid")
		}
		switch item.Status {
		case "completed":
		case "no_answer", "failed", "canceled":
			if item.Answer != "" {
				return RuntimeContext{}, invalidRequest("Knowledge Q&A conversation terminal status is invalid")
			}
		default:
			return RuntimeContext{}, invalidRequest("Knowledge Q&A conversation terminal status is invalid")
		}
		if _, duplicate := seen[item.TurnID]; duplicate {
			return RuntimeContext{}, invalidRequest("Knowledge Q&A conversation history contains duplicate turns")
		}
		seen[item.TurnID] = struct{}{}
		result = append(result, item)
	}
	input.Turns = result
	return input, nil
}

func containsHan(value string) bool {
	for _, item := range value {
		if unicode.Is(unicode.Han, item) {
			return true
		}
	}
	return false
}

func normalizeGroundedAnswer(answer GroundedAnswer) (GroundedAnswer, error) {
	if answer.NoAnswer {
		if len(answer.Claims) != 0 {
			return GroundedAnswer{}, fmt.Errorf("no-answer response contains unsupported claims")
		}
		return answer, nil
	}
	if len(answer.Claims) == 0 {
		return GroundedAnswer{}, fmt.Errorf("grounded claims are required")
	}
	return answer, nil
}

func diagnoseGroundedClaims(answer GroundedAnswer, evidence []Evidence) ([]EvidenceReference, []RuntimeDiagnostic) {
	byID := make(map[string]CitationMetadata, len(evidence))
	for _, item := range evidence {
		byID[strings.TrimSpace(item.ID)] = item.Citation
	}
	references := make([]EvidenceReference, 0)
	seenEvidence := make(map[string]struct{})
	diagnostics := make([]RuntimeDiagnostic, 0, 3)
	seenDiagnostics := make(map[string]struct{}, 3)
	addDiagnostic := func(code string) {
		if _, exists := seenDiagnostics[code]; exists {
			return
		}
		seenDiagnostics[code] = struct{}{}
		diagnostics = append(diagnostics, RuntimeDiagnostic{Code: code, Severity: "warning"})
	}
	for _, claim := range answer.Claims {
		claim.Text = strings.TrimSpace(claim.Text)
		visibleText := strings.TrimSpace(evidenceMarkerPattern.ReplaceAllString(claim.Text, ""))
		if visibleText == "" {
			addDiagnostic(DiagnosticClaimEmpty)
		}
		evidenceIDs := normalizeEvidenceIDs(claim.EvidenceIDs)
		declaredEvidence := make(map[string]struct{}, len(evidenceIDs))
		for _, evidenceID := range evidenceIDs {
			declaredEvidence[evidenceID] = struct{}{}
		}
		markedEvidence := make(map[string]struct{})
		for _, match := range evidenceMarkerPattern.FindAllStringSubmatch(claim.Text, -1) {
			evidenceID := match[1]
			markedEvidence[evidenceID] = struct{}{}
			if _, declared := declaredEvidence[evidenceID]; !declared {
				addDiagnostic(DiagnosticMarkerMismatch)
			}
			if _, available := byID[evidenceID]; !available {
				addDiagnostic(DiagnosticEvidenceUnavailable)
			}
		}
		for _, evidenceID := range evidenceIDs {
			if _, marked := markedEvidence[evidenceID]; !marked {
				addDiagnostic(DiagnosticMarkerMismatch)
			}
			citation, ok := byID[evidenceID]
			if !ok {
				addDiagnostic(DiagnosticEvidenceUnavailable)
				continue
			}
			if _, exists := seenEvidence[evidenceID]; exists {
				continue
			}
			seenEvidence[evidenceID] = struct{}{}
			references = append(references, EvidenceReference{ID: evidenceID, Citation: citation})
		}
	}
	return references, diagnostics
}

func normalizeEvidenceIDs(input []string) []string {
	result := make([]string, 0, len(input))
	seen := make(map[string]struct{}, len(input))
	for _, evidenceID := range input {
		evidenceID = strings.TrimSpace(evidenceID)
		if !evidenceIDPattern.MatchString(evidenceID) {
			continue
		}
		if _, exists := seen[evidenceID]; exists {
			continue
		}
		seen[evidenceID] = struct{}{}
		result = append(result, evidenceID)
	}
	return result
}

var (
	evidenceIDPattern     = regexp.MustCompile(`^E[1-9][0-9]*$`)
	evidenceMarkerPattern = regexp.MustCompile(`\[(E[1-9][0-9]*)\]`)
)
