package permissions

import (
	"context"
	"database/sql"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1"
)

const AssignmentPolicyEligibilityLifecycleCapability = contract.AssignmentPolicyEligibilityLifecycleCapability

type assignmentPolicyEligibilityLifecycleTransaction interface {
	IsTransactionActive(context.Context) bool
	IsPessimisticTx(context.Context) bool
	ExtractTx(context.Context) interface{}
}

type assignmentPolicyEligibilityLifecycleEvent struct {
	event  contract.RecordLifecycleBatchEvent
	config models.AssignmentPolicyEligibilityLifecyclePolicy
	before assignmentPolicyEligibilitySelectorInput
	after  assignmentPolicyEligibilitySelectorInput
}

type assignmentPolicyEligibilitySelectorInput struct {
	direct    string
	reference *assignmentPolicyEligibilityReferenceKey
}

type assignmentPolicyEligibilityReferenceTarget struct {
	object      string
	field       string
	table       string
	idColumn    string
	fieldColumn string
}

type assignmentPolicyEligibilityReferenceKey struct {
	target assignmentPolicyEligibilityReferenceTarget
	id     string
}

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

// NewAssignmentPolicyEligibilityLifecycleHook interprets the app-authored
// lifecycle projection carried by each Catalog-bound event. Object names,
// fields and selector paths remain metadata; this L0 hook
// owns only transactionality, canonical locking, and the neutral eligibility
// transition.
func NewAssignmentPolicyEligibilityLifecycleHook(
	transactions assignmentPolicyEligibilityLifecycleTransaction,
	handler AssignmentPolicyEligibilityMutationBatchHandler,
) (contract.RecordLifecycleBatchHook, error) {
	if transactions == nil {
		return nil, fmt.Errorf("assignment-policy eligibility lifecycle requires a transaction manager")
	}
	if handler == nil {
		return nil, fmt.Errorf("assignment-policy eligibility lifecycle requires a mutation handler")
	}
	return func(ctx context.Context, events []contract.RecordLifecycleBatchEvent) error {
		if len(events) == 0 {
			return nil
		}
		if !transactions.IsTransactionActive(ctx) || !transactions.IsPessimisticTx(ctx) {
			return fmt.Errorf("assignment-policy eligibility lifecycle requires an active pessimistic transaction")
		}
		tx, ok := transactions.ExtractTx(ctx).(*sql.Tx)
		if !ok || tx == nil {
			return fmt.Errorf("assignment-policy eligibility lifecycle transaction must be a non-nil *sql.Tx")
		}

		prepared, references, err := prepareAssignmentPolicyEligibilityLifecycleEvents(events)
		if err != nil {
			return err
		}
		resolved, err := readAssignmentPolicyEligibilityReferenceSelectors(ctx, tx, references)
		if err != nil {
			return err
		}
		mutations := make([]AssignmentPolicyEligibilityMutation, 0, len(prepared))
		for _, candidate := range prepared {
			mutation, mapErr := mapAssignmentPolicyEligibilityLifecycleMutation(candidate, resolved)
			if mapErr != nil {
				return mapErr
			}
			mutations = append(mutations, mutation)
		}
		if err := handler.ApplyAssignmentPolicyEligibilityMutations(ctx, mutations); err != nil {
			return fmt.Errorf("apply assignment-policy eligibility lifecycle batch: %w", err)
		}
		return nil
	}, nil
}

