package recordmutation

import (
	"context"
	"fmt"
	"regexp"
	"strings"

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

var safePolicyIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

type Evaluator struct {
	Query contract.Query
}

func Denial(ctx context.Context, evaluator Evaluator, schema *models.ObjectMetadata, surface string, record models.SObject, user *models.UserSession) (*models.RecordMutationDenial, error) {
	if schema == nil || schema.AccessPolicy == nil || schema.AccessPolicy.RecordMutation == nil {
		return nil, nil
	}
	for i := range schema.AccessPolicy.RecordMutation.Deny {
		denial := &schema.AccessPolicy.RecordMutation.Deny[i]
		matches, err := denialMatches(ctx, evaluator, schema, denial, surface, record, user)
		if err != nil {
			return nil, err
		}
		if matches {
			return denial, nil
		}
	}
	return nil, nil
}

func MayNeedExisting(schema *models.ObjectMetadata, surface string, record models.SObject) bool {
	if schema == nil || schema.AccessPolicy == nil || schema.AccessPolicy.RecordMutation == nil {
		return false
	}
	return schema.AccessPolicy.RecordMutation.MayNeedExisting(surface, record)
}

func WithRecordID(record models.SObject, id string) models.SObject {
	out := make(models.SObject, len(record)+1)
	for key, value := range record {
		out[key] = value
	}
	if strings.TrimSpace(id) != "" {
		out[constants.FieldID] = id
	}
	return out
}

func MergeRecord(existing contract.Record, updates models.SObject) models.SObject {
	merged := models.SObject{}
	for key, value := range existing {
		merged[key] = value
	}
	for key, value := range updates {
		merged[key] = value
	}
	return merged
}

func denialMatches(ctx context.Context, evaluator Evaluator, schema *models.ObjectMetadata, denial *models.RecordMutationDenial, surface string, record models.SObject, user *models.UserSession) (bool, error) {
	if denial == nil || !denialAppliesToSurface(denial, surface) {
		return false, nil
	}
	if denial.When == nil {
		return true, nil
	}
	return conditionMatches(ctx, evaluator, schema, denial.When, record, user)
}

func conditionMatches(ctx context.Context, evaluator Evaluator, schema *models.ObjectMetadata, condition *models.RecordMutationCondition, record models.SObject, user *models.UserSession) (bool, error) {
	if condition == nil {
		return true, nil
	}
	if len(condition.AllOf) > 0 {
		for i := range condition.AllOf {
			matches, err := conditionMatches(ctx, evaluator, schema, &condition.AllOf[i], record, user)
			if err != nil || !matches {
				return matches, err
			}
		}
		return true, nil
	}
	if condition.FieldEquals != nil {
		return fieldEquals(schema, condition.FieldEquals, record, false), nil
	}
	if condition.FieldPresent != nil {
		return fieldPresent(condition.FieldPresent.Field, record), nil
	}
	if condition.FieldPresentEquals != nil {
		return fieldEquals(schema, condition.FieldPresentEquals, record, true), nil
	}
	if condition.RelatedExists != nil {
		return relatedExists(ctx, evaluator, condition.RelatedExists, record, user)
	}
	return true, nil
}

func relatedExists(ctx context.Context, evaluator Evaluator, condition *models.RecordMutationRelatedExistsCondition, record models.SObject, user *models.UserSession) (bool, error) {
	if condition == nil {
		return true, nil
	}
	if evaluator.Query == nil {
		return true, nil
	}
	objectName := strings.TrimSpace(condition.ObjectAPIName)
	parentField := strings.TrimSpace(condition.ParentField)
	parentID := recordID(record)
	if !safePolicyIdentifier.MatchString(objectName) || !safePolicyIdentifier.MatchString(parentField) || strings.TrimSpace(parentID) == "" {
		return true, nil
	}
	filter, ok := relatedExistsFilter(condition, parentID)
	if !ok {
		return true, nil
	}
	result, err := evaluator.Query.Query(ctx, queryspec.QuerySpec{
		ObjectAPIName: objectName,
		Filter:        queryspec.FormulaPredicate(filter),
		Limit:         1,
		ScanAll:       condition.IncludeDeleted,
	}, user)
	if err != nil {
		return false, err
	}
	if result == nil {
		return false, nil
	}
	return len(result.Records) > 0, nil
}

func relatedExistsFilter(condition *models.RecordMutationRelatedExistsCondition, parentID string) (string, bool) {
	parts := []string{fmt.Sprintf("%s == %s", strings.TrimSpace(condition.ParentField), formulaStringLiteral(parentID))}
	for i := range condition.Match {
		match := condition.Match[i]
		field := strings.TrimSpace(match.Field)
		if !safePolicyIdentifier.MatchString(field) {
			return "", false
		}
		value := strings.TrimSpace(match.Value)
		literal := formulaStringLiteral(value)
		if match.CaseInsensitive {
			parts = append(parts, fmt.Sprintf("LOWER(%s) == %s", field, formulaStringLiteral(strings.ToLower(value))))
			continue
		}
		parts = append(parts, fmt.Sprintf("%s == %s", field, literal))
	}
	return strings.Join(parts, " and "), true
}

func formulaStringLiteral(value string) string {
	value = strings.ReplaceAll(value, `\`, `\\`)
	value = strings.ReplaceAll(value, `'`, `\'`)
	return "'" + value + "'"
}

func denialAppliesToSurface(denial *models.RecordMutationDenial, surface string) bool {
	if denial == nil {
		return false
	}
	surface = strings.TrimSpace(surface)
	if len(denial.Surfaces) == 0 {
		return surface != ""
	}
	for _, candidate := range denial.Surfaces {
		if strings.EqualFold(strings.TrimSpace(candidate), surface) {
			return true
		}
	}
	return false
}

func fieldPresent(field string, record models.SObject) bool {
	_, ok := caseInsensitiveRecordValue(record, field)
	return ok
}

func fieldEquals(schema *models.ObjectMetadata, condition *models.RecordMutationFieldEqualsCondition, record models.SObject, requirePresent bool) bool {
	if condition == nil || strings.TrimSpace(condition.Field) == "" {
		return !requirePresent
	}
	value, ok := caseInsensitiveRecordValue(record, condition.Field)
	if !ok {
		return false
	}
	return models.RecordMutationFieldValueEquals(value, condition.Value, condition.CaseInsensitive, recordMutationFieldIsBoolean(schema, condition.Field))
}

func recordMutationFieldIsBoolean(schema *models.ObjectMetadata, field string) bool {
	fieldMetadata := models.FindField(schema, field)
	return fieldMetadata != nil && strings.EqualFold(strings.TrimSpace(string(fieldMetadata.Type)), string(constants.FieldTypeBoolean))
}

func caseInsensitiveRecordValue(record models.SObject, field string) (interface{}, bool) {
	field = strings.TrimSpace(field)
	if record == nil || field == "" {
		return nil, false
	}
	for key, value := range record {
		if strings.EqualFold(strings.TrimSpace(key), field) {
			return value, true
		}
	}
	return nil, false
}

func recordID(record models.SObject) string {
	value, ok := caseInsensitiveRecordValue(record, constants.FieldID)
	if !ok {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}
