package creditcard_eventsourcing.model;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.function.Consumer;

import org.requirementsascode.Condition;
import org.requirementsascode.Model;
import org.requirementsascode.ModelRunner;

import creditcard_eventsourcing.model.command.RequestToCloseCycle;
import creditcard_eventsourcing.model.command.RequestsRepay;
import creditcard_eventsourcing.model.command.RequestsToAssignLimit;
import creditcard_eventsourcing.model.command.RequestsWithdrawal;

public class CreditCardModelRunner {
	// Step names
	public static final String ASSIGN = "Assign limit";
	public static final String ASSIGN_TWICE = "Assign limit twice";
	public static final String WITHDRAW = "Withdraw";
	public static final String WITHDRAW_AGAIN = "Withdraw again";
	public static final String WITHDRAW_TOO_OFTEN = "Withdraw too often";
	public static final String CLOSE = "Close cycle";
	public static final String REPEAT = "Repeat";
	public static final String REPAY = "Repay";

	// Command types
	private static final Class<RequestsToAssignLimit> requestsToAssignLimit = RequestsToAssignLimit.class;
	private static final Class<RequestsWithdrawal> requestsWithdrawal = RequestsWithdrawal.class;
	private static final Class<RequestsRepay> requestsRepay = RequestsRepay.class;
	private static final Class<RequestToCloseCycle> requestToCloseCycle = RequestToCloseCycle.class;

	// Command handlers
	private Consumer<RequestsToAssignLimit> assignsLimit;
	private Consumer<RequestsWithdrawal> withdraws;
	private Consumer<RequestsRepay> repays;
	private Consumer<RequestToCloseCycle> closesCycle;
	private Consumer<RequestsToAssignLimit> throwsAssignLimitException;
	private Consumer<RequestsWithdrawal> throwsTooManyWithdrawalsException;

	// Conditions
	private Condition tooManyWithdrawalsInCycle;
	private Condition limitAlreadyAssigned;
	private Condition accountOpen;

	// Other fields
	private CreditCard creditCard;
	private ModelRunner modelRunner;

	public CreditCardModelRunner(CreditCard creditCard, ModelRunner modelRunner) {
		this.creditCard = creditCard;
		this.modelRunner = modelRunner;

		assignCardDependentFields();
		modelRunner.run(model());
	}

	public Model model() {
		Model model = Model.builder()
		  .useCase("Use credit card")
		    .basicFlow()
		    	.step(ASSIGN).user(requestsToAssignLimit).system(assignsLimit)
		    	.step(WITHDRAW).user(requestsWithdrawal).system(withdraws).reactWhile(accountOpen)
		    	.step(REPAY).user(requestsRepay).system(repays).reactWhile(accountOpen)
		    	
		    .flow("Withdraw again").after(REPAY)
		    	.step(WITHDRAW_AGAIN).user(requestsWithdrawal).system(withdraws)
		    	.step(REPEAT).continuesAt(WITHDRAW)
		    	
		    .flow("Cycle is over").anytime()
		    	.step(CLOSE).on(requestToCloseCycle).system(closesCycle)
		    	
		    .flow("Assign limit twice").condition(limitAlreadyAssigned)
		    	.step(ASSIGN_TWICE).user(requestsToAssignLimit).system(throwsAssignLimitException)
		    	
		    .flow("Too many withdrawals").condition(tooManyWithdrawalsInCycle) 
		    	.step(WITHDRAW_TOO_OFTEN).user(requestsWithdrawal).system(throwsTooManyWithdrawalsException)
		.build();
		return model;
	}

	public void handleCommand(Object command) {
		modelRunner.reactTo(command);
	}

	private void assignCardDependentFields() { 
		this.assignsLimit = this::assignLimit;
		this.withdraws = this::withdraw;
		this.repays = this::repay;
		this.closesCycle = this::closeCycle;
		this.throwsAssignLimitException = this::throwAssignLimitException;
		this.throwsTooManyWithdrawalsException = this::throwTooManyWithdrawalsException;
 
		this.tooManyWithdrawalsInCycle = creditCard::tooManyWithdrawalsInCycle;
		this.limitAlreadyAssigned = creditCard::limitAlreadyAssigned;
		this.accountOpen = creditCard::accountOpen;
	}
	
	// Command handling methods
	private void assignLimit(RequestsToAssignLimit request) {
		BigDecimal amount = request.getAmount();
		creditCard.handle(new LimitAssigned(creditCard.uuid(), amount, Instant.now()));
	}
	
	private void withdraw(RequestsWithdrawal request) {
		BigDecimal amount = request.getAmount();
		if (creditCard.notEnoughMoneyToWithdraw(amount)) {
			throw new IllegalStateException();
		}
		creditCard.handle(new CardWithdrawn(creditCard.uuid(), amount, Instant.now()));
	}
	
	private void repay(RequestsRepay request) {
		BigDecimal amount = request.getAmount();
		creditCard.handle(new CardRepaid(creditCard.uuid(), amount, Instant.now()));
	}
	
	private void closeCycle(RequestToCloseCycle request) {
		creditCard.handle(new CycleClosed(creditCard.uuid(), Instant.now()));
	}
	
	private void throwAssignLimitException(RequestsToAssignLimit request) {
		throw new IllegalStateException();
	}

	private void throwTooManyWithdrawalsException(RequestsWithdrawal request) {
		throw new IllegalStateException();
	}
}

@@ -1424,18 +1424,26 @@
 nd handl
-er
+ing method
 s%0A%09priva
@@ -1489,16 +1489,36 @@
 gnsLimit
+ = this::assignLimit
 ;%0A%09priva
@@ -1558,16 +1558,33 @@
 ithdraws
+ = this::withdraw
 ;%0A%09priva
@@ -1616,16 +1616,30 @@
 %3E repays
+ = this::repay
 ;%0A%09priva
@@ -1678,24 +1678,43 @@
  closesCycle
+ = this::closeCycle
 ;%0A%09private C
@@ -1770,16 +1770,50 @@
 xception
+ = this::throwAssignLimitException
 ;%0A%09priva
@@ -1877,16 +1877,57 @@
 xception
+ = this::throwTooManyWithdrawalsException
 ;%0A%0A%09// C
@@ -3480,305 +3480,8 @@
 ) %7B 
-%0A%09%09this.assignsLimit = this::assignLimit;%0A%09%09this.withdraws = this::withdraw;%0A%09%09this.repays = this::repay;%0A%09%09this.closesCycle = this::closeCycle;%0A%09%09this.throwsAssignLimitException = this::throwAssignLimitException;%0A%09%09this.throwsTooManyWithdrawalsException = this::throwTooManyWithdrawalsException;%0A
  %0A%09%09

Move command handling methods to CreditCardModelRunner