package dialectsfcrm

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/fieldnormalize"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/metadatarel"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/recordtypes"
)

const defaultAPIVersion = "v63.0"
const systemFileObjectName = "_System_File"

func DescribeGlobalNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	metadata, policy, ok := describeDeps(deps)
	if !ok {
		return internalResponse("Unable to resolve effective access"), nil
	}

	objects, err := metadata.ListObjects(ctx)
	if err != nil {
		return internalResponse("Unable to resolve effective access"), nil
	}

	sobjects := make([]describeGlobalObject, 0, len(objects))
	sobjectsBase := versionedSObjectsBasePath(req.Version)
	for _, object := range objects {
		if isHistoryObject(object.APIName) {
			continue
		}
		access, err := policy.Access(ctx, req.User, object.APIName, contract.AccessRequest{})
		if err != nil {
			continue
		}
		if access == nil || !access.Object.Read {
			continue
		}
		objectAccess := access.Object

		dto := describeGlobalObject{
			Activateable:          false,
			AssociateEntityType:   nil,
			AssociateParentEntity: nil,
			Createable:            objectAccess.Create,
			Custom:                object.IsCustom,
			CustomSetting:         object.CustomSetting,
			Deletable:             objectAccess.Delete,
			DeprecatedAndHidden:   false,
			FeedEnabled:           true,
			HasSubtypes:           false,
			IsSubtype:             false,
			KeyPrefix:             metadataKeyPrefix(ctx, metadata, object.APIName),
			Label:                 object.Label,
			LabelPlural:           object.PluralLabel,
			Layoutable:            !object.IsSystem,
			Mergeable:             false,
			MruEnabled:            true,
			Name:                  object.APIName,
			IsSystem:              object.IsSystem,
			Queryable:             objectAccess.Read,
			Replicateable:         objectAccess.Read,
			Retrieveable:          objectAccess.Read,
			Searchable:            objectAccess.Read && object.Searchable,
			SharingModel:          string(object.SharingModel),
			TableType:             string(object.TableType),
			Triggerable:           !object.IsSystem,
			Undeletable:           true,
			Updateable:            objectAccess.Update,
			Urls: map[string]string{
				"sobject":     fmt.Sprintf("%s/%s", sobjectsBase, object.APIName),
				"describe":    fmt.Sprintf("%s/%s/describe", sobjectsBase, object.APIName),
				"rowTemplate": fmt.Sprintf("%s/%s/{ID}", sobjectsBase, object.APIName),
			},
		}
		if object.SharingModel == constants.SharingModelControlledByParent {
			dto.SharingParentObject = sharingParentObject(ctx, metadata, object.APIName)
		}
		sobjects = append(sobjects, dto)
		if historySchemaSupportsFieldHistory(&object) {
			sobjects = append(sobjects, describeHistoryGlobalObject(req.Version, &object))
		}
	}

	return &dialect.Response{
		Status: http.StatusOK,
		Body: describeGlobalResult{
			Encoding:     "UTF-8",
			MaxBatchSize: 200,
			SObjects:     sobjects,
		},
	}, nil
}

func DescribeObjectNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	objectName := req.Params["sobject"]
	metadata, policy, ok := describeDeps(deps)
	if !ok {
		return internalResponse("Unable to resolve effective access"), nil
	}
	if isHistoryObject(objectName) {
		return describeHistoryObject(ctx, req, metadata, policy, objectName), nil
	}

	access, resp := effectiveAccessForDescribe(ctx, policy, req.User, objectName, "Insufficient access to describe this object", "Unable to resolve effective access")
	if resp != nil {
		return resp, nil
	}

	schema, err := metadata.Describe(ctx, objectName)
	if err != nil || schema == nil {
		return notFoundResponse("The requested resource does not exist"), nil
	}

	response, err := describeObjectResponse(ctx, req.Version, metadata, access, schema)
	if err != nil {
		return internalResponse("Unable to resolve effective access"), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: response}, nil
}

func DescribeSystemFileNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	if req == nil {
		req = &dialect.Request{}
	}
	cloned := *req
	cloned.Params = make(map[string]string, len(req.Params)+1)
	for key, value := range req.Params {
		cloned.Params[key] = value
	}
	cloned.Params["sobject"] = systemFileObjectName
	return DescribeObjectNeutralHandler(ctx, &cloned, deps)
}

