package runtimeapi

import (
	"context"
	"errors"
	"net/http"

	"github.com/nexuscrm/kernel/pkg/telemetry"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinmutationv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

// RecycleBinMutator is the existing neutral records-engine mutation seam.
// Runtime owns transport and admission; the records engine remains the sole
// owner of restore and purge transaction semantics.
type RecycleBinMutator interface {
	RestoreRecycleBinRecord(context.Context, string, *models.UserSession) error
	PurgeRecycleBinRecord(context.Context, string, *models.UserSession) error
}

type RecycleBinMutationServer struct {
	runtimev1.UnimplementedRecycleBinMutationServiceServer
	mutator  RecycleBinMutator
	receipts ReceiptSource
	metrics  *recordsMetrics
}

func NewRecycleBinMutationServer(mutator RecycleBinMutator, receipts ReceiptSource, metrics *recordsMetrics) (*RecycleBinMutationServer, error) {
	if mutator == nil {
		return nil, errors.New("runtime-api: recycle-bin mutator is required")
	}
	if metrics == nil {
		metrics = newRecordsMetrics(nil)
	}
	return &RecycleBinMutationServer{mutator: mutator, receipts: receipts, metrics: metrics}, nil
}

func (s *RecycleBinMutationServer) RestoreRecycleBin(ctx context.Context, wireRequest *runtimev1.RestoreRecycleBinRequest) (*runtimev1.RestoreRecycleBinResponse, error) {
	trusted, ok := trustedCallFromContext(ctx)
	if !ok {
		return nil, mapping.ToRecycleBinMutationStatusError(&semantic.Failure{Category: semantic.FailureUnauthenticated})
	}
	request, err := mapping.FromWireRestoreRecycleBinRequest(ctx, wireRequest)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	receipts, err := s.prepare(ctx, request)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	repositoryCtx, sqlCounter := telemetry.WithSQLStatementCounter(ctx)
	err = s.mutator.RestoreRecycleBinRecord(repositoryCtx, request.RecordID, userSessionPointer(trusted))
	s.metrics.observeRepository(recordsOperationRecycleBinRestore, sqlCounter.Count())
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(classifyRecycleBinMutationEngineError(ctx, err))
	}
	response := semantic.Response{Receipts: receipts}
	wireResponse, err := mapping.ToWireRestoreRecycleBinResponse(request, response)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	return wireResponse, nil
}

func (s *RecycleBinMutationServer) PurgeRecycleBin(ctx context.Context, wireRequest *runtimev1.PurgeRecycleBinRequest) (*runtimev1.PurgeRecycleBinResponse, error) {
	trusted, ok := trustedCallFromContext(ctx)
	if !ok {
		return nil, mapping.ToRecycleBinMutationStatusError(&semantic.Failure{Category: semantic.FailureUnauthenticated})
	}
	request, err := mapping.FromWirePurgeRecycleBinRequest(ctx, wireRequest)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	receipts, err := s.prepare(ctx, request)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	repositoryCtx, sqlCounter := telemetry.WithSQLStatementCounter(ctx)
	err = s.mutator.PurgeRecycleBinRecord(repositoryCtx, request.RecordID, userSessionPointer(trusted))
	s.metrics.observeRepository(recordsOperationRecycleBinPurge, sqlCounter.Count())
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(classifyRecycleBinMutationEngineError(ctx, err))
	}
	response := semantic.Response{Receipts: receipts}
	wireResponse, err := mapping.ToWirePurgeRecycleBinResponse(request, response)
	if err != nil {
		return nil, mapping.ToRecycleBinMutationStatusError(err)
	}
	return wireResponse, nil
}

func (s *RecycleBinMutationServer) prepare(ctx context.Context, request semantic.Request) (semantic.Receipts, error) {
	receipts, err := recycleBinExecutionReceipts(ctx, s.receipts)
	if err != nil {
		return semantic.Receipts{}, err
	}
	receipts = receiptsForCapability(receipts, semantic.CapabilityName)
	if request.MinCatalogRevision != nil && *request.MinCatalogRevision > receipts.CatalogRevision {
		return semantic.Receipts{}, &semantic.Failure{
			Category: semantic.FailureStaleState, Receipts: &receipts,
			StaleAxis: semantic.StaleAxisCatalog, RequiredRevision: *request.MinCatalogRevision,
			ServedRevision: receipts.CatalogRevision,
		}
	}
	mutationView, ok := RuntimeMutationMetadataViewFromContext(ctx)
	if !ok || mutationView.Profile() != catalogsnapshot.ProjectionRecordMutationV1 ||
		mutationView.CapabilityID() != mutationsemantic.CapabilityName {
		return semantic.Receipts{}, &semantic.Failure{Category: semantic.FailureUnavailable}
	}
	return receipts, nil
}

func userSessionPointer(trusted TrustedCall) *models.UserSession {
	user := trusted.UserSession()
	return &user
}

func classifyRecycleBinMutationEngineError(ctx context.Context, err error) error {
	if contextFailure, owned := classifyEngineContextOwnership(ctx, err); owned {
		return contextFailure
	}
	switch sharedErrors.ClassifyInfrastructure(err) {
	case sharedErrors.InfrastructureConditionRetriable:
		failure, _ := classifyRetriableEngineCondition(err)
		return failure
	case sharedErrors.InfrastructureConditionInternal:
		return &semantic.Failure{Category: semantic.FailureInternal}
	}
	for current := err; current != nil; current = errors.Unwrap(current) {
		if _, joined := current.(interface{ Unwrap() []error }); joined {
			return &semantic.Failure{Category: semantic.FailureInternal}
		}
	}
	if sharedErrors.IsConflict(err) {
		return &semantic.Failure{Category: semantic.FailureConflict}
	}
	var statuser interface{ HTTPStatus() int }
	if errors.As(err, &statuser) && statuser.HTTPStatus() == http.StatusConflict {
		return &semantic.Failure{Category: semantic.FailureConflict}
	}
	return classifyPlatformEngineError(ctx, err, semantic.InvalidComponentOperation)
}
