package files

import (
	"context"
	"errors"
	"strings"

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

// RuntimeAttachmentSourceLookup is the one batch persistence continuation
// selected by a Catalog-closed resolver plan.
type RuntimeAttachmentSourceLookup interface {
	Lookup(context.Context, persistence.RecordAttachmentSourcePlan, string, string, []string) (map[string]string, error)
}

// RuntimeAttachmentSourceResolver performs only the generic source-row and
// protected-file half of attachment resolution. The records Runtime proves
// source-field FLS visibility and exact parent-field bytes before this seam is
// reachable; this resolver rechecks parent/file authorization before returning
// any file fact.
type RuntimeAttachmentSourceResolver struct {
	lookup     RuntimeAttachmentSourceLookup
	authorizer *recordAttachmentMetadataAuthorizer
}

func NewRuntimeAttachmentSourceResolver(
	lookup RuntimeAttachmentSourceLookup,
	records fileRecordStore,
	permissions filePermissionChecker,
	metadataService *metadata.MetadataService,
) (*RuntimeAttachmentSourceResolver, error) {
	if lookup == nil || records == nil || permissions == nil || metadataService == nil {
		return nil, errors.New("runtime attachment source resolver requires lookup, records, permissions and metadata")
	}
	// This narrow authorizer uses only existing metadata/file authorization
	// operations. It intentionally has no blob-store dependency: source
	// resolution returns metadata only and never opens file contents.
	return &RuntimeAttachmentSourceResolver{
		lookup: lookup,
		authorizer: &recordAttachmentMetadataAuthorizer{
			records: records, permissions: permissions, metadata: metadataService,
		},
	}, nil
}

func (r *RuntimeAttachmentSourceResolver) ResolveAttachmentSources(
	ctx context.Context,
	view *catalogsnapshot.RuntimeMetadataView,
	parentObjectID string,
	parentRecordID string,
	resolver catalogsnapshot.RecordAttachmentSourceResolver,
	sourceValues []string,
	user *models.UserSession,
) (results []semantic.SourceResult, err error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	// SourceUnavailable masks protected-file authorization facts. It must never
	// mask request cancellation or expiry, including a cancellation that races a
	// successful authorization step.
	defer func() {
		if cancellationErr := attachmentSourceCancellation(ctx, err); cancellationErr != nil {
			results, err = nil, cancellationErr
		}
	}()
	if r == nil || r.lookup == nil || r.authorizer == nil || view == nil ||
		view.Profile() != catalogsnapshot.ProjectionRecordViewV1 ||
		resolver.Kind != catalogsnapshot.RecordAttachmentSourceByRecordV2 {
		return nil, &semantic.Failure{Category: semantic.FailureUnavailable}
	}
	// This exported seam accepts a resolver value only as an opaque handle
	// supplied by the records owner. Re-read and compare it against the bound
	// immutable view before it can become a persistence plan: a copied or
	// fabricated struct must not select a relation that Catalog did not publish.
	declaredResolver, declared := view.AttachmentSourceResolver(resolver.ResolverID)
	if !declared || !sameAttachmentSourceResolver(resolver, declaredResolver) {
		return nil, &semantic.Failure{Category: semantic.FailureUnavailable}
	}
	plan, err := runtimeAttachmentSourcePlan(view, resolver)
	if err != nil {
		return nil, &semantic.Failure{Category: semantic.FailureUnavailable}
	}
	// Defense in depth for this package boundary. The records owner has already
	// checked the current parent row and its field proof, but no source query may
	// run until the established neutral parent access check succeeds as well.
	if err := r.authorizer.authorizeParentAccess(ctx, parentObjectID, parentRecordID, user); err != nil {
		return nil, err
	}
	fileIDs, err := r.lookup.Lookup(ctx, plan, parentObjectID, parentRecordID, sourceValues)
	if err != nil {
		return nil, err
	}

	results = make([]semantic.SourceResult, len(sourceValues))
	for index, sourceValue := range sourceValues {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		results[index] = semantic.SourceResult{Index: uint32(index), Outcome: semantic.SourceNotFound}
		fileID := strings.TrimSpace(fileIDs[sourceValue])
		if fileID == "" {
			continue
		}
		file, found, authorizationErr := r.authorizer.authorizeRecordAttachmentIdentity(ctx, fileID, user)
		if cancellationErr := attachmentSourceCancellation(ctx, authorizationErr); cancellationErr != nil {
			return nil, cancellationErr
		}
		if authorizationErr != nil || !found || file == nil || strings.TrimSpace(file.FileID) == "" {
			// Do not reveal whether source lookup succeeded but protected file
			// authorization failed.
			results[index].Outcome = semantic.SourceUnavailable
			continue
		}
		name := strings.TrimSpace(file.FileName)
		if name == "" {
			name = strings.TrimSpace(file.FileID)
		}
		results[index] = semantic.SourceResult{
			Index: uint32(index), Outcome: semantic.SourceResolved,
			FileID: strings.TrimSpace(file.FileID), FileName: name,
		}
	}
	return results, nil
}

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 sameAttachmentSourceResolver(left, right catalogsnapshot.RecordAttachmentSourceResolver) bool {
	if left.ResolverID != right.ResolverID || left.Kind != right.Kind ||
		left.SourceObjectAPIName != right.SourceObjectAPIName ||
		left.SourceFileReferenceFieldAPIName != right.SourceFileReferenceFieldAPIName ||
		left.SourceDeletedFieldAPIName != right.SourceDeletedFieldAPIName {
		return false
	}
	if left.SourceRecordMatcher == nil || right.SourceRecordMatcher == nil {
		return left.SourceRecordMatcher == nil && right.SourceRecordMatcher == nil
	}
	if left.SourceRecordMatcher.Kind != right.SourceRecordMatcher.Kind ||
		left.SourceRecordMatcher.LengthBytes != right.SourceRecordMatcher.LengthBytes {
		return false
	}
	if left.SourceRecordMatcher.StartOffsetBytes == nil || right.SourceRecordMatcher.StartOffsetBytes == nil {
		return left.SourceRecordMatcher.StartOffsetBytes == nil && right.SourceRecordMatcher.StartOffsetBytes == nil
	}
	return *left.SourceRecordMatcher.StartOffsetBytes == *right.SourceRecordMatcher.StartOffsetBytes
}

func runtimeAttachmentSourcePlan(view *catalogsnapshot.RuntimeMetadataView, resolver catalogsnapshot.RecordAttachmentSourceResolver) (persistence.RecordAttachmentSourcePlan, error) {
	if resolver.SourceRecordMatcher == nil {
		return persistence.RecordAttachmentSourcePlan{}, errors.New("attachment source resolver has no matcher")
	}
	sourceSchema, ok := view.Object(resolver.SourceObjectAPIName)
	if !ok || sourceSchema == nil ||
		models.FindField(sourceSchema, resolver.SourceFileReferenceFieldAPIName) == nil ||
		(strings.TrimSpace(resolver.SourceDeletedFieldAPIName) != "" && models.FindField(sourceSchema, resolver.SourceDeletedFieldAPIName) == nil) {
		return persistence.RecordAttachmentSourcePlan{}, errors.New("attachment source resolver references an unavailable source schema")
	}
	plan := persistence.RecordAttachmentSourcePlan{
		SourceObject: sourceSchema, FileReferenceField: resolver.SourceFileReferenceFieldAPIName,
		DeletedField: resolver.SourceDeletedFieldAPIName,
	}
	switch resolver.SourceRecordMatcher.Kind {
	case catalogsnapshot.RecordAttachmentSourceMatchRecordIDV1:
		return plan, nil
	case catalogsnapshot.RecordAttachmentSourceMatchRecordIDSegmentV2:
		if resolver.SourceRecordMatcher.StartOffsetBytes == nil {
			return persistence.RecordAttachmentSourcePlan{}, errors.New("attachment source segment matcher has no start")
		}
		plan.IdentifierSegment = &persistence.RecordAttachmentSourceIdentifierSegment{
			StartOffsetBytes: *resolver.SourceRecordMatcher.StartOffsetBytes,
			LengthBytes:      resolver.SourceRecordMatcher.LengthBytes,
		}
		return plan, nil
	default:
		return persistence.RecordAttachmentSourcePlan{}, errors.New("attachment source resolver has no serving matcher")
	}
}
