package com.github.sandokandias.payments.application.impl;

import com.github.sandokandias.payments.application.PaymentProcessManager;
import com.github.sandokandias.payments.domain.command.AuthorizePayment;
import com.github.sandokandias.payments.domain.command.ConfirmPayment;
import com.github.sandokandias.payments.domain.command.PerformPayment;
import com.github.sandokandias.payments.domain.entity.Payment;
import com.github.sandokandias.payments.domain.event.PaymentAuthorized;
import com.github.sandokandias.payments.domain.event.PaymentConfirmed;
import com.github.sandokandias.payments.domain.event.PaymentEvent;
import com.github.sandokandias.payments.domain.event.PaymentRequested;
import com.github.sandokandias.payments.domain.shared.CommandFailure;
import com.github.sandokandias.payments.domain.vo.PaymentId;
import com.github.sandokandias.payments.domain.vo.PaymentStatus;
import com.github.sandokandias.payments.infrastructure.util.i18n.I18nCode;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import io.vavr.control.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import static io.vavr.control.Either.left;
import static io.vavr.control.Either.right;
import static java.util.concurrent.CompletableFuture.completedFuture;

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Service
class PaymentProcessManagerImpl implements PaymentProcessManager {

    private static final Logger LOG = LoggerFactory.getLogger(PaymentProcessManagerImpl.class);

    private final ApplicationContext applicationContext;

    PaymentProcessManagerImpl(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }


    @HystrixCommand(fallbackMethod = "fallback")
    @Override
    public CompletionStage<Either<CommandFailure, Tuple2<PaymentId, PaymentStatus>>> process(PerformPayment performPayment) {

        LOG.debug("Payment process {}", performPayment);

        Payment payment = new Payment(applicationContext);
        CompletionStage<Either<CommandFailure, PaymentRequested>> performPaymentPromise = payment.handle(performPayment);
        return performPaymentPromise.thenCompose(paymentRequested -> paymentRequested.fold(
                rejectPayment -> completed(rejectPayment),
                acceptPayment -> authorize(payment, acceptPayment).thenCompose(paymentAuthorized -> paymentAuthorized.fold(
                        rejectAuthorization -> completed(rejectAuthorization),
                        acceptAuthorization -> {
                            if (acceptPayment.getPaymentIntent().isAuthorize()) {
                                return completed(acceptAuthorization, PaymentStatus.AUTHORIZED);
                            } else {
                                return confirm(payment, acceptPayment, acceptAuthorization);
                            }
                        }
                ))
        ));
    }

    public CompletionStage<Either<CommandFailure, Tuple2<PaymentId, PaymentStatus>>> fallback(PerformPayment performPayment) {
        return completed(new CommandFailure(new HashSet<I18nCode>() {{
            add(new I18nCode("SERVICE_UNAVAILABLE"));
        }}));
    }

    private CompletableFuture<Either<CommandFailure, Tuple2<PaymentId, PaymentStatus>>> completed(CommandFailure rejectAuthorization) {
        return completedFuture(left(rejectAuthorization));
    }

    private CompletableFuture<Either<CommandFailure, Tuple2<PaymentId, PaymentStatus>>> completed(PaymentEvent paymentEvent, PaymentStatus paymentStatus) {
        return completedFuture(right(Tuple.of(paymentEvent.getPaymentId(), paymentStatus)));
    }

    private CompletionStage<Either<CommandFailure, Tuple2<PaymentId, PaymentStatus>>> confirm(Payment payment, PaymentRequested acceptPayment, PaymentAuthorized acceptAuthorization) {
        ConfirmPayment confirmPayment = ConfirmPayment.commandOf(
                acceptAuthorization.getPaymentId(),
                acceptAuthorization.getCustomerId()
        );
        CompletionStage<Either<CommandFailure, PaymentConfirmed>> confirmPaymentPromise = payment.handle(confirmPayment);
        return confirmPaymentPromise.thenApply(paymentConfirmed -> paymentConfirmed.fold(
                rejectConfirmation -> left(rejectConfirmation),
                acceptConfirmation -> right(Tuple.of(acceptPayment.getPaymentId(), PaymentStatus.CAPTURED))
        ));
    }

    private CompletionStage<Either<CommandFailure, PaymentAuthorized>> authorize(Payment payment, PaymentRequested acceptPayment) {
        AuthorizePayment authorizePayment = AuthorizePayment.commandOf(
                acceptPayment.getPaymentId(),
                acceptPayment.getCustomerId(),
                acceptPayment.getPaymentMethod(),
                acceptPayment.getTransaction()
        );
        return payment.handle(authorizePayment);
    }
}