package analytics

import "regexp"

// syntheticShape classifies the encoded form of a Report.SourceObject that is
// not a stored Custom Report Type. See docs/design/crt-report-support.md §4.6
// and docs/design/crt-research-data.md Q3 for the observed forms.
type syntheticShape int

const (
	shapeUnknown syntheticShape = iota
	shapeCustomEntity
	shapeCustomEntityScoped
	shapeNestedCustomEntity
)

type parsedSynthetic struct {
	shape        syntheticShape
	baseObject   string
	anchorObject string
	anchorField  string
	secondary    string
}

// syntheticParser converts Report.SourceObject strings into a parsedSynthetic.
// It is a pure string parser with no dependencies; the zero value is usable.
type syntheticParser struct{}

// ident matches a Salesforce-style identifier segment, including namespace
// prefixes (`pqcrush__Account_Plan__c`) and standard fields.
const ident = `[A-Za-z0-9_]+`

// optPrefix is an optional, semantically-inert leading identifier on the
// single-entity forms. Salesforce prefixes some synthetic-CRT source strings
// with an object name (the historical hardcoded case was the standard sales
// order object); the prefix never changes how the row is synthesized — base and
// anchor are always resolved from live metadata, and the resolver routes a
// prefixed form down the identical path as the unprefixed one. We therefore
// accept ANY prefix structurally and capture nothing, rather than enumerate app
// object names. L0 speaks the v63 dialect by grammar, not by a register of
// object names, which keeps this parser application-neutral (a Jira/ITSM pack's
// objects are recognized identically).
const optPrefix = `(?:` + ident + `)?`

// Ordering matters: the nested-with-tail pattern must be tried before the plain
// nested one, and the scoped variant before the bare one, so the most specific
// shape wins. The generalized single-entity arms match exactly one "$"-segment
// (plus an optional "@anchor.field" tail), so they cannot swallow the
// two-segment nested forms above.
var (
	reNestedScoped = regexp.MustCompile(
		`^CustomEntityCustomEntity\$(` + ident + `)\$(` + ident + `)@(` + ident + `)\.(` + ident + `)$`,
	)
	reNested = regexp.MustCompile(
		`^CustomEntityCustomEntity\$(` + ident + `)\$(` + ident + `)$`,
	)
	reScoped = regexp.MustCompile(
		`^` + optPrefix + `CustomEntity\$(` + ident + `)@(` + ident + `)\.(` + ident + `)$`,
	)
	reBare = regexp.MustCompile(
		`^` + optPrefix + `CustomEntity\$(` + ident + `)$`,
	)
)

// Parse classifies sourceObject into a parsedSynthetic. Never panics, never
// returns an error; unrecognised input yields parsedSynthetic{shape: shapeUnknown}.
func (p *syntheticParser) Parse(sourceObject string) parsedSynthetic {
	if sourceObject == "" {
		return parsedSynthetic{}
	}

	if m := reNestedScoped.FindStringSubmatch(sourceObject); m != nil {
		// Nested-with-anchor is downgraded to shapeNestedCustomEntity; the synthesizer
		// degrades further to base-object-only per design §4.10 C5.
		return parsedSynthetic{
			shape:        shapeNestedCustomEntity,
			baseObject:   m[1],
			secondary:    m[2],
			anchorObject: m[3],
			anchorField:  m[4],
		}
	}
	if m := reNested.FindStringSubmatch(sourceObject); m != nil {
		return parsedSynthetic{
			shape:      shapeNestedCustomEntity,
			baseObject: m[1],
			secondary:  m[2],
		}
	}
	// Generalized single-entity forms. An optional object-name prefix
	// (formerly the literal "Order…") is matched but discarded; a prefixed
	// string resolves identically to its unprefixed counterpart. As a
	// consequence, previously-unmatched prefixed forms (e.g.
	// "OpportunityCustomEntity$Foo", or a single-segment doubled-prefix
	// "CustomEntityCustomEntity$Foo") now classify here instead of
	// shapeUnknown. This does not regress: an unknown shape fell through to a
	// single-table path that fails on a non-real base object, and so does
	// synthesizeSynthetic via the C4 base-missing branch — while a real base
	// now synthesizes correctly.
	if m := reScoped.FindStringSubmatch(sourceObject); m != nil {
		return parsedSynthetic{
			shape:        shapeCustomEntityScoped,
			baseObject:   m[1],
			anchorObject: m[2],
			anchorField:  m[3],
		}
	}
	if m := reBare.FindStringSubmatch(sourceObject); m != nil {
		return parsedSynthetic{
			shape:      shapeCustomEntity,
			baseObject: m[1],
		}
	}
	return parsedSynthetic{}
}
