package orchestrator

import (
	"context"

	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oobs"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/pquery"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/storage"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	msg "github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

type v2MongoDBProvider interface {
	MongoDatabase() *mongo.Database
}

type v2RuntimeDeps struct {
	LedgerClient            ledgerclient.Client
	GatewayInvokeResolver   GatewayInvokeResolver
	GatewayRegistry         GatewayRegistry
	CardGatewayRoutes       map[string]CardGatewayRoute
	BatchOptimizationPolicy psvc.BatchOptimizationPolicy
	Producer                msg.Producer
}

func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, runtimeDeps v2RuntimeDeps) (psvc.Service, prepo.Repository, error) {
	if logger == nil {
		logger = zap.NewNop()
	}
	if repo == nil {
		return nil, nil, merrors.Internal("No repo for orchestrator v2 provided")
	}
	if runtimeDeps.LedgerClient == nil {
		logger.Error("Orchestration v2 disabled: ledger client is missing")
		return nil, nil, merrors.Internal("ledger client is required")
	}
	if checker, ok := runtimeDeps.LedgerClient.(interface{ Available() bool }); ok && !checker.Available() {
		logger.Error("Orchestration v2 disabled: ledger client is unavailable")
		return nil, nil, merrors.Internal("ledger client is unavailable")
	}

	paymentRepo, err := buildPaymentRepositoryV2(repo, logger)
	if paymentRepo == nil || err != nil {
		logger.Error("Orchestration v2 disabled: database not available", zap.Error(err))
		return nil, nil, merrors.Internal("database is not available")
	}

	query, err := pquery.New(pquery.Dependencies{
		Repository: paymentRepo,
		Logger:     logger,
	})
	if err != nil {
		logger.Error("Orchestration v2 disabled: query service init failed", zap.Error(err))
		return nil, paymentRepo, err
	}
	observer, err := oobs.New(oobs.Dependencies{Logger: logger})
	if err != nil {
		logger.Error("Orchestration v2 disabled: observer init failed", zap.Error(err))
		return nil, paymentRepo, err
	}
	executors := buildOrchestrationV2Executors(logger, runtimeDeps)

	svc, err := psvc.New(psvc.Dependencies{
		Logger:     logger.Named("v2"),
		QuoteStore: repo.Quotes(),
		Repository: paymentRepo,
		Query:      query,
		Observer:   observer,
		Executors:  executors,
		BatchOptimizer: psvc.NewPolicyBatchOptimizer(psvc.PolicyBatchOptimizerDependencies{
			Logger:           logger.Named("v2"),
			Policy:           runtimeDeps.BatchOptimizationPolicy,
			MergeKeyResolver: newOrchestratorBatchMergeKeyResolver(runtimeDeps.CardGatewayRoutes),
		}),
		BatchOptimizationPolicy: runtimeDeps.BatchOptimizationPolicy,
		Producer:                runtimeDeps.Producer,
	})
	if err != nil {
		logger.Error("Orchestration v2 disabled: service init failed", zap.Error(err))
		return nil, paymentRepo, err
	}
	return svc, paymentRepo, err
}

func buildOrchestrationV2Executors(logger mlogger.Logger, runtimeDeps v2RuntimeDeps) sexec.Registry {
	execLogger := logger.Named("v2")

	var cryptoExecutor sexec.CryptoExecutor
	var providerSettlementExecutor sexec.ProviderSettlementExecutor
	var guardExecutor sexec.GuardExecutor
	if runtimeDeps.GatewayInvokeResolver != nil && runtimeDeps.GatewayRegistry != nil {
		cryptoExecutor = &gatewayCryptoExecutor{
			gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver,
			gatewayRegistry:       runtimeDeps.GatewayRegistry,
			cardGatewayRoutes:     cloneCardGatewayRoutes(runtimeDeps.CardGatewayRoutes),
		}
		providerSettlementExecutor = &gatewayProviderSettlementExecutor{
			gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver,
			gatewayRegistry:       runtimeDeps.GatewayRegistry,
		}
		guardExecutor = &gatewayGuardExecutor{
			logger:                execLogger.Named("guard"),
			gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver,
			gatewayRegistry:       runtimeDeps.GatewayRegistry,
		}
	}

	ledgerExecutor := &gatewayLedgerExecutor{
		ledgerClient: runtimeDeps.LedgerClient,
	}
	var cardPayoutExecutor sexec.CardPayoutExecutor
	if runtimeDeps.GatewayRegistry != nil {
		cardPayoutExecutor = &gatewayCardPayoutExecutor{
			gatewayRegistry: runtimeDeps.GatewayRegistry,
		}
	}
	return psvc.NewDefaultExecutors(execLogger, sexec.Dependencies{
		Ledger:             ledgerExecutor,
		Crypto:             cryptoExecutor,
		ProviderSettlement: providerSettlementExecutor,
		CardPayout:         cardPayoutExecutor,
		Guard:              guardExecutor,
	})
}

func buildPaymentRepositoryV2(repo storage.Repository, logger mlogger.Logger) (prepo.Repository, error) {
	if repo == nil {
		return nil, merrors.InvalidArgument("repo must be provided")
	}
	provider, ok := repo.(v2MongoDBProvider)
	if !ok {
		return nil, merrors.Internal("Failed to fetch correct repository interface")
	}
	db := provider.MongoDatabase()
	if db == nil {
		return nil, merrors.Internal("Failed to fetch database")
	}
	paymentRepo, err := prepo.NewMongo(
		db.Collection(mservice.Payments),
		prepo.Dependencies{Logger: logger},
	)
	return paymentRepo, err
}

type v2GRPCServer struct {
	orchestrationv2.UnimplementedPaymentOrchestratorServiceServer
	svc    psvc.Service
	logger mlogger.Logger
}

func newV2GRPCServer(svc psvc.Service, logger mlogger.Logger) *v2GRPCServer {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &v2GRPCServer{
		svc:    svc,
		logger: logger.Named("grpc"),
	}
}

func (s *v2GRPCServer) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) {
	resp, err := s.svc.ExecutePayment(ctx, req)
	if err != nil {
		return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.ExecutePaymentResponse](s.logger, mservice.PaymentOrchestrator, err))
	}
	return resp, nil
}

func (s *v2GRPCServer) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error) {
	resp, err := s.svc.ExecuteBatchPayment(ctx, req)
	if err != nil {
		return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.ExecuteBatchPaymentResponse](s.logger, mservice.PaymentOrchestrator, err))
	}
	return resp, nil
}

func (s *v2GRPCServer) GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) {
	resp, err := s.svc.GetPayment(ctx, req)
	if err != nil {
		return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, err))
	}
	return resp, nil
}

func (s *v2GRPCServer) ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) {
	resp, err := s.svc.ListPayments(ctx, req)
	if err != nil {
		return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, err))
	}
	return resp, nil
}
