package persistence

import (
	"context"
	"fmt"
	"strings"

	pkgerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/formula"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func (r *QueryRepository) prepareRLSForRead(ctx context.Context, result *domainrls.Result, scanAll bool) (*domainrls.Result, error) {
	if result == nil {
		return nil, nil
	}

	prepared := *result

	if result.ParentFilter != nil {
		parentFilter := *result.ParentFilter
		parentRLS, err := r.prepareRLSForRead(ctx, parentFilter.ParentRLSResult, scanAll)
		if err != nil {
			return nil, err
		}
		if parentFilter.ParentSchema != nil {
			readTable, err := r.resolveRLSReadTarget(ctx, parentFilter.ParentSchema, parentRLS)
			if err != nil {
				return nil, err
			}
			parentRLS, err = prepareRLSForReadTarget(parentRLS, parentFilter.ParentSchema, readTable)
			if err != nil {
				return nil, err
			}
			parentFilter.ParentReadTable = readTable
			parentFilter.ParentIDColumn = readTargetColumnName(parentFilter.ParentSchema, readTable, constants.FieldID)
		}
		parentFilter.ParentRLSResult = parentRLS
		prepared.ParentFilter = &parentFilter
	}

	if len(result.TargetRecordFilters) > 0 {
		prepared.TargetRecordFilters = make([]domainrls.TargetRecordFilter, len(result.TargetRecordFilters))
		for i, filter := range result.TargetRecordFilters {
			preparedFilter := filter
			if len(filter.Alternatives) > 0 {
				preparedFilter.Alternatives = make([]domainrls.TargetRecordFilterAlternative, len(filter.Alternatives))
				for alternativeIndex, alternative := range filter.Alternatives {
					preparedAlternative, err := r.prepareTargetRecordAlternativeForRead(ctx, alternative, scanAll)
					if err != nil {
						return nil, err
					}
					preparedFilter.Alternatives[alternativeIndex] = preparedAlternative
				}
				if len(preparedFilter.Alternatives) == 1 {
					copyTargetRecordAlternativeToLegacy(&preparedFilter, preparedFilter.Alternatives[0])
				}
			} else {
				preparedAlternative, err := r.prepareTargetRecordAlternativeForRead(ctx, domainrls.TargetRecordFilterAlternative{
					TargetObjectAPIName:   filter.TargetObjectAPIName,
					TargetRLSResult:       filter.TargetRLSResult,
					TargetSchema:          filter.TargetSchema,
					TargetReadTable:       filter.TargetReadTable,
					TargetIDColumn:        filter.TargetIDColumn,
					TargetIsDeletedColumn: filter.TargetIsDeletedColumn,
					TargetIncludeDeleted:  filter.TargetIncludeDeleted,
				}, scanAll)
				if err != nil {
					return nil, err
				}
				copyTargetRecordAlternativeToLegacy(&preparedFilter, preparedAlternative)
			}

			prepared.TargetRecordFilters[i] = preparedFilter
		}
	}

	return &prepared, nil
}

func (r *QueryRepository) prepareTargetRecordAlternativeForRead(ctx context.Context, alternative domainrls.TargetRecordFilterAlternative, scanAll bool) (domainrls.TargetRecordFilterAlternative, error) {
	prepared := alternative
	targetRLS, err := r.prepareRLSForRead(ctx, alternative.TargetRLSResult, scanAll)
	if err != nil {
		return domainrls.TargetRecordFilterAlternative{}, err
	}
	prepared.TargetRLSResult = targetRLS
	if alternative.TargetSchema == nil {
		return prepared, nil
	}
	readTable, err := r.resolveRLSReadTarget(ctx, alternative.TargetSchema, targetRLS)
	if err != nil {
		return domainrls.TargetRecordFilterAlternative{}, err
	}
	targetRLS, err = prepareRLSForReadTarget(targetRLS, alternative.TargetSchema, readTable)
	if err != nil {
		return domainrls.TargetRecordFilterAlternative{}, err
	}
	prepared.TargetReadTable = readTable
	prepared.TargetIDColumn = readTargetColumnName(alternative.TargetSchema, readTable, constants.FieldID)
	prepared.TargetIncludeDeleted = scanAll
	if schemaHasIsDeleted(alternative.TargetSchema) {
		prepared.TargetIsDeletedColumn = readTargetColumnName(alternative.TargetSchema, readTable, constants.FieldIsDeleted)
	}
	prepared.TargetRLSResult = targetRLS
	return prepared, nil
}