func describeObjectResponse(ctx context.Context, version string, metadata contract.Metadata, access *contract.EffectiveObjectAccess, schema *models.ObjectMetadata) (describeSObjectResult, error) {
	canRead := access.Object.Read
	canCreate := access.Object.Create
	canEdit := access.Object.Update
	canDelete := access.Object.Delete

	schemaLookup := contractSchemaLookup{metadata: metadata}
	relationships, err := metadata.Relationships(ctx, schema.APIName)
	if err != nil {
		return describeSObjectResult{}, err
	}
	relationshipsByFieldMap := relationshipsByField(relationships)

	compoundParentByMember := make(map[string]string)
	for _, cf := range schema.Fields {
		isCompound, composedFrom := fieldprovenance.ClassifyCompound(schema.APIName, cf.APIName)
		if !isCompound {
			continue
		}
		for _, member := range composedFrom {
			compoundParentByMember[strings.ToLower(member)] = cf.APIName
		}
	}

	fields := make([]sObjectField, 0)
	for _, f := range schema.Fields {
		normalizedType := constants.NormalizeFieldType(string(f.Type))
		rels := relationshipsByFieldMap[strings.ToLower(f.APIName)]
		deleteSemantics := metadatarel.ResolveFieldDeleteSemantics(f, rels)
		cascadeDelete, restrictedDelete := deleteSemantics.Cascade, deleteSemantics.Restricted
		runtimePicklist := buildRuntimePicklistInfo(ctx, schemaLookup, f)
		runtimeLookup := buildRuntimeLookupInfo(f)

		sfType := mapFieldTypeToSF(f)
		compoundParentName, compoundComponentRole := fieldprovenance.ClassifyCompoundMember(schema.APIName, f.APIName)
		if compoundParentName == "" {
			if parent, ok := compoundParentByMember[strings.ToLower(f.APIName)]; ok {
				compoundParentName = parent
			}
		}
		var compoundFieldName *string
		if compoundParentName != "" {
			compoundFieldName = &compoundParentName
		}
		var compoundComponentRolePtr *string
		if strings.TrimSpace(compoundComponentRole) != "" {
			compoundComponentRolePtr = &compoundComponentRole
		}
		var typedDefault interface{}
		if f.DefaultValue != nil {
			typedDefault = fieldnormalize.NormalizeDDLDefault(*f.DefaultValue, string(f.Type))
		}
		calculated := normalizedType == constants.FieldTypeFormula ||
			normalizedType == constants.FieldTypeRollupSummary ||
			(f.IsVirtual && f.VirtualKind == constants.VirtualFieldCompound) ||
			formulastorage.IsActiveComputedField(&f)
		var calculatedFormula *string
		if calculated && f.Formula != nil && strings.TrimSpace(*f.Formula) != "" {
			calculatedFormula = f.Formula
		}

		staticDependentPicklist := supportsStaticPicklistDependency(schema, f)
		picklistValues := mapPicklistValues(schema, f)
		if len(picklistValues) == 0 {
			// A formula that purely passes through a source picklist (e.g.
			// Opportunity.Region__c = text(Account.Region__c)) carries the source
			// picklist's values; surface them so the UI renders the same badge.
			// Render-only: the wire type is left as the resolved scalar, so
			// filters and (absent) edit inputs are unaffected.
			if mirror := resolveFormulaMirrorOptions(ctx, schemaLookup, schema, f); len(mirror) > 0 {
				picklistValues = mirrorPicklistEntries(mirror)
			}
		}
		fieldAccess := access.Fields[f.APIName]
		dto := sObjectField{
			Aggregatable:                 isAggregatable(sfType),
			AiPredictionField:            false,
			AutoNumber:                   normalizedType == constants.FieldTypeAutonumber,
			ByteLength:                   fieldLength(f),
			Calculated:                   calculated,
			CalculatedFormula:            calculatedFormula,
			CascadeDelete:                cascadeDelete,
			CaseSensitive:                false,
			CompoundFieldName:            compoundFieldName,
			CompoundComponentRole:        compoundComponentRolePtr,
			Createable:                   fieldAccess.Createable,
			Custom:                       !f.IsSystem,
			DefaultValue:                 typedDefault,
			DefaultedOnCreate:            f.DefaultedOnCreate || f.DefaultValue != nil,
			DependentPicklist:            runtimePicklist != nil || staticDependentPicklist,
			DeprecatedAndHidden:          strings.Contains(strings.ToLower(f.Label), "(do not use)"),
			Digits:                       fieldDigits(f),
			DisplayLocationInDecimal:     sfType == "location",
			Encrypted:                    false,
			ExternalID:                   f.IsUnique,
			Filterable:                   isFilterable(sfType),
			FilteredLookupInfo:           nil,
			FormulaTreatNullNumberAsZero: false,
			Groupable:                    isGroupable(sfType),
			HighScaleNumber:              false,
			HtmlFormatted:                f.Type == constants.FieldTypeRichtextarea,
			IdLookup:                     f.IsUnique || f.APIName == "Id",
			InlineHelpText:               f.HelpText,
			Label:                        f.Label,
			Length:                       fieldLength(f),
			Name:                         f.APIName,
			NameField:                    f.IsNameField,
			Nillable:                     isNillable(sfType, f.Required),
			Permissionable:               true,
			PicklistValues:               picklistValues,
			PolymorphicForeignKey:        false,
			Precision:                    fieldPrecision(f),
			QueryByDistance:              sfType == "location",
			Readable:                     fieldAccess.Readable,
			ReferenceTo:                  nonNilStringSlice(f.ReferenceTo),
			RelationshipName:             f.RelationshipName,
			RestrictedDelete:             restrictedDelete,
			RestrictedPicklist:           false,
			Scale:                        fieldScale(f),
			SearchPrefilterable:          true,
			SoapType:                     mapFieldTypeToSoapType(models.FieldType(sfType)),
			Sortable:                     isSortable(sfType),
			Type:                         sfType,
			Unique:                       f.IsUnique,
			Updateable:                   fieldAccess.Updateable,
		}
		if f.IsVirtual {
			dto.Filterable = false
			dto.Sortable = false
			dto.Groupable = false
			dto.Aggregatable = false
			if f.VirtualKind == constants.VirtualFieldSystemPermission {
				dto.DefaultValue = false
				dto.DefaultedOnCreate = true
				dto.Custom = false
			} else if f.VirtualKind == constants.VirtualFieldCompound {
				dto.Filterable = true
				dto.Custom = false
			}
		}
		if staticDependentPicklist {
			dto.ControllerName = f.ControllingField
		}
		if runtimePicklist != nil || runtimeLookup != nil {
			dto.ControllerName = f.ControllingField
			dto.FilteredLookupInfo = filteredLookupInfo{RuntimePicklist: runtimePicklist, RuntimeLookup: runtimeLookup}
		}
		fields = append(fields, dto)
	}

	childRelationships, err := describeChildRelationships(ctx, metadata, schema.APIName)
	if err != nil {
		return describeSObjectResult{}, err
	}

	sobjectsBase := versionedSObjectsBasePath(version)
	presentationRecordTypes := recordtypes.ProjectContractSelectionEntries(access)
	recordTypeInfos := make([]recordTypeInfo, 0, len(presentationRecordTypes))
	for _, rt := range presentationRecordTypes {
		recordTypeInfos = append(recordTypeInfos, recordTypeInfo{
			Active:                   true,
			Available:                rt.Available,
			DefaultRecordTypeMapping: rt.Default,
			DeveloperName:            rt.APIName,
			Master:                   rt.Master,
			Name:                     rt.Name,
			RecordTypeId:             rt.ID,
			Urls: map[string]string{
				"layout": fmt.Sprintf("%s/%s/describe/layouts/%s", sobjectsBase, schema.APIName, rt.ID),
			},
		})
	}

	pathField := ""
	if schema.PathField != nil {
		pathField = *schema.PathField
	}
	displayNameField := ""
	if schema.DisplayNameField != nil && strings.TrimSpace(*schema.DisplayNameField) != "" {
		displayNameField = *schema.DisplayNameField
	} else {
		for _, field := range schema.Fields {
			if field.IsNameField {
				displayNameField = field.APIName
				break
			}
		}
	}

	return describeSObjectResult{
		ActionOverrides:       []interface{}{},
		Activateable:          false,
		AssociateEntityType:   nil,
		AssociateParentEntity: nil,
		ChildRelationships:    childRelationships,
		CompactLayoutable:     true,
		Createable:            canCreate,
		Custom:                schema.IsCustom,
		CustomSetting:         schema.CustomSetting,
		Deletable:             canDelete,
		DeepCloneable:         false,
		DeprecatedAndHidden:   false,
		FeedEnabled:           true,
		Fields:                fields,
		HasSubtypes:           false,
		IsSubtype:             false,
		KeyPrefix:             metadataKeyPrefix(ctx, metadata, schema.APIName),
		Label:                 schema.Label,
		LabelPlural:           schema.PluralLabel,
		Layoutable:            !schema.IsSystem,
		ListFields:            schema.ListFields,
		PathField:             pathField,
		DisplayNameField:      displayNameField,
		Mergeable:             false,
		MruEnabled:            true,
		Name:                  schema.APIName,
		NamedLayoutInfos:      []interface{}{},
		Queryable:             canRead,
		RecordTypeInfos:       recordTypeInfos,
		Replicateable:         canRead,
		Retrieveable:          canRead,
		SearchLayoutable:      true,
		Searchable:            canRead && schema.Searchable,
		SupportedScopes: []interface{}{
			map[string]string{"label": fmt.Sprintf("All %s", schema.PluralLabel), "name": "everything"},
			map[string]string{"label": fmt.Sprintf("My %s", schema.PluralLabel), "name": "mine"},
		},
		SObjectDescribeOption: "FULL",
		Triggerable:           !schema.IsSystem,
		Undeletable:           true,
		Updateable:            canEdit,
		Urls: map[string]string{
			"sobject":         fmt.Sprintf("%s/%s", sobjectsBase, schema.APIName),
			"describe":        fmt.Sprintf("%s/%s/describe", sobjectsBase, schema.APIName),
			"rowTemplate":     fmt.Sprintf("%s/%s/{ID}", sobjectsBase, schema.APIName),
			"layouts":         fmt.Sprintf("%s/%s/describe/layouts", sobjectsBase, schema.APIName),
			"compactLayouts":  fmt.Sprintf("%s/%s/describe/compactLayouts", sobjectsBase, schema.APIName),
			"approvalLayouts": fmt.Sprintf("%s/%s/describe/approvalLayouts", sobjectsBase, schema.APIName),
			"quickActions":    fmt.Sprintf("%s/%s/quickActions", sobjectsBase, schema.APIName),
			"listviews":       fmt.Sprintf("%s/%s/listviews", sobjectsBase, schema.APIName),
			"defaultValues":   fmt.Sprintf("%s/%s/defaultValues", sobjectsBase, schema.APIName),
		},
	}, nil
}

