package memorysearch

import (
	"errors"

	"github.com/blevesearch/bleve/v2/analysis/lang/cjk"
	"github.com/blevesearch/bleve/v2/mapping"
	index "github.com/blevesearch/bleve_index_api"
)

// AnalyzerVersion pins the analyzer configuration the search derivative is
// built against. Bumping it invalidates every prior generation's manifest
// (the manifest records this value at build time) and forces a clean rebuild
// on the next derivative build. The default analyzer is the Bleve `cjk`
// analyzer: its unicode tokenizer segments ASCII on word boundaries while
// emitting width-normalized bigrams for CJK runs, so one mapping covers
// mixed-language Memory content without a per-language fallback.
const AnalyzerVersion = "cjk-v1"

// ChunkerVersion pins the chunker configuration the search derivative is
// built against. Bumping it invalidates every prior generation's manifest
// and forces a clean rebuild. The current chunker is the Goldmark-driven
// Markdown chunker (heading-path-anchored, stable IDs).
const ChunkerVersion = "goldmark-v1"

// Indexed field names. These are part of the on-disk Bleve document schema;
// renaming any of them invalidates every prior generation. The names are kept
// stable and snake_case so they survive JSON round-trips unchanged.
const (
	fieldPath            = "path"
	fieldTitle           = "title"
	fieldHeadingPath     = "heading_path"
	fieldHeadingText     = "heading_text"
	fieldHeadingAnchor   = "heading_anchor"
	fieldBody            = "body"
	fieldKind            = "kind"
	fieldFormat          = "format"
	fieldArchived        = "archived"
	fieldIdentifiers     = "identifiers"
	fieldIdentifierParts = "identifier_parts"
	fieldStartLine       = "start_line"
	fieldEndLine         = "end_line"
	fieldOrdinal         = "ordinal"
	fieldContentHash     = "content_hash"
	fieldSourceRevision  = "source_revision"
)

// BuildIndexMapping returns the Bleve mapping used by every generation of the
// search derivative. The mapping is deterministic: the same AnalyzerVersion
// always produces the same field layout, so a generation built today is
// byte-comparable in shape to one built months ago. Dynamic field mapping is
// disabled so that a future SearchChunk field cannot silently land in the
// index as an unanalyzed stored value (which would leak size or content to
// callers that ask for stored fields).
func BuildIndexMapping() (*mapping.IndexMappingImpl, error) {
	m := mapping.NewIndexMapping()
	m.ScoringModel = index.BM25Scoring
	m.DefaultAnalyzer = cjk.AnalyzerName
	// Disable dynamic mapping. Any field not declared on rootDocument below
	// is dropped at index time. This is the only way to guarantee that no
	// future SearchChunk field accidentally becomes a stored, retrievable
	// field that the response contract did not sign up for.
	m.IndexDynamic = false
	m.StoreDynamic = false
	m.DocValuesDynamic = false

	root := mapping.NewDocumentMapping()
	m.DefaultMapping = root

	// AddFieldMappingsAt(property, fm) is the correct API: it associates the
	// field with a property path so Bleve applies it when indexing
	// map[string]interface{} documents whose keys match the property. The
	// root-level AddFieldMapping, by contrast, attaches the field with no
	// property association and queries against map documents silently
	// return zero hits.
	addField := func(name string, fm *mapping.FieldMapping) {
		root.AddFieldMappingsAt(name, fm)
	}

	// Analyzed text fields. The body is the primary BM25 scoring field
	// but Body.Store is deliberately false: the response contract forbids
	// returning body content from a search hit. Snippets must be regenerated
	// by reopening the source file through a confinement root and
	// re-deriving line bounds from current content. Storing body would let
	// any caller with an index handle read historical body text without the
	// hash-verification gate the contract requires.
	//
	// Term vectors are also disabled on body: phrase queries are not in the
	// V1 field and term vectors would duplicate body bytes into the index.
	bodyField := mapping.NewTextFieldMapping()
	bodyField.Analyzer = cjk.AnalyzerName
	bodyField.Store = false
	bodyField.IncludeTermVectors = false
	bodyField.IncludeInAll = false

	// Title, heading_text, and heading_path are display fields the response
	// returns as locator context, so they remain stored. They are short
	// (heading text only) so storage cost is bounded.
	titleField := mapping.NewTextFieldMapping()
	titleField.Analyzer = cjk.AnalyzerName
	titleField.Store = true
	titleField.IncludeTermVectors = true

	headingTextField := mapping.NewTextFieldMapping()
	headingTextField.Analyzer = cjk.AnalyzerName
	headingTextField.Store = true
	headingTextField.IncludeTermVectors = true

	headingPathField := mapping.NewTextFieldMapping()
	headingPathField.Analyzer = cjk.AnalyzerName
	headingPathField.Store = true
	headingPathField.IncludeTermVectors = true

	// Keyword fields. Path, kind, anchor, and content_hash are locator
	// fields the response contract returns; they stay stored. Format,
	// identifiers, identifier_parts, and source_revision are indexed for
	// search/filter but never returned, so Store is false to keep the
	// response contract honest and the index lean.
	pathField := mapping.NewKeywordFieldMapping()
	kindField := mapping.NewKeywordFieldMapping()
	anchorField := mapping.NewKeywordFieldMapping()
	contentHashField := mapping.NewKeywordFieldMapping()

	formatField := mapping.NewKeywordFieldMapping()
	formatField.Store = false
	formatField.IncludeInAll = false

	identifiersField := mapping.NewKeywordFieldMapping()
	identifiersField.Store = false
	identifiersField.IncludeInAll = false

	identifierPartsField := mapping.NewKeywordFieldMapping()
	identifierPartsField.Store = false
	identifierPartsField.IncludeInAll = false

	sourceRevisionField := mapping.NewKeywordFieldMapping()
	sourceRevisionField.Store = false
	sourceRevisionField.IncludeInAll = false

	// Numeric and boolean fields. start_line / end_line / archived stay
	// stored because the response returns them as locator metadata.
	// ordinal is internal (chunk ordering within a file) and never
	// returned, so Store is false.
	startLineField := mapping.NewNumericFieldMapping()
	endLineField := mapping.NewNumericFieldMapping()
	archivedField := mapping.NewBooleanFieldMapping()

	ordinalField := mapping.NewNumericFieldMapping()
	ordinalField.Store = false
	ordinalField.IncludeInAll = false

	addField(fieldPath, pathField)
	addField(fieldTitle, titleField)
	addField(fieldHeadingPath, headingPathField)
	addField(fieldHeadingText, headingTextField)
	addField(fieldHeadingAnchor, anchorField)
	addField(fieldBody, bodyField)
	addField(fieldKind, kindField)
	addField(fieldFormat, formatField)
	addField(fieldArchived, archivedField)
	addField(fieldIdentifiers, identifiersField)
	addField(fieldIdentifierParts, identifierPartsField)
	addField(fieldStartLine, startLineField)
	addField(fieldEndLine, endLineField)
	addField(fieldOrdinal, ordinalField)
	addField(fieldContentHash, contentHashField)
	addField(fieldSourceRevision, sourceRevisionField)

	return m, nil
}

// ErrMappingConstruction signals that the mapping could not be produced. The
// current BuildIndexMapping never triggers this in practice, but the sentinel
// keeps the API symmetric with callers that already handle a returned error.
var ErrMappingConstruction = errors.New("memory search: mapping construction failed")