// resolveRLSReadTarget keeps visibility-only reads on the physical table when
// every predicate can be compiled against physical columns. RLS subqueries
// project only Id and inspect visibility fields, so routing them through a
// formula view needlessly expands display joins and can make a point lookup
// materialize the entire view. Formula/view-only criteria still fail closed to
// the canonical resolved view.
func (r *QueryRepository) resolveRLSReadTarget(ctx context.Context, schema *models.ObjectMetadata, result *domainrls.Result) (string, error) {
	baseTable, err := schema.ResolveReadSourceTable()
	if err != nil {
		return "", pkgerrors.NewValidationError("readSource", err.Error())
	}
	if r.viewResolver == nil || rlsCurrentTableCanUseBaseTable(schema, baseTable, result) {
		return baseTable, nil
	}
	return r.resolveReadTable(ctx, schema)
}

func rlsCurrentTableCanUseBaseTable(schema *models.ObjectMetadata, baseTable string, result *domainrls.Result) bool {
	return rlsDirectCriteriaCanUseBaseTable(schema, baseTable, result) &&
		rlsDirectPredicateFieldsCanUseBaseTable(schema, result)
}

// rlsDirectPredicateFieldsCanUseBaseTable covers fields emitted by the RLS
// renderer outside criteria compilation. The query adapter maps these fields
// after selecting the read target, so physical aliases are safe; view-only
// fields require the canonical view. Checking only fields that this Result can
// emit keeps unrelated display formulas from forcing visibility probes onto
// the view.
func rlsDirectPredicateFieldsCanUseBaseTable(schema *models.ObjectMetadata, result *domainrls.Result) bool {
	if result == nil || result.Skip || result.DenyAll {
		return true
	}
	if visibility := result.OwnerVisibility; visibility != nil {
		if len(visibility.PublicVisibleValues) > 0 && !rlsDirectFieldHasBaseColumn(schema, visibility.VisibilityField) {
			return false
		}
		if len(visibility.OwnerVisibleValues) > 0 &&
			(!rlsDirectFieldHasBaseColumn(schema, visibility.OwnerField) ||
				!rlsDirectFieldHasBaseColumn(schema, visibility.VisibilityField)) {
			return false
		}
		return true
	}
	if len(result.AllowedIDs) > 0 {
		column := strings.TrimSpace(result.AllowedIDsColumn)
		if column == "" {
			column = constants.FieldID
		}
		if !fieldHasBarePhysicalColumn(models.FindField(schema, column)) {
			return false
		}
	}
	if result.HasOwnerField && (len(result.OwnerUserIDs) > 0 || len(result.SharingRuleOwnerIDs) > 0 ||
		len(result.SharingRuleFromGroupIDs) > 0 || len(result.GroupOwnerIDs) > 0) &&
		!rlsDirectFieldHasBaseColumn(schema, constants.FieldOwnerID) {
		return false
	}
	if (result.IncludeManualShares || result.IncludeTeamMembers) &&
		!rlsDirectFieldHasBaseColumn(schema, constants.FieldID) {
		return false
	}
	if parent := result.ParentFilter; parent != nil && parent.ParentRLSResult != nil &&
		!rlsDirectFieldHasBaseColumn(schema, parent.ForeignKeyField) {
		return false
	}
	for _, filter := range result.TargetRecordFilters {
		if strings.TrimSpace(filter.TargetObjectField) == "" || strings.TrimSpace(filter.TargetRecordField) == "" {
			continue
		}
		if !rlsDirectFieldHasBaseColumn(schema, filter.TargetObjectField) ||
			!rlsDirectFieldHasBaseColumn(schema, filter.TargetRecordField) {
			return false
		}
	}
	return true
}

func rlsDirectFieldHasBaseColumn(schema *models.ObjectMetadata, apiName string) bool {
	apiName = strings.Trim(strings.TrimSpace(apiName), "`")
	return fieldHasBarePhysicalColumn(models.FindField(schema, apiName))
}