func describeDeps(deps dialect.Deps) (contract.Metadata, contract.Policy, bool) {
	metadata := deps.Contracts.Metadata
	policy := deps.Contracts.Policy
	return metadata, policy, metadata != nil && policy != nil
}

func effectiveAccessForDescribe(ctx context.Context, policy contract.Policy, user *models.UserSession, objectName string, forbiddenMessage string, internalMessage string) (*contract.EffectiveObjectAccess, *dialect.Response) {
	if policy == nil {
		return nil, internalResponse(internalMessage)
	}
	access, err := policy.Access(ctx, user, objectName, contract.AccessRequest{
		IncludeFields:      true,
		IncludeRecordTypes: true,
	})
	if err != nil {
		return nil, internalResponse(internalMessage)
	}
	if access == nil || !access.Object.Read {
		return nil, accessDeniedResponse(forbiddenMessage)
	}
	return access, nil
}

func describeChildRelationships(ctx context.Context, metadata contract.Metadata, parentObject string) ([]childRelationship, error) {
	childSchemas, err := metadata.ChildRelationships(ctx, parentObject)
	if err != nil {
		return nil, err
	}
	childRelationships := make([]childRelationship, 0)
	for _, childSchema := range childSchemas {
		if childSchema == nil {
			continue
		}
		rels, err := metadata.Relationships(ctx, childSchema.APIName)
		if err != nil {
			return nil, err
		}
		childRelationshipsByField := relationshipsByParentAndField(rels)
		for _, f := range childSchema.Fields {
			if !metadatarel.FieldReferencesParent(f, parentObject) {
				continue
			}
			if f.ChildRelationshipName == nil || *f.ChildRelationshipName == "" {
				continue
			}
			rel := childRelationshipsByField[strings.ToLower(parentObject)+"|"+strings.ToLower(f.APIName)]
			deleteSemantics := metadatarel.ResolveDeleteSemantics(f, rel)
			childRelationships = append(childRelationships, childRelationship{
				CascadeDelete:       deleteSemantics.Cascade,
				ChildSObject:        childSchema.APIName,
				DeprecatedAndHidden: false,
				Field:               f.APIName,
				JunctionIdListNames: nonNilStringSlice(nil),
				JunctionReferenceTo: nonNilStringSlice(nil),
				RelationshipName:    *f.ChildRelationshipName,
				RestrictedDelete:    deleteSemantics.Restricted,
			})
		}
	}
	return childRelationships, nil
}

