package dialectsfcrm

import (
	"context"
	"regexp"
	"sort"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// Formula fields that are a pure passthrough of a source picklist — e.g.
// Opportunity.Region__c = text(Account.Region__c) — display exactly the source
// picklist's values but describe as a plain scalar with no options, so the UI
// renders them as flat text instead of the source's colored badge. This resolver
// recovers the source picklist's options (labels + tags, including presentation:*
// semantic tags) so the describe response can carry them and the field renders
// with the same badge as its source. It is render-only: callers attach these to
// PicklistValues without changing the wire type, so filters and (absent) edit
// inputs are unaffected — the same discipline the rich-text describe path uses.

const formulaMirrorMaxDepth = 4

// commentRe strips /* ... */ formula comments; wsRe collapses whitespace.
var (
	formulaCommentRe = regexp.MustCompile(`(?s)/\*.*?\*/`)
	formulaWsRe      = regexp.MustCompile(`\s+`)
	// A single passthrough: TEXT(ref) or bare ref, where ref is a field reached
	// through zero or more parent relationship segments (Region__c,
	// Account.Region__c, Order__r.X, Contact.Account.Industry).
	mirrorSingleRe = regexp.MustCompile(`^(?i:text)\(([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*)\)$|^([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*)$`)
	// A dual-source passthrough chosen by a presence check:
	// IF(ISBLANK(anything),TEXT(refA),TEXT(refB)). Both refs must resolve to the
	// same source picklist (the CampaignMember Lead-vs-Contact shape, whose
	// Contact branch commonly walks Contact.Account.<field>).
	mirrorDualRe = regexp.MustCompile(`^(?i:if)\((?i:isblank)\([^()]*\),(?i:text)\(([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*)\),(?i:text)\(([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*)\)\)$`)
)

func normalizeFormula(formula string) string {
	f := formulaCommentRe.ReplaceAllString(formula, "")
	f = formulaWsRe.ReplaceAllString(f, "")
	return f
}

// mirrorRefsFromFormula returns the source references a passthrough formula
// mirrors, or nil if the formula is not a recognized pure passthrough. Each ref
// is "Rel.Field" or bare "Field".
func mirrorRefsFromFormula(formula string) []string {
	f := normalizeFormula(formula)
	if f == "" {
		return nil
	}
	if m := mirrorSingleRe.FindStringSubmatch(f); m != nil {
		ref := m[1]
		if ref == "" {
			ref = m[2]
		}
		return []string{ref}
	}
	if m := mirrorDualRe.FindStringSubmatch(f); m != nil {
		return []string{m[1], m[2]}
	}
	return nil
}

// relationshipTargets maps a schema's parent-relationship names (lowercased) to
// the object they point at, derived from the schema's own lookup fields so the
// resolver stays self-contained (no external relationship query needed).
func relationshipTargets(schema *models.ObjectMetadata) map[string]string {
	out := make(map[string]string)
	if schema == nil {
		return out
	}
	for _, f := range schema.Fields {
		// Concrete single-target lookups only. A polymorphic relationship (WhatId,
		// WhoId, OwnerId, …) has no one concrete parent; picking ReferenceTo[0]
		// could resolve to the wrong object and, if that object has a same-named
		// picklist, inherit the wrong options. Check the IsPolymorphic flag too,
		// not just the target count: a transitional polymorphic reference can
		// advertise a single ReferenceTo while still being polymorphic. Skipping
		// them keeps the promised plain-text fallback instead of a wrong badge.
		if f.IsPolymorphic || len(f.ReferenceTo) != 1 || strings.TrimSpace(f.ReferenceTo[0]) == "" {
			continue
		}
		target := f.ReferenceTo[0]
		if f.RelationshipName != nil && strings.TrimSpace(*f.RelationshipName) != "" {
			out[strings.ToLower(strings.TrimSpace(*f.RelationshipName))] = target
		}
		// Standard relationship name: FooId -> Foo.
		if strings.HasSuffix(f.APIName, "Id") && len(f.APIName) > 2 {
			out[strings.ToLower(f.APIName[:len(f.APIName)-2])] = target
		}
	}
	return out
}

func fieldByName(schema *models.ObjectMetadata, name string) *models.FieldMetadata {
	if schema == nil {
		return nil
	}
	lower := strings.ToLower(name)
	for i := range schema.Fields {
		if strings.ToLower(schema.Fields[i].APIName) == lower {
			return &schema.Fields[i]
		}
	}
	return nil
}

// A mirror source must be a single-select picklist: TEXT(multipicklist) yields a
// semicolon-joined composite value that has no single badge, so multipicklist
// sources are intentionally excluded (such a field falls back to plain text).
func isPicklistType(f *models.FieldMetadata) bool {
	return constants.NormalizeFieldType(string(f.Type)) == constants.FieldTypePicklist
}

func isFormulaType(f *models.FieldMetadata) bool {
	t := constants.NormalizeFieldType(string(f.Type))
	return t == constants.FieldTypeFormula || t == constants.FieldTypeRollupSummary
}

// resolveSourceOptions resolves a single "Rel.Field" / "Field" ref against the
// given schema to the source picklist's options, recursing through intermediate
// passthrough formulas (the two-hop Invoice/Revenue -> Order -> Opportunity chain).
func resolveSourceOptions(ctx context.Context, schemas schemaLookup, schema *models.ObjectMetadata, ref string, depth int) models.PicklistOptions {
	if depth > formulaMirrorMaxDepth || schema == nil {
		return nil
	}
	// A ref is a field reached through zero or more parent relationship hops:
	// the last segment is the field, the preceding segments are relationships to
	// walk (Contact.Account.Industry -> hop Contact, hop Account, field Industry).
	segments := strings.Split(ref, ".")
	fieldName := segments[len(segments)-1]
	srcSchema := schema
	for _, rel := range segments[:len(segments)-1] {
		target := relationshipTargets(srcSchema)[strings.ToLower(rel)]
		if target == "" || schemas == nil {
			return nil
		}
		srcSchema = schemas.getSchema(ctx, target)
		if srcSchema == nil {
			return nil
		}
	}
	srcField := fieldByName(srcSchema, fieldName)
	if srcField == nil {
		return nil
	}
	if isPicklistType(srcField) && len(srcField.Options) > 0 {
		return srcField.Options
	}
	if isFormulaType(srcField) && srcField.Formula != nil {
		refs := mirrorRefsFromFormula(*srcField.Formula)
		return resolveMirrorRefs(ctx, schemas, srcSchema, refs, depth+1)
	}
	return nil
}

// resolveMirrorRefs resolves a set of refs; when there is more than one (dual
// source), every ref must resolve to the same option set (compared by value) or
// the result is nil, since a badge only makes sense when the value domain is one
// picklist.
func resolveMirrorRefs(ctx context.Context, schemas schemaLookup, schema *models.ObjectMetadata, refs []string, depth int) models.PicklistOptions {
	if len(refs) == 0 {
		return nil
	}
	var first models.PicklistOptions
	for i, ref := range refs {
		opts := resolveSourceOptions(ctx, schemas, schema, ref, depth)
		if len(opts) == 0 {
			return nil
		}
		if i == 0 {
			first = opts
			continue
		}
		if !sameOptionMetadata(first, opts) {
			return nil
		}
	}
	return first
}

// sameOptionMetadata compares full option metadata (value, label, and tags) as a
// multiset, order-insensitive. A dual-source mirror returns the first source's
// options, so the two sources must agree on labels and presentation tags — not
// just values — or a record resolved through the second branch would render the
// wrong label or semantic badge. On any divergence we fail closed (plain text).
func sameOptionMetadata(a, b models.PicklistOptions) bool {
	if len(a) != len(b) {
		return false
	}
	counts := make(map[string]int, len(a))
	for i := range a {
		counts[optionMetadataKey(a[i])]++
	}
	for i := range b {
		key := optionMetadataKey(b[i])
		if counts[key] == 0 {
			return false
		}
		counts[key]--
	}
	return true
}

func optionMetadataKey(o models.PicklistOption) string {
	tags := append([]string(nil), o.Tags...)
	sort.Strings(tags)
	return o.Value + "\x00" + o.Label + "\x00" + strings.Join(tags, "\x01")
}

// resolveFormulaMirrorOptions returns the source picklist options a formula field
// mirrors, or nil when the field is not a pure passthrough of a picklist. The
// returned options carry the source labels and tags verbatim.
func resolveFormulaMirrorOptions(ctx context.Context, schemas schemaLookup, schema *models.ObjectMetadata, field models.FieldMetadata) models.PicklistOptions {
	if !isFormulaType(&field) || field.Formula == nil {
		return nil
	}
	// Only a text-returning formula can mirror a picklist value; skip numeric,
	// date, and boolean formulas before parsing/resolving so most computed
	// fields never trigger a cross-object schema lookup.
	if field.ReturnType != nil && !isTextScalarType(*field.ReturnType) {
		return nil
	}
	refs := mirrorRefsFromFormula(*field.Formula)
	return resolveMirrorRefs(ctx, schemas, schema, refs, 1)
}

func isTextScalarType(t models.FieldType) bool {
	switch constants.NormalizeFieldType(string(t)) {
	case constants.FieldTypeString, constants.FieldTypePicklist, constants.FieldTypeMultiPicklist,
		constants.FieldTypeTextarea, constants.FieldTypeRichtextarea:
		return true
	default:
		return false
	}
}

// mirrorPicklistEntries renders resolved source options into the same describe
// entry shape mapPicklistValues emits, so the field's PicklistValues carries
// active/label/value plus any tags (including presentation:* semantics).
func mirrorPicklistEntries(opts models.PicklistOptions) []interface{} {
	entries := make([]interface{}, 0, len(opts))
	for _, opt := range opts {
		entry := map[string]interface{}{
			"active":       true,
			"label":        opt.Label,
			"value":        opt.Value,
			"defaultValue": false,
		}
		if len(opt.Tags) > 0 {
			entry["tags"] = opt.Tags
		}
		entries = append(entries, entry)
	}
	return entries
}
