package runtime

import (
	"encoding/json"
	"errors"
)

// explorationRepairIssue is the complete model-visible validation projection.
// Every string comes from the closed templates below; model-authored arguments,
// parser errors, handles, paths, and reference values are never echoed.
type explorationRepairIssue struct {
	CallIndex   int    `json:"callIndex,omitempty"`
	Tool        string `json:"tool,omitempty"`
	Code        string `json:"code"`
	Field       string `json:"field,omitempty"`
	Requirement string `json:"requirement"`
}

// These value templates are the closed validator vocabulary. Keep their
// fields static: callers may add only a bounded call index and an allowed tool.
var (
	repairIssueResponseContractInvalid   = explorationRepairIssue{Code: "response_contract_invalid", Requirement: "response must satisfy the native function-call contract"}
	repairIssueArgumentsSchemaInvalid    = explorationRepairIssue{Code: "arguments_schema_invalid", Requirement: "arguments must match the tool schema exactly"}
	repairIssueSearchQueryRequired       = explorationRepairIssue{Code: "search_query_required", Field: "query", Requirement: "query is required"}
	repairIssueSearchQueryInvalid        = explorationRepairIssue{Code: "search_query_invalid", Field: "query", Requirement: "query must be a non-empty string within the declared size limit"}
	repairIssueSearchQueryPattern        = explorationRepairIssue{Code: "search_query_pattern_invalid", Field: "query", Requirement: "query must be a valid POSIX extended regular expression"}
	repairIssueSearchRootsInvalid        = explorationRepairIssue{Code: "search_root_aliases_invalid", Field: "rootAliases", Requirement: "rootAliases, when present, must be a non-empty unique array of issued root aliases"}
	repairIssueSearchGlobsInvalid        = explorationRepairIssue{Code: "search_path_globs_invalid", Field: "pathGlobs", Requirement: "pathGlobs, when present, must be a non-empty unique array of valid relative glob strings"}
	repairIssueListSelectorRequired      = explorationRepairIssue{Code: "list_selector_required", Requirement: "provide rootAlias or directoryRef"}
	repairIssueListRootInvalid           = explorationRepairIssue{Code: "list_root_alias_invalid", Field: "rootAlias", Requirement: "rootAlias must be a valid issued root alias and cannot be null"}
	repairIssueListDirectoryInvalid      = explorationRepairIssue{Code: "list_directory_ref_invalid", Field: "directoryRef", Requirement: "directoryRef must be a valid issued directory reference and cannot be null"}
	repairIssueReadFileRequired          = explorationRepairIssue{Code: "read_file_ref_required", Field: "fileRef", Requirement: "fileRef is required"}
	repairIssueReadStartRequired         = explorationRepairIssue{Code: "read_line_start_required", Field: "lineStart", Requirement: "lineStart is required"}
	repairIssueReadEndRequired           = explorationRepairIssue{Code: "read_line_end_required", Field: "lineEnd", Requirement: "lineEnd is required"}
	repairIssueReadFileInvalid           = explorationRepairIssue{Code: "read_file_ref_invalid", Field: "fileRef", Requirement: "fileRef must be a valid issued file reference"}
	repairIssueReadStartInvalid          = explorationRepairIssue{Code: "read_line_start_invalid", Field: "lineStart", Requirement: "lineStart must be an integer greater than or equal to 1"}
	repairIssueReadEndInvalid            = explorationRepairIssue{Code: "read_line_end_invalid", Field: "lineEnd", Requirement: "lineEnd must be an integer greater than or equal to 1"}
	repairIssueReadRangeReversed         = explorationRepairIssue{Code: "read_range_reversed", Field: "lineEnd", Requirement: "lineEnd must be greater than or equal to lineStart"}
	repairIssueReadRangeTooLarge         = explorationRepairIssue{Code: "read_range_too_large", Field: "lineEnd", Requirement: "the requested range must contain at most 1000 lines"}
	repairIssueRefRootRequired           = explorationRepairIssue{Code: "ref_root_alias_required", Field: "rootAlias", Requirement: "rootAlias is required"}
	repairIssueRefRootInvalid            = explorationRepairIssue{Code: "ref_root_alias_invalid", Field: "rootAlias", Requirement: "rootAlias must be a valid issued primary Source root alias"}
	repairIssueRefNameRequired           = explorationRepairIssue{Code: "ref_name_required", Field: "ref", Requirement: "ref is required"}
	repairIssueRefNameInvalid            = explorationRepairIssue{Code: "ref_name_invalid", Field: "ref", Requirement: "ref must be a non-empty string within the declared size limit"}
	repairIssueRefTypeRequired           = explorationRepairIssue{Code: "ref_type_required", Field: "refType", Requirement: "refType is required"}
	repairIssueRefTypeInvalid            = explorationRepairIssue{Code: "ref_type_invalid", Field: "refType", Requirement: "refType must be branch or tag"}
	repairIssueAnswerClaimsRequired      = explorationRepairIssue{Code: "answer_claims_required", Field: "claims", Requirement: "claims must be a non-empty array of strings"}
	repairIssueAnswerClaimEmpty          = explorationRepairIssue{Code: "answer_claim_empty", Field: "claims", Requirement: "every claim must contain non-whitespace text"}
	repairIssueAnswerClaimCitationOnly   = explorationRepairIssue{Code: "answer_claim_citation_only", Field: "claims", Requirement: "every evidence marker must immediately follow an assertion; a claim cannot contain only evidence markers"}
	repairIssueAnswerBranchChangeInvalid = explorationRepairIssue{Code: "answer_branch_change_invalid", Field: "sourceBranchChange", Requirement: "sourceBranchChange must contain exactly one issued branchRootAlias"}
	repairIssueNoAnswerArguments         = explorationRepairIssue{Code: "no_answer_arguments_not_empty", Requirement: "knowledge_no_answer arguments must be an empty object"}
	repairIssueTerminalNotExclusive      = explorationRepairIssue{Code: "terminal_call_not_exclusive", Requirement: "knowledge_answer or knowledge_no_answer must be the sole call in the response"}
	repairIssueReferenceUnavailable      = explorationRepairIssue{Code: "reference_not_available", Requirement: "use only root, file, and directory references issued in the current exploration"}
)

