// Package sourcerelatedness reports advisory findings when scene prose has no lexical grounding.
package sourcerelatedness

import (
	"context"
	"fmt"
	"unicode/utf8"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/scenedescriptions/shape"
)

const (
	Key        = "extract/dnd/scene-descriptions/source_relatedness"
	ReasonCode = "scene_description_not_near_source"
	policy     = "dnd.scene_descriptions.validator.source_relatedness.v1"
)

var stopwords = map[string]struct{}{
	"a": {}, "an": {}, "and": {}, "are": {}, "as": {}, "at": {}, "be": {}, "but": {}, "by": {}, "for": {}, "from": {},
	"had": {}, "has": {}, "have": {}, "he": {}, "her": {}, "him": {}, "his": {}, "in": {}, "into": {}, "is": {}, "it": {}, "its": {},
	"of": {}, "on": {}, "or": {}, "she": {}, "that": {}, "the": {}, "their": {}, "them": {}, "they": {}, "this": {}, "to": {},
	"was": {}, "were": {}, "with": {},
}

type Options struct{}
type Validator struct{}

var _ contracts.TypedValidator[dnd.SceneDescriptionList] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)

func New(Options) *Validator      { return &Validator{} }
func (v *Validator) Name() string { return Key }
func (v *Validator) ExecutionClass() contracts.ExecutionClass {
	return contracts.ExecutionClassDeterministic
}
func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
	return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
}

func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.SceneDescriptionList]) (contracts.ValidationResult, error) {
	if shape.Validate(req.Value) != nil {
		return contracts.ValidationResult{Approved: true}, nil
	}
	resolver, err := shared.NewCitationResolver(req.Source)
	if err != nil {
		return contracts.ValidationResult{Approved: true}, nil
	}
	findings := make([]diagnostics.Finding, 0)
	for index, scene := range req.Value.Scenes {
		citedText, err := resolver.CitedText([]source.SourceRef{scene.SourceRef})
		if err != nil {
			return contracts.ValidationResult{Approved: true}, nil
		}
		citedTokens := tokenSet(citedText)
		if !hasGroundedToken(citedTokens, scene.Title) {
			findings = append(findings, finding(index, "title"))
		}
		if !hasGroundedToken(citedTokens, scene.Summary) {
			findings = append(findings, finding(index, "summary"))
		}
	}
	return diagnostics.DataQualityResult(findings)
}

func tokenSet(value string) map[string]struct{} {
	tokens := make(map[string]struct{})
	for _, token := range shared.NormalizedTokens(value) {
		if significant(token) {
			tokens[token] = struct{}{}
		}
	}
	return tokens
}

func hasGroundedToken(cited map[string]struct{}, value string) bool {
	for _, token := range shared.NormalizedTokens(value) {
		if !significant(token) {
			continue
		}
		if _, ok := cited[token]; ok {
			return true
		}
	}
	return false
}

func significant(token string) bool {
	if utf8.RuneCountInString(token) < 3 {
		return false
	}
	_, ignored := stopwords[token]
	return !ignored
}

func finding(index int, field string) diagnostics.Finding {
	return diagnostics.Finding{
		Scope:      fmt.Sprintf("scenes[%d].%s", index, field),
		ReasonCode: ReasonCode,
		Message:    "scene description " + field + " has no significant token in cited source text",
	}
}

func Spec() pipeline.ValidatorSpec {
	return pipeline.ValidatorSpec{Key: Key, ExecutionClass: contracts.ExecutionClassDeterministic}
}

func Register(registry *pipeline.ValidatorRegistry) error {
	return pipeline.RegisterTypedValidatorBuilder(registry, dnd.SceneDescriptionListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.SceneDescriptionList], error) {
		options, err := DecodeOptions(request.Options)
		if err != nil {
			return nil, err
		}
		return New(options), nil
	})
}

func DecodeOptions(options map[string]any) (Options, error) {
	if err := pipeline.RejectUnknownOptions(options); err != nil {
		return Options{}, err
	}
	return Options{}, nil
}

func validateOptions(options map[string]any) error { _, err := DecodeOptions(options); return err }