// rlsDirectCriteriaCanUseBaseTable checks only criteria evaluated against this
// read target. Nested parent and polymorphic targets choose their own target as
// prepareRLSForRead descends the RLS tree.
func rlsDirectCriteriaCanUseBaseTable(schema *models.ObjectMetadata, baseTable string, result *domainrls.Result) bool {
	if result == nil || result.Skip || result.DenyAll {
		return true
	}
	fragmentCount := len(result.CriteriaSQLFragments)
	expressionCount := len(result.CriteriaExpressions)
	if fragmentCount == 0 && expressionCount == 0 {
		return true
	}
	if expressionCount == 0 || (fragmentCount > 0 && expressionCount != fragmentCount) {
		return !schemaHasOpaquePredicateUnsafeField(schema)
	}
	for _, expr := range result.CriteriaExpressions {
		if _, err := rlsCriteriaExpressionSQLForReadTarget(schema, baseTable, expr, true); err != nil {
			return false
		}
	}
	return true
}

func copyTargetRecordAlternativeToLegacy(filter *domainrls.TargetRecordFilter, alternative domainrls.TargetRecordFilterAlternative) {
	if filter == nil {
		return
	}
	filter.TargetObjectAPIName = alternative.TargetObjectAPIName
	filter.TargetRLSResult = alternative.TargetRLSResult
	filter.TargetSchema = alternative.TargetSchema
	filter.TargetReadTable = alternative.TargetReadTable
	filter.TargetIDColumn = alternative.TargetIDColumn
	filter.TargetIsDeletedColumn = alternative.TargetIsDeletedColumn
	filter.TargetIncludeDeleted = alternative.TargetIncludeDeleted
}

func prepareRLSForReadTarget(result *domainrls.Result, schema *models.ObjectMetadata, readTable string) (*domainrls.Result, error) {
	prepared, err := prepareRLSAllowedIDsForReadTarget(result, schema, readTable)
	if err != nil {
		return nil, err
	}
	prepared, err = prepareRLSDirectFieldsForReadTarget(prepared, schema, readTable)
	if err != nil {
		return nil, err
	}
	return prepareRLSCriteriaForReadTarget(prepared, schema, readTable)
}

func prepareRLSDirectFieldsForReadTarget(result *domainrls.Result, schema *models.ObjectMetadata, readTable string) (*domainrls.Result, error) {
	if result == nil || schema == nil || result.Skip || result.DenyAll {
		return result, nil
	}
	prepared := *result
	mapField := func(field string) (string, error) {
		apiName := strings.Trim(strings.TrimSpace(field), "`")
		if models.FindField(schema, apiName) == nil {
			return "", pkgerrors.NewValidationError("rlsField", fmt.Sprintf("invalid RLS field: %s.%s", schema.APIName, apiName))
		}
		return readTargetColumnName(schema, readTable, apiName), nil
	}

	if visibility := result.OwnerVisibility; visibility != nil {
		mapped := *visibility
		var err error
		if len(visibility.PublicVisibleValues) > 0 || len(visibility.OwnerVisibleValues) > 0 {
			mapped.VisibilityField, err = mapField(visibility.VisibilityField)
			if err != nil {
				return nil, err
			}
		}
		if len(visibility.OwnerVisibleValues) > 0 {
			mapped.OwnerField, err = mapField(visibility.OwnerField)
			if err != nil {
				return nil, err
			}
		}
		prepared.OwnerVisibility = &mapped
		return &prepared, nil
	}

	if result.HasOwnerField && (len(result.OwnerUserIDs) > 0 || len(result.SharingRuleOwnerIDs) > 0 ||
		len(result.SharingRuleFromGroupIDs) > 0 || len(result.GroupOwnerIDs) > 0) {
		var err error
		prepared.OwnerIDColumn, err = mapField(constants.FieldOwnerID)
		if err != nil {
			return nil, err
		}
	}
	if result.IncludeManualShares || result.IncludeTeamMembers {
		var err error
		prepared.RecordIDColumn, err = mapField(constants.FieldID)
		if err != nil {
			return nil, err
		}
	}
	if parent := result.ParentFilter; parent != nil && parent.ParentRLSResult != nil {
		mapped := *parent
		var err error
		mapped.ForeignKeyField, err = mapField(parent.ForeignKeyField)
		if err != nil {
			return nil, err
		}
		prepared.ParentFilter = &mapped
	}
	if len(result.TargetRecordFilters) > 0 {
		prepared.TargetRecordFilters = append([]domainrls.TargetRecordFilter(nil), result.TargetRecordFilters...)
		for i := range prepared.TargetRecordFilters {
			filter := &prepared.TargetRecordFilters[i]
			if strings.TrimSpace(filter.TargetObjectField) == "" || strings.TrimSpace(filter.TargetRecordField) == "" {
				continue
			}
			var err error
			filter.TargetObjectField, err = mapField(filter.TargetObjectField)
			if err != nil {
				return nil, err
			}
			filter.TargetRecordField, err = mapField(filter.TargetRecordField)
			if err != nil {
				return nil, err
			}
		}
	}
	return &prepared, nil
}