func relationshipsByField(relationships []*models.Relationship) map[string][]*models.Relationship {
	byField := make(map[string][]*models.Relationship, len(relationships))
	for _, rel := range relationships {
		if rel == nil || rel.FieldAPIName == "" {
			continue
		}
		key := strings.ToLower(rel.FieldAPIName)
		byField[key] = append(byField[key], rel)
	}
	return byField
}

func relationshipsByParentAndField(relationships []*models.Relationship) map[string]*models.Relationship {
	byKey := make(map[string]*models.Relationship, len(relationships))
	for _, rel := range relationships {
		if rel == nil || rel.ParentObjectAPIName == "" || rel.FieldAPIName == "" {
			continue
		}
		key := strings.ToLower(rel.ParentObjectAPIName) + "|" + strings.ToLower(rel.FieldAPIName)
		byKey[key] = rel
	}
	return byKey
}

func sharingParentObject(ctx context.Context, metadata contract.Metadata, childObjectAPIName string) string {
	rels, err := metadata.Relationships(ctx, childObjectAPIName)
	if err != nil {
		return ""
	}
	for _, rel := range rels {
		if rel == nil || !rel.IsSharingParent {
			continue
		}
		if !strings.EqualFold(rel.ChildObjectAPIName, childObjectAPIName) {
			continue
		}
		return rel.ParentObjectAPIName
	}
	return ""
}