func prepareAssignmentPolicyEligibilityLifecycleEvents(
	events []contract.RecordLifecycleBatchEvent,
) ([]assignmentPolicyEligibilityLifecycleEvent, map[assignmentPolicyEligibilityReferenceTarget]map[string]struct{}, error) {
	prepared := make([]assignmentPolicyEligibilityLifecycleEvent, 0, len(events))
	references := make(map[assignmentPolicyEligibilityReferenceTarget]map[string]struct{})
	for _, event := range events {
		if event.Policy == nil || event.Policy.AssignmentPolicyEligibility == nil {
			return nil, nil, fmt.Errorf(
				"assignment-policy eligibility lifecycle on %s requires a Catalog projection",
				event.Event.ObjectName,
			)
		}
		config := *event.Policy.AssignmentPolicyEligibility
		if err := validateAssignmentPolicyEligibilityLifecycleConfig(config); err != nil {
			return nil, nil, fmt.Errorf("assignment-policy eligibility lifecycle on %s: %w", event.Event.ObjectName, err)
		}
		beforeRecord, afterRecord, err := assignmentPolicyEligibilityLifecycleRecords(event)
		if err != nil {
			return nil, nil, err
		}
		before, err := assignmentPolicyEligibilityLifecycleSelector(beforeRecord, config)
		if err != nil {
			return nil, nil, fmt.Errorf("assignment-policy eligibility %s %s before image: %w", event.Event.ObjectName, event.Event.RecordID, err)
		}
		after, err := assignmentPolicyEligibilityLifecycleSelector(afterRecord, config)
		if err != nil {
			return nil, nil, fmt.Errorf("assignment-policy eligibility %s %s after image: %w", event.Event.ObjectName, event.Event.RecordID, err)
		}
		for _, input := range []assignmentPolicyEligibilitySelectorInput{before, after} {
			if input.reference == nil {
				continue
			}
			ids := references[input.reference.target]
			if ids == nil {
				ids = make(map[string]struct{})
				references[input.reference.target] = ids
			}
			ids[input.reference.id] = struct{}{}
		}
		prepared = append(prepared, assignmentPolicyEligibilityLifecycleEvent{
			event: event, config: config, before: before, after: after,
		})
	}
	return prepared, references, nil
}

func validateAssignmentPolicyEligibilityLifecycleConfig(config models.AssignmentPolicyEligibilityLifecyclePolicy) error {
	if len(config.EnabledWhen) == 0 {
		return fmt.Errorf("enabledWhen requires at least one boolean condition")
	}
	direct := strings.TrimSpace(config.Selector.Field)
	reference := config.Selector.Reference
	if (direct == "") == (reference == nil) {
		return fmt.Errorf("selector requires exactly one direct field or reference")
	}
	if direct != "" && !assignmentPolicyEligibilitySQLIdentifier.MatchString(direct) {
		return fmt.Errorf("selector field %q is not a valid identifier", direct)
	}
	if reference != nil {
		for name, value := range map[string]string{
			"reference.sourceField":               reference.SourceField,
			"reference.targetObject":              reference.TargetObject,
			"reference.targetField":               reference.TargetField,
			"reference.resolvedTargetTable":       reference.ResolvedTargetTable,
			"reference.resolvedTargetIdColumn":    reference.ResolvedTargetIDColumn,
			"reference.resolvedTargetFieldColumn": reference.ResolvedTargetFieldColumn,
		} {
			if !assignmentPolicyEligibilitySQLIdentifier.MatchString(strings.TrimSpace(value)) {
				return fmt.Errorf("%s %q is not a valid identifier", name, value)
			}
		}
	}
	seenConditions := make(map[string]struct{}, len(config.EnabledWhen))
	for _, condition := range config.EnabledWhen {
		field := strings.TrimSpace(condition.Field)
		if !assignmentPolicyEligibilitySQLIdentifier.MatchString(field) {
			return fmt.Errorf("enabledWhen field %q is not a valid identifier", condition.Field)
		}
		key := strings.ToLower(field)
		if _, duplicate := seenConditions[key]; duplicate {
			return fmt.Errorf("enabledWhen repeats field %q", field)
		}
		seenConditions[key] = struct{}{}
	}
	return nil
}

