// Package nomen renders human- and machine-facing names from a reusable
// composition model (Latin nomen, "name"). It is the engine extracted from
// the one-off naming functions that shipped for submission draft titles
// (m/paliad#155) and exported .docx filenames (t-paliad-354); see
// docs/plans/prd-filename-generator-2026-06-01.md.
//
// The package is pure: no DB, no HTTP, no filesystem, and no dependency on
// the rest of paliad. A consumer supplies a Composition (the template), a
// VarResolver (the values for this render), and a RenderTarget (the output
// policy — a human title vs a sanitised filename). The same Composition
// renders to different targets.
//
// # Separator semantics (trailing, not leading)
//
// Each Segment carries a Sep that is the separator emitted AFTER it, and
// only when a later segment also emits. So the separator between two
// consecutive emitted segments is owned by the LEFT segment. This is what
// lets a composition stay byte-faithful when a middle segment drops out:
// the draft-title scheme joins the date to the party trio with a space and
// the parties to each other with " ./. ", and when the client is absent the
// date must still join the forum with a space — which only works if the
// space is the date's trailing separator, independent of which identity
// segment happens to come next. A leading-separator model can't express
// that (the same segment would need two different leading separators
// depending on what was omitted before it).
package nomen

import "strings"

// Version is the current Composition schema version. Stored compositions
// (firm/user overrides, once those land) carry it so a future change can be
// detected and migrated; the seed system defaults always use this value.
const Version = 1

// MaxSegments is a sanity cap on how many segments a single composition may
// have. The wired artifacts use 3–4; the cap exists so a stored override
// can't smuggle an unbounded blob through Validate.
const MaxSegments = 16

// MissingKind selects what a segment contributes when its variable resolves
// empty or unavailable.
type MissingKind int

const (
	// KindOmit drops the segment entirely (and suppresses its trailing
	// separator). Generalises the #155 "drop empty segment with its
	// separator" rule.
	KindOmit MissingKind = iota
	// KindPlaceholder substitutes a stand-in value for missing data, e.g.
	// "(Az. folgt)" for an as-yet-unknown case number (t-paliad-354).
	KindPlaceholder
	// KindLiteral substitutes a fixed label. Functionally identical to a
	// placeholder today, but kept distinct so the settings UI can word them
	// differently ("fixed label" vs "stand-in for missing data") and so
	// future policy can diverge.
	KindLiteral
)

// MissingRule is a segment's missing-value policy. Value is ignored for
// KindOmit.
type MissingRule struct {
	Kind  MissingKind
	Value string
}

// Omit returns a drop-when-empty rule.
func Omit() MissingRule { return MissingRule{Kind: KindOmit} }

// Placeholder returns a substitute-when-empty rule for missing data.
func Placeholder(v string) MissingRule { return MissingRule{Kind: KindPlaceholder, Value: v} }

// Literal returns a substitute-when-empty rule for a fixed label.
func Literal(v string) MissingRule { return MissingRule{Kind: KindLiteral, Value: v} }

// Segment is one piece of a composition.
type Segment struct {
	// Var is the variable key resolved against the catalog/resolver.
	Var string
	// Sep is the trailing separator: emitted AFTER this segment iff a later
	// segment also emits. The last emitted segment's Sep is never used.
	Sep string
	// Wrap surrounds the resolved value with fixed literals, e.g.
	// {"(", ")"} for a bracketed case number. The wrap is part of the frame:
	// it is NOT passed through the target's value sanitiser.
	Wrap [2]string
	// Missing is the policy applied when Var resolves empty/unavailable.
	Missing MissingRule
}

// Composition is the canonical, validated name template: an ordered list of
// segments plus a schema version.
type Composition struct {
	Version  int
	Segments []Segment
}

// VarResolver yields a variable's value for one render. It returns
// (value, true) when the variable is available (even if the consumer wants
// to force it empty by returning ("", true) — though the engine treats a
// blank value as absent regardless), and ("", false) when the variable is
// unavailable in this context, in which case the segment's MissingRule
// applies.
type VarResolver func(key string) (value string, ok bool)

