package dialectsfcrm

import (
	"encoding/base64"

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

// fieldLength returns the field length from metadata or type-based Salesforce defaults.
func fieldLength(f models.FieldMetadata) int {
	if f.MaxLength != nil && *f.MaxLength > 0 {
		return *f.MaxLength
	}
	sfType := mapFieldTypeToSF(f)
	switch sfType {
	case "id", "reference":
		return 18
	case "boolean":
		return 0
	case "textarea":
		return 32000
	case "double", "int", "currency", "percent":
		return 0
	case "multipicklist":
		return 4099
	case "encryptedstring":
		return 175
	default:
		return 255
	}
}

func resolveEffectiveFieldType(f models.FieldMetadata) models.FieldType {
	t := f.Type
	if f.LogicalType != nil && *f.LogicalType != "" {
		t = *f.LogicalType
	}
	return resolveComputedScalarType(t, f)
}

// resolveComputedScalarType resolves the scalar result type carried by a formula
// or rollup-summary field, so the describe/UI-API/report projections report the
// source scalar type instead of the generic "formula"/"rollupsummary" wrapper.
// Both carry their result scalar in ReturnType — for rollups, persistence
// hydrateRollupResultMetadata copies the resolver-owned RollupConfig.ResultType
// into ReturnType at load time — so a rollup over a percent source resolves to
// "percent", matching Salesforce, which describes such a field as type: percent,
// calculated: true.
//
// This is the dialect's single computed-type read path. resolveEffectiveFieldType
// (describe #374), effectiveUIRecordFieldType (UI-API record values), the UI-API
// object-info field metadata, and the report column dataType all route through it,
// so no site can drift back to reporting the wrapper type. Callers pass the base
// type they have already resolved (e.g. after a LogicalType override); every
// non-computed type is returned unchanged.
func resolveComputedScalarType(t models.FieldType, f models.FieldMetadata) models.FieldType {
	if (t == constants.FieldTypeFormula || t == constants.FieldTypeRollupSummary) &&
		f.ReturnType != nil && *f.ReturnType != "" {
		return *f.ReturnType
	}
	return t
}

func mapFieldTypeToSF(f models.FieldMetadata) string {
	if f.APIName == "Id" {
		return "id"
	}

	effectiveType := constants.NormalizeFieldType(string(resolveEffectiveFieldType(f)))
	switch effectiveType {
	case "text", "short_text", constants.FieldTypeString:
		return "string"
	case "long_text", "rich_text", "textarea", "richtextarea":
		return "textarea"
	case "number", constants.FieldTypeDouble:
		return "double"
	case constants.FieldTypeCurrency:
		return "currency"
	case constants.FieldTypePercent:
		return "percent"
	case "int", "integer":
		return "int"
	case "checkbox", constants.FieldTypeBoolean:
		return "boolean"
	case constants.FieldTypeDate:
		return "date"
	case constants.FieldTypeDatetime:
		return "datetime"
	case "lookup", "master_detail", "masterdetail", "reference":
		return "reference"
	case "select", constants.FieldTypePicklist:
		return "picklist"
	case "multi_select", constants.FieldTypeMultiPicklist:
		return "multipicklist"
	case constants.FieldTypeEmail:
		return "email"
	case constants.FieldTypePhone:
		return "phone"
	case constants.FieldTypeUrl:
		return "url"
	case "password", "encryptedstring":
		return "encryptedstring"
	case "location":
		return "location"
	case "address":
		return "address"
	case "anytype":
		return "anyType"
	default:
		return "string"
	}
}

func fieldDigits(f models.FieldMetadata) int {
	sfType := mapFieldTypeToSF(f)
	switch sfType {
	case "int":
		if f.Precision != nil && *f.Precision > 0 {
			return *f.Precision
		}
		return 18
	default:
		return 0
	}
}

func mapPicklistValues(schema *models.ObjectMetadata, f models.FieldMetadata) []interface{} {
	sfType := mapFieldTypeToSF(f)
	if (sfType != "picklist" && sfType != "multipicklist") || len(f.Options) == 0 {
		return []interface{}{}
	}

	controllerValues, staticDependencySupported := staticDependentPicklistControllerValues(schema, f)
	values := make([]interface{}, len(f.Options))
	for i, opt := range f.Options {
		entry := map[string]interface{}{
			"active":       true,
			"label":        opt.Label,
			"value":        opt.Value,
			"defaultValue": f.DefaultValue != nil && *f.DefaultValue == opt.Value,
		}
		if len(opt.Tags) > 0 {
			entry["tags"] = opt.Tags
		}
		if staticDependencySupported {
			validFor, ok := encodePicklistValidFor(controllerValues, f.PicklistDependency, opt.Value)
			if !ok {
				return []interface{}{}
			}
			entry["validFor"] = validFor
		}
		values[i] = entry
	}
	return values
}

func supportsStaticPicklistDependency(schema *models.ObjectMetadata, field models.FieldMetadata) bool {
	fieldType := normalizeDescribePicklistFieldType(field)
	if (fieldType != "picklist" && fieldType != "multipicklist") ||
		field.ControllingField == nil ||
		len(field.PicklistDependency) == 0 {
		return false
	}
	_, ok := staticDependentPicklistControllerValues(schema, field)
	return ok
}

func staticDependentPicklistControllerValues(schema *models.ObjectMetadata, field models.FieldMetadata) ([]string, bool) {
	if schema == nil || field.ControllingField == nil || len(field.PicklistDependency) == 0 {
		return nil, false
	}

	controller := models.FindField(schema, *field.ControllingField)
	if controller == nil {
		return nil, false
	}
	controllerType := normalizeDescribePicklistFieldType(*controller)
	if (controllerType != "picklist" && controllerType != "multipicklist") || len(controller.Options) == 0 {
		return nil, false
	}

	controllerValues := make([]string, 0, len(controller.Options))
	controllerSet := make(map[string]struct{}, len(controller.Options))
	for _, option := range controller.Options {
		if option.Value == "" {
			continue
		}
		controllerValues = append(controllerValues, option.Value)
		controllerSet[option.Value] = struct{}{}
	}
	if len(controllerValues) == 0 {
		return nil, false
	}

	dependentSet := make(map[string]struct{}, len(field.Options))
	for _, option := range field.Options {
		if option.Value == "" {
			continue
		}
		dependentSet[option.Value] = struct{}{}
	}
	if len(dependentSet) == 0 {
		return nil, false
	}

	referencedDependentValues := make(map[string]struct{}, len(field.Options))
	for controllerValue, dependentValues := range field.PicklistDependency {
		if _, ok := controllerSet[controllerValue]; !ok {
			return nil, false
		}
		for _, dependentValue := range dependentValues {
			if _, ok := dependentSet[dependentValue]; !ok {
				return nil, false
			}
			referencedDependentValues[dependentValue] = struct{}{}
		}
	}

	for dependentValue := range dependentSet {
		if _, ok := referencedDependentValues[dependentValue]; !ok {
			return nil, false
		}
	}
	return controllerValues, true
}

func normalizeDescribePicklistFieldType(field models.FieldMetadata) string {
	if field.LogicalType != nil {
		return string(*field.LogicalType)
	}
	return string(field.Type)
}

func encodePicklistValidFor(controllerValues []string, dependency map[string][]string, dependentValue string) (string, bool) {
	if len(controllerValues) == 0 || len(dependency) == 0 || dependentValue == "" {
		return "", false
	}

	bits := make([]bool, len(controllerValues))
	hasAny := false
	for idx, controllerValue := range controllerValues {
		for _, allowedValue := range dependency[controllerValue] {
			if allowedValue != dependentValue {
				continue
			}
			bits[idx] = true
			hasAny = true
			break
		}
	}
	if !hasAny {
		return "", false
	}

	raw := make([]byte, (len(bits)+7)/8)
	for idx, enabled := range bits {
		if !enabled {
			continue
		}
		raw[idx/8] |= 0x80 >> (idx % 8)
	}
	return base64.StdEncoding.EncodeToString(raw), true
}

func fieldPrecision(f models.FieldMetadata) int {
	sfType := mapFieldTypeToSF(f)
	switch sfType {
	case "double", "currency", "percent", "int":
		if f.Precision != nil && *f.Precision > 0 {
			return *f.Precision
		}
		return 18
	default:
		return 0
	}
}

// maxDeclaredScale bounds every answer fieldScale gives. Salesforce's maximum
// decimal places is 18, so a larger value could not have come from a Salesforce
// baseline; the tooling authoring path accepts Scale from client JSON with no
// range check, and an unbounded value both sizes a per-record allocation in the
// record projection and advertises precision the projection would not honour.
// Bounding here rather than at the formatter keeps describe and record display
// answering with the same number. No field reaches this bound today: the only
// producer of a scale above 18 is the rollup resolver's AVG-over-int result
// (Scale 30), and the deployed pack declares no AVG rollup.
const maxDeclaredScale = 18

func fieldScale(f models.FieldMetadata) int {
	sfType := mapFieldTypeToSF(f)
	switch sfType {
	case "double", "currency", "percent":
		// No type default. pack#4831 D2 settles the platform default as
		// Salesforce's (currency and percent 2), but D2 pairs it with an
		// exception list for the fields Salesforce holds at 0 —
		// Opportunity.Probability among them — and there is nowhere to declare
		// a per-field scale until the substrate lands. Defaulting without that
		// list would move those fields to 2 with no way to except them, so the
		// default ships with the substrate. Until then a field that declares no
		// scale reports none, exactly as before.
		//
		// A negative scale is not a declaration. Nothing can store one today —
		// persistence has no scale column — but the tooling authoring path
		// takes Scale straight from client JSON, and this value reaches a
		// "%.*f" verb, which renders "%!(BADPREC)" rather than a number.
		if f.Scale != nil && *f.Scale >= 0 {
			if *f.Scale > maxDeclaredScale {
				return maxDeclaredScale
			}
			return *f.Scale
		}
		return 0
	default:
		return 0
	}
}

func nonNilStringSlice(s []string) []string {
	if s == nil {
		return []string{}
	}
	return s
}

func isSortable(sfType string) bool {
	switch sfType {
	case "textarea", "base64", "anyType", "address", "location",
		"multipicklist", "encryptedstring":
		return false
	default:
		return true
	}
}

func isAggregatable(sfType string) bool {
	switch sfType {
	case "textarea", "base64", "anyType", "address", "location",
		"multipicklist", "encryptedstring":
		return false
	default:
		return true
	}
}

func isGroupable(sfType string) bool {
	switch sfType {
	case "textarea", "base64", "anyType", "address", "location",
		"multipicklist", "encryptedstring",
		"datetime":
		return false
	default:
		return true
	}
}

func isFilterable(sfType string) bool {
	switch sfType {
	case "encryptedstring":
		return false
	default:
		return true
	}
}

func isNillable(sfType string, required bool) bool {
	if sfType == "boolean" {
		return false
	}
	return !required
}