func assignmentPolicyEligibilityLifecycleRecords(
	event contract.RecordLifecycleBatchEvent,
) (contract.Record, contract.Record, error) {
	switch event.Phase {
	case contract.RecordLifecycleAfterCreateInTx:
		return nil, event.Event.Record, nil
	case contract.RecordLifecycleAfterUpdateInTx:
		return event.Event.OldRecord, event.Event.Record, nil
	case contract.RecordLifecycleAfterDeleteInTx:
		return event.Event.OldRecord, nil, nil
	default:
		return nil, nil, fmt.Errorf("assignment-policy eligibility lifecycle phase %q is unsupported", event.Phase)
	}
}

func assignmentPolicyEligibilityLifecycleSelector(
	record contract.Record,
	config models.AssignmentPolicyEligibilityLifecyclePolicy,
) (assignmentPolicyEligibilitySelectorInput, error) {
	if record == nil {
		return assignmentPolicyEligibilitySelectorInput{}, nil
	}
	for _, condition := range config.EnabledWhen {
		value, ok := record[strings.TrimSpace(condition.Field)].(bool)
		if !ok {
			return assignmentPolicyEligibilitySelectorInput{}, fmt.Errorf("field %s must be a boolean", condition.Field)
		}
		if value != condition.Equals {
			return assignmentPolicyEligibilitySelectorInput{}, nil
		}
	}
	if field := strings.TrimSpace(config.Selector.Field); field != "" {
		value, ok := record[field].(string)
		if !ok || strings.TrimSpace(value) == "" {
			return assignmentPolicyEligibilitySelectorInput{}, fmt.Errorf("selector field %s must be a non-empty string", field)
		}
		return assignmentPolicyEligibilitySelectorInput{direct: value}, nil
	}
	reference := config.Selector.Reference
	id, ok := record[strings.TrimSpace(reference.SourceField)].(string)
	if !ok || strings.TrimSpace(id) == "" {
		return assignmentPolicyEligibilitySelectorInput{}, fmt.Errorf("reference source field %s must be a non-empty string", reference.SourceField)
	}
	return assignmentPolicyEligibilitySelectorInput{reference: &assignmentPolicyEligibilityReferenceKey{
		target: assignmentPolicyEligibilityReferenceTarget{
			object:      strings.TrimSpace(reference.TargetObject),
			field:       strings.TrimSpace(reference.TargetField),
			table:       strings.TrimSpace(reference.ResolvedTargetTable),
			idColumn:    strings.TrimSpace(reference.ResolvedTargetIDColumn),
			fieldColumn: strings.TrimSpace(reference.ResolvedTargetFieldColumn),
		},
		id: id,
	}}, nil
}