// RenderTarget post-processes a render. SanitiseValue runs per resolved
// variable value (before wrapping/assembly); Finalise runs once on the
// fully-assembled string (e.g. to append an extension).
type RenderTarget interface {
	Name() string
	SanitiseValue(v string) string
	Finalise(assembled string) string
}

// Render assembles the name. For each segment in order it resolves the
// value (applying the MissingRule when empty), sanitises the value via the
// target, wraps it, and joins it to the previous emitted segment using that
// previous segment's trailing Sep. The assembled string is passed once
// through Finalise.
func (c Composition) Render(resolve VarResolver, target RenderTarget) string {
	var b strings.Builder
	var pendingSep string
	emitted := false
	for _, seg := range c.Segments {
		val, ok := effectiveValue(seg, resolve)
		if !ok {
			continue
		}
		val = target.SanitiseValue(val)
		piece := seg.Wrap[0] + val + seg.Wrap[1]
		if emitted {
			b.WriteString(pendingSep)
		}
		b.WriteString(piece)
		pendingSep = seg.Sep
		emitted = true
	}
	return target.Finalise(b.String())
}

// effectiveValue resolves a segment to its emitted value, applying the
// MissingRule. The second return is false when the segment contributes
// nothing (omit, or a placeholder/literal whose value is itself blank).
// A resolved value is trimmed; a blank resolved value is treated as absent.
func effectiveValue(seg Segment, resolve VarResolver) (string, bool) {
	val, ok := resolve(seg.Var)
	val = strings.TrimSpace(val)
	if ok && val != "" {
		return val, true
	}
	switch seg.Missing.Kind {
	case KindPlaceholder, KindLiteral:
		v := strings.TrimSpace(seg.Missing.Value)
		if v == "" {
			return "", false
		}
		return v, true
	default: // KindOmit
		return "", false
	}
}

// VarDef is a variable's catalog metadata: it drives write-time validation
// and the settings palette. Values come from the per-render VarResolver, not
// from here — the catalog is metadata only.
type VarDef struct {
	Key         string
	Label       string // DE primary
	LabelEN     string
	Description string
	Group       string
}

// VarCatalog is the set of variables available to an artifact, keyed by Var.
type VarCatalog map[string]VarDef

// Validate enforces the structural invariants on a composition against the
// catalog of an artifact. Used on writes (stored firm/user overrides). The
// seed system defaults are validated by a unit test so a typo can't ship.
func (c Composition) Validate(catalog VarCatalog) error {
	if c.Version != Version {
		return &ValidationError{Msg: "unsupported composition version"}
	}
	if len(c.Segments) > MaxSegments {
		return &ValidationError{Msg: "too many segments"}
	}
	for _, seg := range c.Segments {
		if strings.TrimSpace(seg.Var) == "" {
			return &ValidationError{Msg: "segment has empty variable"}
		}
		if _, ok := catalog[seg.Var]; !ok {
			return &ValidationError{Msg: "unknown variable: " + seg.Var}
		}
	}
	return nil
}

// ValidationError is returned by Composition.Validate. It is a distinct type
// so consumers can map it to a 400 without string-matching.
type ValidationError struct{ Msg string }

func (e *ValidationError) Error() string { return "nomen: " + e.Msg }

// FuncTarget is the general RenderTarget: an optional per-value sanitiser
// and a fixed suffix appended on finalise. A zero FuncTarget (nil sanitiser,
// empty suffix) is an identity target suitable for human titles.
type FuncTarget struct {
	NameVal   string
	Sanitiser func(string) string
	Suffix    string
}

// Name reports the target name (e.g. "title", "filename").
func (t FuncTarget) Name() string { return t.NameVal }

// SanitiseValue applies the per-value sanitiser, or is identity when none.
func (t FuncTarget) SanitiseValue(v string) string {
	if t.Sanitiser == nil {
		return v
	}
	return t.Sanitiser(v)
}

// Finalise appends the target's suffix to the assembled string.
func (t FuncTarget) Finalise(assembled string) string { return assembled + t.Suffix }

// PlainTarget returns an identity target (no sanitisation, no suffix) for
// human-facing names such as draft titles.
func PlainTarget(name string) RenderTarget { return FuncTarget{NameVal: name} }
