package records

import (
	"context"
	"errors"
	"strings"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordattachmentsv1"
)

// RuntimeAttachmentSourceEngine is the existing CRUD/RLS/FLS owner used by
// Runtime. It is deliberately an in-process seam, not a new contract.Deps
// plane. The engine retrieves the current parent row after product parsing so
// source values can be proven against FLS-authorized bytes.
type RuntimeAttachmentSourceEngine interface {
	Retrieve(context.Context, string, string, *models.UserSession) (models.Record, error)
	FieldReadable(context.Context, string, string, *models.UserSession) (bool, error)
}

type runtimeAttachmentSourcePersistence struct{ service *PersistenceService }

func (p runtimeAttachmentSourcePersistence) Retrieve(ctx context.Context, object, id string, user *models.UserSession) (models.Record, error) {
	return p.service.Retrieve(ctx, object, id, user)
}

func (p runtimeAttachmentSourcePersistence) FieldReadable(ctx context.Context, object, field string, user *models.UserSession) (bool, error) {
	if constants.GetSecurityContext(ctx).BypassesCRUDandFLS() || constants.ScopedInternalAccessBypassesFLS(ctx, object, constants.PermRead) {
		return true, nil
	}
	return p.service.isReadableUserField(ctx, object, field, user)
}

// RuntimeAttachmentSourceMetadata exposes only the request-bound Catalog view.
// It intentionally offers no global schema fallback: a source resolution has
// no serving authority without the immutable view chosen by ServingAdmission.
type RuntimeAttachmentSourceMetadata interface {
	BoundRuntimeMetadataView(context.Context) (*catalogsnapshot.RuntimeMetadataView, bool)
}

// RuntimeAttachmentSourceResolver is the files/persistence continuation after
// this service has checked every product-parsed source value against the
// current FLS-authorized parent record.
type RuntimeAttachmentSourceResolver interface {
	ResolveAttachmentSources(context.Context, *catalogsnapshot.RuntimeMetadataView, string, string, catalogsnapshot.RecordAttachmentSourceResolver, []string, *models.UserSession) ([]semantic.SourceResult, error)
}

type RuntimeAttachmentSourceConfig struct {
	Engine   RuntimeAttachmentSourceEngine
	Metadata RuntimeAttachmentSourceMetadata
	Resolver RuntimeAttachmentSourceResolver
}

// RuntimeAttachmentSourceService owns field provenance. It accepts only
// product-parsed strings plus offsets, proves them in the current authorized
// parent record, then delegates a Catalog-closed batch lookup to files. Bad
// bindings, FLS omissions, missing/RLS-denied parents, and byte mismatches are
// all per-item UNAVAILABLE results and never reach source lookup.
type RuntimeAttachmentSourceService struct {
	engine   RuntimeAttachmentSourceEngine
	metadata RuntimeAttachmentSourceMetadata
	resolver RuntimeAttachmentSourceResolver
}

func NewRuntimeAttachmentSourceService(config RuntimeAttachmentSourceConfig) (*RuntimeAttachmentSourceService, error) {
	if config.Engine == nil || config.Metadata == nil || config.Resolver == nil {
		return nil, errors.New("runtime attachment source service requires engine, metadata and resolver")
	}
	return &RuntimeAttachmentSourceService{engine: config.Engine, metadata: config.Metadata, resolver: config.Resolver}, nil
}

func NewRuntimeAttachmentSourceServiceFromPersistence(
	persistence *PersistenceService,
	metadata RuntimeAttachmentSourceMetadata,
	resolver RuntimeAttachmentSourceResolver,
) (*RuntimeAttachmentSourceService, error) {
	if persistence == nil {
		return nil, errors.New("runtime attachment source service requires persistence")
	}
	return NewRuntimeAttachmentSourceService(RuntimeAttachmentSourceConfig{
		Engine: runtimeAttachmentSourcePersistence{service: persistence}, Metadata: metadata, Resolver: resolver,
	})
}