func readAssignmentPolicyEligibilityReferenceSelectors(
	ctx context.Context,
	tx *sql.Tx,
	references map[assignmentPolicyEligibilityReferenceTarget]map[string]struct{},
) (map[assignmentPolicyEligibilityReferenceKey]string, error) {
	targets := make([]assignmentPolicyEligibilityReferenceTarget, 0, len(references))
	for target := range references {
		targets = append(targets, target)
	}
	sort.Slice(targets, func(i, j int) bool {
		if targets[i].table != targets[j].table {
			return targets[i].table < targets[j].table
		}
		if targets[i].idColumn != targets[j].idColumn {
			return targets[i].idColumn < targets[j].idColumn
		}
		return targets[i].fieldColumn < targets[j].fieldColumn
	})
	resolved := make(map[assignmentPolicyEligibilityReferenceKey]string)
	for _, target := range targets {
		ids := make([]string, 0, len(references[target]))
		for id := range references[target] {
			ids = append(ids, id)
		}
		sort.Strings(ids)
		placeholders := make([]string, len(ids))
		arguments := make([]any, len(ids))
		for i, id := range ids {
			placeholders[i] = "?"
			arguments[i] = id
		}
		query := fmt.Sprintf(
			"SELECT `%s`, `%s` FROM `%s` WHERE `%s` IN (%s) ORDER BY `%s` ASC FOR UPDATE",
			target.idColumn,
			target.fieldColumn,
			target.table,
			target.idColumn,
			strings.Join(placeholders, ", "),
			target.idColumn,
		)
		rows, err := tx.QueryContext(ctx, query, arguments...)
		if err != nil {
			return nil, fmt.Errorf("resolve assignment-policy eligibility selectors from %s.%s: %w", target.object, target.field, err)
		}
		for rows.Next() {
			var id, selector string
			if err := rows.Scan(&id, &selector); err != nil {
				rows.Close()
				return nil, fmt.Errorf("scan assignment-policy eligibility selector from %s.%s: %w", target.object, target.field, err)
			}
			key := assignmentPolicyEligibilityReferenceKey{target: target, id: id}
			if id == "" || strings.TrimSpace(selector) == "" {
				rows.Close()
				return nil, fmt.Errorf("assignment-policy eligibility selector row in %s.%s is not canonical", target.object, target.field)
			}
			if _, duplicate := resolved[key]; duplicate {
				rows.Close()
				return nil, fmt.Errorf("assignment-policy eligibility selector row %s in %s is duplicated", id, target.object)
			}
			resolved[key] = selector
		}
		if err := rows.Err(); err != nil {
			rows.Close()
			return nil, fmt.Errorf("read assignment-policy eligibility selectors from %s.%s: %w", target.object, target.field, err)
		}
		rows.Close()
		for _, id := range ids {
			key := assignmentPolicyEligibilityReferenceKey{target: target, id: id}
			if _, found := resolved[key]; !found {
				return nil, fmt.Errorf("assignment-policy eligibility reference %s.%s %s is missing", target.object, target.field, id)
			}
		}
	}
	return resolved, nil
}

func mapAssignmentPolicyEligibilityLifecycleMutation(
	candidate assignmentPolicyEligibilityLifecycleEvent,
	resolved map[assignmentPolicyEligibilityReferenceKey]string,
) (AssignmentPolicyEligibilityMutation, error) {
	recordID := strings.TrimSpace(candidate.event.Event.RecordID)
	if recordID == "" {
		return AssignmentPolicyEligibilityMutation{}, fmt.Errorf("assignment-policy eligibility lifecycle requires a record ID")
	}
	objectKey, err := catalogassignmentidentityv1.NormalizeObjectAPIName(candidate.event.Event.ObjectName)
	if err != nil {
		return AssignmentPolicyEligibilityMutation{}, fmt.Errorf("assignment-policy eligibility lifecycle requires a valid Catalog object API name: %w", err)
	}
	selectors := func(input assignmentPolicyEligibilitySelectorInput) ([]string, error) {
		if input.direct != "" {
			return []string{input.direct}, nil
		}
		if input.reference == nil {
			return nil, nil
		}
		selector, found := resolved[*input.reference]
		if !found {
			return nil, fmt.Errorf("assignment-policy eligibility reference selector was not resolved")
		}
		return []string{selector}, nil
	}
	before, err := selectors(candidate.before)
	if err != nil {
		return AssignmentPolicyEligibilityMutation{}, err
	}
	after, err := selectors(candidate.after)
	if err != nil {
		return AssignmentPolicyEligibilityMutation{}, err
	}
	return AssignmentPolicyEligibilityMutation{
		SourceIdentity: objectKey.NormalizedAPIName + "/" + recordID,
		Before:         AssignmentPolicyEligibilityState{Selectors: before},
		After:          AssignmentPolicyEligibilityState{Selectors: after},
		// A subject-population row can change the projected subject identity
		// without changing selector membership. That transition must reconcile
		// the exact target population even when Before/After selectors are byte-
		// identical; L1 configuration cannot opt out of this correctness rule.
		ReconcileAfterSelectors: candidate.event.Phase == contract.RecordLifecycleAfterUpdateInTx &&
			len(after) != 0 &&
			(candidate.config.Role == models.AssignmentPolicyEligibilitySubjectPopulation ||
				candidate.config.ReconcileAfterUpdate),
	}, nil
}