func metadataKeyPrefix(ctx context.Context, metadata contract.Metadata, objectAPIName string) string {
	if metadata == nil {
		return ""
	}
	keyPrefix, ok := metadata.KeyPrefix(ctx, objectAPIName)
	if !ok {
		return ""
	}
	return keyPrefix
}

type contractSchemaLookup struct {
	metadata contract.Metadata
}

func (l contractSchemaLookup) getSchema(ctx context.Context, apiName string) *models.ObjectMetadata {
	if l.metadata == nil {
		return nil
	}
	schema, err := l.metadata.Describe(ctx, apiName)
	if err != nil {
		return nil
	}
	return schema
}

func versionedSObjectsBasePath(version string) string {
	return fmt.Sprintf("/services/data/%s/sobjects", apiVersionOrDefault(version))
}

func apiVersionOrDefault(version string) string {
	version = strings.TrimSpace(version)
	if version == "" {
		return defaultAPIVersion
	}
	return version
}

func mapFieldTypeToSoapType(t models.FieldType) string {
	switch constants.NormalizeFieldType(string(t)) {
	case constants.FieldTypeString, constants.FieldTypeTextarea, constants.FieldTypeRichtextarea, constants.FieldTypePhone, constants.FieldTypeEmail, constants.FieldTypeUrl, constants.FieldTypePicklist, constants.FieldTypeMultiPicklist, constants.FieldTypeEncryptedString, constants.FieldTypeAutonumber:
		return "xsd:string"
	case "int", "integer":
		return "xsd:int"
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		return "xsd:double"
	case constants.FieldTypeBoolean:
		return "xsd:boolean"
	case constants.FieldTypeDate:
		return "xsd:date"
	case constants.FieldTypeDatetime:
		return "xsd:dateTime"
	case constants.FieldTypeReference, constants.FieldTypeMasterDetail:
		return "tns:ID"
	default:
		return "xsd:string"
	}
}

func internalResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusInternalServerError,
		Body:   wire.SFErrorResponse(wire.SFCodeInternalError, message),
	}
}

func accessDeniedResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusForbidden,
		Body:   wire.SFErrorResponse(wire.SFCodeForbidden, message),
	}
}

func notFoundResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusNotFound,
		Body:   wire.SFErrorResponse(wire.SFCodeNotFound, message),
	}
}