func (s *RuntimeAttachmentSourceService) ResolveAttachmentSources(ctx context.Context, request semantic.ResolveRequest, user *models.UserSession) (results []semantic.SourceResult, err error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	// The wire mapper enforces this public semantic bound before it reaches
	// Runtime. Keep the same guard at the in-process owner boundary so a future
	// caller cannot allocate an unbounded result vector by bypassing transport.
	if len(request.Sources) > semantic.MaxSources {
		return nil, &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitOperationCount, Maximum: semantic.MaxSources}
	}
	// Cancellation is never an authorization outcome. The ordinary unavailable
	// result intentionally masks record and field facts, but it must not turn a
	// canceled request into a successful semantic response while a dependency is
	// returning. Keep that invariant at this owner boundary for every normal
	// return, including cancellation which races a successful dependency result.
	defer func() {
		if cancellationErr := attachmentSourceCancellation(ctx, err); cancellationErr != nil {
			results, err = nil, cancellationErr
		}
	}()
	results = unavailableAttachmentSourceResults(len(request.Sources))
	if s == nil || s.engine == nil || s.metadata == nil || s.resolver == nil {
		return results, nil
	}
	view, ok := s.metadata.BoundRuntimeMetadataView(ctx)
	if !ok || !view.SupportsRecordViewServing() {
		return results, nil
	}
	parentSchema, ok := view.Object(request.ParentObjectID)
	if !ok || parentSchema == nil {
		return results, nil
	}
	resolver, ok := view.AttachmentSourceResolver(request.ResolverID)
	if !ok {
		return results, nil
	}
	bindings := attachmentSourceBindings(view, parentSchema.APIName, resolver.ResolverID)
	if len(bindings) == 0 {
		return results, nil
	}

	// Check FLS before loading the parent. A hidden field cannot reveal a
	// matching row, and no valid source from it is passed to the files layer.
	readable := make(map[string]bool, len(bindings))
	for _, source := range request.Sources {
		fieldID, bound := bindings[strings.ToLower(source.SourceFieldID)]
		if !bound {
			continue
		}
		key := strings.ToLower(fieldID)
		if _, checked := readable[key]; checked {
			continue
		}
		allowed, err := s.engine.FieldReadable(ctx, parentSchema.APIName, fieldID, user)
		if err != nil {
			if cancellationErr := attachmentSourceCancellation(ctx, err); cancellationErr != nil {
				return nil, cancellationErr
			}
			// This continuation deliberately has no existence/field oracle. An
			// FLS lookup failure is indistinguishable from unavailable proof.
			return results, nil
		}
		readable[key] = allowed
	}
	if !anyAttachmentSourceFieldReadable(readable) {
		return results, nil
	}

	parent, err := s.engine.Retrieve(ctx, parentSchema.APIName, request.ParentRecordID, user)
	if err != nil || parent == nil {
		if cancellationErr := attachmentSourceCancellation(ctx, err); cancellationErr != nil {
			return nil, cancellationErr
		}
		// CRUD denial, RLS masking, missing parents, and repository-level lookup
		// failures all remain opaque to the source-resolution caller.
		return results, nil
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}

	validIndexes := make([]int, 0, len(request.Sources))
	validValues := make([]string, 0, len(request.Sources))
	for index, source := range request.Sources {
		fieldID, bound := bindings[strings.ToLower(source.SourceFieldID)]
		if !bound || !readable[strings.ToLower(fieldID)] {
			continue
		}
		raw, ok := parent[fieldID].(string)
		if !ok || !attachmentSourceProofMatches(raw, source.SourceValueOffset, source.SourceValue) {
			continue
		}
		validIndexes = append(validIndexes, index)
		validValues = append(validValues, source.SourceValue)
	}
	if len(validValues) == 0 {
		return results, nil
	}

	resolved, err := s.resolver.ResolveAttachmentSources(ctx, view, parentSchema.APIName, request.ParentRecordID, resolver, validValues, user)
	if err != nil {
		if cancellationErr := attachmentSourceCancellation(ctx, err); cancellationErr != nil {
			return nil, cancellationErr
		}
		return results, nil
	}
	if len(resolved) != len(validIndexes) {
		return nil, &semantic.Failure{Category: semantic.FailureInternal}
	}
	for resultIndex, result := range resolved {
		if result.Index != uint32(resultIndex) || !result.Outcome.Valid() ||
			(result.Outcome != semantic.SourceResolved && (result.FileID != "" || result.FileName != "")) ||
			(result.Outcome == semantic.SourceResolved && (strings.TrimSpace(result.FileID) == "" || result.FileName == "")) {
			return nil, &semantic.Failure{Category: semantic.FailureInternal}
		}
		result.Index = uint32(validIndexes[resultIndex])
		results[validIndexes[resultIndex]] = result
	}
	return results, nil
}

// attachmentSourceCancellation distinguishes the request-lifecycle outcomes
// from opaque authorization and availability failures. Callers may mask the
// latter as SourceUnavailable, but never the former.
func attachmentSourceCancellation(ctx context.Context, cause error) error {
	if ctxErr := ctx.Err(); ctxErr != nil {
		return ctxErr
	}
	if errors.Is(cause, context.Canceled) {
		return context.Canceled
	}
	if errors.Is(cause, context.DeadlineExceeded) {
		return context.DeadlineExceeded
	}
	return nil
}

func attachmentSourceBindings(view *catalogsnapshot.RuntimeMetadataView, objectID, resolverID string) map[string]string {
	bindings := view.AttachmentSourceFieldsForObject(objectID)
	result := make(map[string]string, len(bindings))
	for _, binding := range bindings {
		if binding.ResolverID == resolverID {
			result[strings.ToLower(binding.FieldAPIName)] = binding.FieldAPIName
		}
	}
	return result
}

func anyAttachmentSourceFieldReadable(readable map[string]bool) bool {
	for _, value := range readable {
		if value {
			return true
		}
	}
	return false
}

func attachmentSourceProofMatches(raw string, offset uint32, sourceValue string) bool {
	start := uint64(offset)
	end := start + uint64(len(sourceValue))
	if start > uint64(len(raw)) || end > uint64(len(raw)) {
		return false
	}
	return raw[int(start):int(end)] == sourceValue
}

func unavailableAttachmentSourceResults(count int) []semantic.SourceResult {
	result := make([]semantic.SourceResult, count)
	for index := range result {
		result[index] = semantic.SourceResult{Index: uint32(index), Outcome: semantic.SourceUnavailable}
	}
	return result
}
