package services

import (
	"context"
	"errors"
	"strings"
	"time"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
)

// rollupRecalculateDeadline is when a fenced maintenance request must stop
// starting new parents.
//
// It is deliberately the zero instant — no time bound — for now. Bounding the
// server before a dialect can report a partial scan would be worse than the
// problem it solves: the response is byte-identical to a full scan, so a
// consumer that cannot read Complete would take a truncated pass for a
// finished one and stop healing an object that is still drifted. Today's
// all-or-nothing behavior fails visibly instead. The bound turns on in the
// same change that pins a dialect able to express partial completion, and it
// must be computed HERE, at request entry, rather than inside the fence: the
// callback runs only after the control-plane apply lock is acquired, and that
// wait belongs inside the request's window, not on top of it.
func rollupRecalculateDeadline() time.Time { return time.Time{} }

// rollupRecalculateBounds maps the wire request onto the engine's bounds.
func rollupRecalculateBounds(request contract.RollupRecalculateRequest, deadline time.Time) ports.RollupRecalculateBounds {
	return ports.RollupRecalculateBounds{
		AfterID:    strings.TrimSpace(request.AfterID),
		MaxParents: request.MaxParents,
		Deadline:   deadline,
	}
}

// rollupRecalculateResult maps the engine's progress back onto the wire result.
func rollupRecalculateResult(objectName string, progress ports.RollupRecalculateProgress) *contract.RollupRecalculateResult {
	return &contract.RollupRecalculateResult{
		Object:       objectName,
		Recalculated: progress.Recalculated,
		NextAfterID:  progress.NextAfterID,
		Complete:     progress.Complete,
	}
}

// RollupMaintenanceFenceResult keeps callback-owned failures separate from
// failures of the serving/apply fence itself. OperationErr carries an exact
// precondition or mutation error; FenceErr is reserved for failures that make
// the mutation's fencing unavailable or uncertain.
type RollupMaintenanceFenceResult struct {
	OperationErr error
	FenceErr     error
}

// RollupMaintenanceFence runs one maintenance mutation only after validating
// that the caller's opaque serving attestation still identifies this process
// and its active app-pack state. The composition root supplies the deployment
// fence and must return callback and fence failures through their distinct
// channels.
type RollupMaintenanceFence func(
	ctx context.Context,
	expectedServingAttestation string,
	mutation func(context.Context) error,
) RollupMaintenanceFenceResult

func (sm *ServiceManager) RollupMaintenanceContract() contract.RollupMaintenance {
	if sm == nil {
		return nil
	}
	if sm.NeutralRollupMaintenance != nil {
		return sm.NeutralRollupMaintenance
	}
	if sm.Rollup == nil {
		return nil
	}
	return sm
}

func splitCommaFields(raw string) []string {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	fields := make([]string, 0, len(parts))
	for _, part := range parts {
		field := strings.TrimSpace(part)
		if field == "" {
			continue
		}
		fields = append(fields, field)
	}
	if len(fields) == 0 {
		return nil
	}
	return fields
}

func (sm *ServiceManager) RecalculateRollups(ctx context.Context, request contract.RollupRecalculateRequest) (*contract.RollupRecalculateResult, error) {
	if sm == nil || sm.Rollup == nil {
		return nil, &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorUnavailable,
			Message: "rollup service not initialized",
		}
	}
	objectName := strings.TrimSpace(request.Object)
	if objectName == "" {
		return nil, &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorInvalidRequest,
			Message: "object is required",
		}
	}
	if request.ExpectedServingAttestation == "" {
		return nil, &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorPreconditionRequired,
			Message: "serving attestation is required for rollup maintenance",
		}
	}
	// Request validation precedes availability checks: a malformed bound is
	// malformed whether or not the fence happens to be wired.
	if request.MaxParents < 0 {
		return nil, &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorInvalidRequest,
			Message: "maxParents must not be negative",
		}
	}
	if sm.deps.RollupMaintenanceFence == nil {
		return nil, &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorUnavailable,
			Message: "rollup maintenance serving fence is unavailable",
		}
	}

	bounds := rollupRecalculateBounds(request, rollupRecalculateDeadline())

	var progress ports.RollupRecalculateProgress
	result := sm.deps.RollupMaintenanceFence(
		ctx,
		request.ExpectedServingAttestation,
		func(fencedContext context.Context) error {
			var recalculateErr error
			progress, recalculateErr = sm.Rollup.RecalculateAll(fencedContext, objectName, bounds)
			return recalculateErr
		},
	)
	err := rollupMaintenanceFenceResultError(result)
	if err != nil {
		return nil, err
	}
	return rollupRecalculateResult(objectName, progress), nil
}