func prepareRLSAllowedIDsForReadTarget(result *domainrls.Result, schema *models.ObjectMetadata, readTable string) (*domainrls.Result, error) {
	if result == nil || schema == nil || len(result.AllowedIDs) == 0 {
		return result, nil
	}
	column := strings.TrimSpace(result.AllowedIDsColumn)
	if column == "" {
		column = constants.FieldID
	}
	if models.FindField(schema, column) == nil {
		return nil, pkgerrors.NewValidationError("rlsAllowedIDs", fmt.Sprintf("invalid allowed-id field: %s.%s", schema.APIName, column))
	}

	prepared := *result
	prepared.AllowedIDsColumn = readTargetColumnName(schema, readTable, column)
	return &prepared, nil
}

func prepareRLSCriteriaForReadTarget(result *domainrls.Result, schema *models.ObjectMetadata, readTable string) (*domainrls.Result, error) {
	if result == nil || schema == nil || len(result.CriteriaExpressions) == 0 {
		return result, nil
	}
	if len(result.CriteriaSQLFragments) > 0 && len(result.CriteriaExpressions) != len(result.CriteriaSQLFragments) {
		return result, nil
	}

	fragments := make([]string, 0, len(result.CriteriaExpressions))
	for _, expr := range result.CriteriaExpressions {
		fragment, err := rlsCriteriaExpressionSQLForReadTarget(schema, readTable, expr, false)
		if err != nil {
			return nil, pkgerrors.NewValidationError("rlsCriteria", fmt.Sprintf("invalid criteria expression: %v", err))
		}
		if strings.TrimSpace(fragment) != "" {
			fragments = append(fragments, fragment)
		}
	}
	if len(fragments) == 0 {
		return result, nil
	}

	prepared := *result
	prepared.CriteriaSQLFragments = fragments
	return &prepared, nil
}

func rlsCriteriaExpressionSQLForReadTarget(schema *models.ObjectMetadata, readTable string, expr string, requireBaseSafe bool) (string, error) {
	baseSafe := true
	mapper := readTargetFieldMapperForTable(schema, readTable)
	if requireBaseSafe && !searchReadTargetExposesLogicalColumns(schema, readTable) {
		mapper = func(identifier string) string {
			trimmed := strings.Trim(strings.TrimSpace(identifier), "`")
			if compoundExpr := virtualCompoundSQLExpression(schema, trimmed, readTable); compoundExpr != "" {
				return compoundExpr
			}
			field := models.FindField(schema, trimmed)
			if field == nil || !fieldHasBarePhysicalColumn(field) {
				baseSafe = false
				return trimmed
			}
			return resolvePhysicalName(schema, trimmed)
		}
	}

	sqlFrag, args, err := formula.ToSQLWithMapper(expr, mapper)
	if err != nil {
		return "", err
	}
	if requireBaseSafe && !baseSafe {
		return "", fmt.Errorf("criteria references a field not readable from base table")
	}
	return resolveRLSCriteriaParams(sqlFrag, args), nil
}

func resolveRLSCriteriaParams(sql string, args []interface{}) string {
	if len(args) == 0 || !strings.Contains(sql, "?") {
		return sql
	}
	var b strings.Builder
	b.Grow(len(sql))
	argIdx := 0
	for _, r := range sql {
		if r == '?' && argIdx < len(args) {
			b.WriteString(rlsCriteriaSQLLiteral(args[argIdx]))
			argIdx++
			continue
		}
		b.WriteRune(r)
	}
	return b.String()
}

func rlsCriteriaSQLLiteral(arg interface{}) string {
	switch v := arg.(type) {
	case string:
		return fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", "''"))
	case int:
		return fmt.Sprintf("%d", v)
	case int64:
		return fmt.Sprintf("%d", v)
	case float64:
		return fmt.Sprintf("%v", v)
	case bool:
		if v {
			return "1"
		}
		return "0"
	case nil:
		return "NULL"
	default:
		return fmt.Sprintf("'%s'", strings.ReplaceAll(fmt.Sprintf("%v", v), "'", "''"))
	}
}