type explorationRepairFeedback struct {
	RepairAttempt     int                      `json:"repairAttempt"`
	MaxRepairAttempts int                      `json:"maxRepairAttempts"`
	Issues            []explorationRepairIssue `json:"issues"`
}

type explorationDebugInvalidResponse struct {
	ResponseNumber int                      `json:"responseNumber"`
	Issues         []explorationRepairIssue `json:"issues"`
}

type explorationDebugOutput struct {
	InvalidResponses []explorationDebugInvalidResponse `json:"invalidResponses"`
}

const explorationDebugModelOutputSeparator = "\n\nLAST_MODEL_OUTPUT:\n"

type explorationToolCallValidationError struct {
	issue explorationRepairIssue
}

func (validationErr *explorationToolCallValidationError) Error() string {
	return "Knowledge Q&A function tool call arguments are invalid"
}

func explorationToolValidationError(tool string, issue explorationRepairIssue) error {
	issue.Tool = validRepairToolName(tool)
	return &explorationToolCallValidationError{issue: issue}
}

func explorationRepairIssueFromError(callIndex int, err error) explorationRepairIssue {
	var validationErr *explorationToolCallValidationError
	if errors.As(err, &validationErr) {
		issue := validationErr.issue
		issue.CallIndex = callIndex
		return issue
	}
	issue := repairIssueResponseContractInvalid
	issue.CallIndex = callIndex
	return issue
}

func explorationReferenceRepairIssue(callIndex int, tool string) explorationRepairIssue {
	issue := repairIssueReferenceUnavailable
	issue.CallIndex = callIndex
	issue.Tool = validRepairToolName(tool)
	return issue
}

func validRepairToolName(name string) string {
	switch name {
	case knowledgeSearchFunction, knowledgeListFunction, knowledgeReadFunction,
		knowledgeRefFunction, knowledgeAnswerFunction, knowledgeNoAnswerFunction:
		return name
	default:
		return ""
	}
}

func explorationInvalidResponseRepairPrompt(feedback explorationRepairFeedback) string {
	payload, _ := json.Marshal(feedback)
	return explorationInvalidResponseRepairSuffix + " Trusted validator feedback: " + string(payload)
}

func renderExplorationDebugOutput(
	invalidResponses []explorationDebugInvalidResponse,
	lastModelOutput string,
) string {
	if len(invalidResponses) == 0 {
		return lastModelOutput
	}
	payload, _ := json.Marshal(explorationDebugOutput{InvalidResponses: invalidResponses})
	if lastModelOutput == "" {
		return string(payload)
	}
	// Keep the raw response outside the JSON diagnostic prefix. This avoids a
	// second quoting expansion before ExecuteResult itself is encoded as JSON.
	return string(payload) + explorationDebugModelOutputSeparator + lastModelOutput
}

func (accounting *modelCallAccounting) rejectInvalidExplorationResponse(
	issues []explorationRepairIssue,
) bool {
	if len(issues) == 0 {
		issues = []explorationRepairIssue{repairIssueResponseContractInvalid}
	}
	accounting.invalidResponseCount++
	if accounting.runtime.DebugModelOutput {
		accounting.debugInvalidResponses = append(
			accounting.debugInvalidResponses,
			explorationDebugInvalidResponse{
				ResponseNumber: accounting.invalidResponseCount,
				Issues:         append([]explorationRepairIssue(nil), issues...),
			},
		)
	}
	if accounting.invalidResponseRepairs >= maxInvalidResponseRepairs {
		return false
	}
	accounting.invalidResponseRepairs++
	accounting.pendingInvalidResponseRepair = &explorationRepairFeedback{
		RepairAttempt:     accounting.invalidResponseRepairs,
		MaxRepairAttempts: maxInvalidResponseRepairs,
		Issues:            append([]explorationRepairIssue(nil), issues...),
	}
	return true
}

func (accounting *modelCallAccounting) invalidResponseRepairPrompt() string {
	if accounting.pendingInvalidResponseRepair == nil {
		return ""
	}
	return explorationInvalidResponseRepairPrompt(*accounting.pendingInvalidResponseRepair)
}

func (accounting *modelCallAccounting) clearInvalidResponseRepair() {
	accounting.pendingInvalidResponseRepair = nil
}