func rollupMaintenanceFenceResultError(result RollupMaintenanceFenceResult) error {
	if result.FenceErr != nil {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorUnavailable,
			Message: "rollup maintenance serving fence is unavailable",
			Cause:   errors.Join(result.OperationErr, result.FenceErr),
		}
	}
	return rollupMaintenanceError(result.OperationErr)
}

func rollupMaintenanceError(err error) error {
	if err == nil {
		return nil
	}
	var maintenanceErr *contract.RollupMaintenanceError
	if errors.As(err, &maintenanceErr) {
		return maintenanceErr
	}
	if appErrors.IsInternal(err) {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorInternal,
			Message: "An internal server error occurred. Contact your system administrator.",
			Cause:   err,
		}
	}
	var lockErr *appErrors.LockContentionError
	if errors.As(err, &lockErr) {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorLockContention,
			Message: lockErr.Error(),
			Cause:   err,
		}
	}
	var permissionErr *appErrors.PermissionError
	if errors.As(err, &permissionErr) {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorPermission,
			Message: permissionErr.Error(),
			Cause:   err,
		}
	}
	var notFound *appErrors.NotFoundError
	if errors.As(err, &notFound) {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorNotFound,
			Message: err.Error(),
			Cause:   err,
		}
	}
	var domainWire *sharedErrors.DomainWireError
	if errors.As(err, &domainWire) {
		return &contract.RollupMaintenanceError{
			Kind:    contract.RollupMaintenanceErrorDomainWire,
			Message: domainWire.Message,
			Fields:  append([]string(nil), domainWire.Fields...),
			Cause:   err,
		}
	}
	if leaves, ok := appErrors.AsClientValidationErrors(err); ok {
		return rollupMaintenanceClientValidationError(err, leaves)
	}
	return &contract.RollupMaintenanceError{
		Kind:    contract.RollupMaintenanceErrorInvalidRequest,
		Message: err.Error(),
		Cause:   err,
	}
}

func rollupMaintenanceClientValidationError(cause error, leaves []error) *contract.RollupMaintenanceError {
	kind := contract.RollupMaintenanceErrorKind("")
	messages := make([]string, 0, len(leaves))
	fields := make([]string, 0, len(leaves))
	for _, leaf := range leaves {
		leafKind, message, leafFields := rollupMaintenanceClientValidationLeaf(leaf)
		kind = combineRollupMaintenanceClientValidationKinds(kind, leafKind)
		if message = strings.TrimSpace(message); message != "" {
			messages = append(messages, message)
		}
		for _, field := range leafFields {
			if field = strings.TrimSpace(field); field != "" {
				fields = append(fields, field)
			}
		}
	}
	if kind == "" {
		kind = contract.RollupMaintenanceErrorInvalidRequest
	}
	message := strings.Join(messages, "; ")
	if message == "" && cause != nil {
		message = cause.Error()
	}
	return &contract.RollupMaintenanceError{
		Kind:    kind,
		Message: message,
		Fields:  fields,
		Cause:   cause,
	}
}

func rollupMaintenanceClientValidationLeaf(leaf error) (contract.RollupMaintenanceErrorKind, string, []string) {
	if failures, ok := appErrors.AsValidationRuleFailures(leaf); ok {
		messages := make([]string, 0, len(failures))
		fields := make([]string, 0, len(failures))
		for _, failure := range failures {
			if message := strings.TrimSpace(failure.Message); message != "" {
				messages = append(messages, message)
			}
			if field := strings.TrimSpace(failure.Field); field != "" {
				fields = append(fields, field)
			}
		}
		return contract.RollupMaintenanceErrorValidationRule, strings.Join(messages, "; "), fields
	}
	if fields, message, ok := sharedErrors.FieldWriteDeniedDetails(leaf); ok {
		return contract.RollupMaintenanceErrorFieldDenied, message, fields
	}
	if field, message, ok := sharedErrors.ValidationDetails(leaf); ok {
		return contract.RollupMaintenanceErrorValidation, message, splitCommaFields(field)
	}
	if leaf == nil {
		return contract.RollupMaintenanceErrorInvalidRequest, "", nil
	}
	return contract.RollupMaintenanceErrorInvalidRequest, leaf.Error(), nil
}

func combineRollupMaintenanceClientValidationKinds(current, next contract.RollupMaintenanceErrorKind) contract.RollupMaintenanceErrorKind {
	if current == "" || current == next {
		return next
	}
	if (current == contract.RollupMaintenanceErrorValidation || current == contract.RollupMaintenanceErrorValidationRule) &&
		(next == contract.RollupMaintenanceErrorValidation || next == contract.RollupMaintenanceErrorValidationRule) {
		return contract.RollupMaintenanceErrorValidation
	}
	return contract.RollupMaintenanceErrorInvalidRequest
}
