/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2018-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package jdocs.org.apache.pekko.cluster.sharding.typed;

import com.fasterxml.jackson.annotation.JsonCreator;
import java.math.BigDecimal;
import org.apache.pekko.Done;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.cluster.sharding.typed.javadsl.EntityTypeKey;
import org.apache.pekko.pattern.StatusReply;
import org.apache.pekko.persistence.typed.PersistenceId;
import org.apache.pekko.persistence.typed.state.javadsl.CommandHandlerWithReply;
import org.apache.pekko.persistence.typed.state.javadsl.CommandHandlerWithReplyBuilder;
import org.apache.pekko.persistence.typed.state.javadsl.DurableStateBehaviorWithEnforcedReplies;
import org.apache.pekko.persistence.typed.state.javadsl.ReplyEffect;
import org.apache.pekko.serialization.jackson.CborSerializable;

/**
 * This bank account example illustrates the following: - different state classes representing the
 * lifecycle of the account - null as emptyState - command handlers that delegate to methods in the
 * DurableStateBehavior class, and - replies of various types that use
 * DurableStateBehaviorWithEnforcedReplies
 */
public interface AccountExampleWithNullDurableState {

  // #account-entity
  // #withEnforcedReplies
  public class AccountEntity
      extends DurableStateBehaviorWithEnforcedReplies<
          AccountEntity.Command, AccountEntity.Account> {
    // #withEnforcedReplies

    public static final EntityTypeKey<Command> ENTITY_TYPE_KEY =
        EntityTypeKey.create(Command.class, "Account");

    // Command
    // #reply-command
    interface Command extends CborSerializable {}

    // #reply-command

    public static class CreateAccount implements Command {
      public final ActorRef<StatusReply<Done>> replyTo;

      @JsonCreator
      public CreateAccount(ActorRef<StatusReply<Done>> replyTo) {
        this.replyTo = replyTo;
      }
    }

    public static class Deposit implements Command {
      public final BigDecimal amount;
      public final ActorRef<StatusReply<Done>> replyTo;

      public Deposit(BigDecimal amount, ActorRef<StatusReply<Done>> replyTo) {
        this.replyTo = replyTo;
        this.amount = amount;
      }
    }

    public static class Withdraw implements Command {
      public final BigDecimal amount;
      public final ActorRef<StatusReply<Done>> replyTo;

      public Withdraw(BigDecimal amount, ActorRef<StatusReply<Done>> replyTo) {
        this.amount = amount;
        this.replyTo = replyTo;
      }
    }

    public static class GetBalance implements Command {
      public final ActorRef<CurrentBalance> replyTo;

      @JsonCreator
      public GetBalance(ActorRef<CurrentBalance> replyTo) {
        this.replyTo = replyTo;
      }
    }

    public static class CloseAccount implements Command {
      public final ActorRef<StatusReply<Done>> replyTo;

      @JsonCreator
      public CloseAccount(ActorRef<StatusReply<Done>> replyTo) {
        this.replyTo = replyTo;
      }
    }

    // Reply
    public static class CurrentBalance implements CborSerializable {
      public final BigDecimal balance;

      @JsonCreator
      public CurrentBalance(BigDecimal balance) {
        this.balance = balance;
      }
    }

    // State
    interface Account extends CborSerializable {}

    public static class OpenedAccount implements Account {
      public final BigDecimal balance;

      public OpenedAccount() {
        this.balance = BigDecimal.ZERO;
      }

      @JsonCreator
      public OpenedAccount(BigDecimal balance) {
        this.balance = balance;
      }

      OpenedAccount makeDeposit(BigDecimal amount) {
        return new OpenedAccount(balance.add(amount));
      }

      boolean canWithdraw(BigDecimal amount) {
        return (balance.subtract(amount).compareTo(BigDecimal.ZERO) >= 0);
      }

      OpenedAccount makeWithdraw(BigDecimal amount) {
        if (!canWithdraw(amount))
          throw new IllegalStateException("Account balance can't be negative");
        return new OpenedAccount(balance.subtract(amount));
      }

      ClosedAccount closedAccount() {
        return new ClosedAccount();
      }
    }

    public static class ClosedAccount implements Account {}

    public static AccountEntity create(String accountNumber, PersistenceId persistenceId) {
      return new AccountEntity(accountNumber, persistenceId);
    }

    private final String accountNumber;

    private AccountEntity(String accountNumber, PersistenceId persistenceId) {
      super(persistenceId);
      this.accountNumber = accountNumber;
    }

    @Override
    public Account emptyState() {
      return null;
    }

    @Override
    public CommandHandlerWithReply<Command, Account> commandHandler() {
      CommandHandlerWithReplyBuilder<Command, Account> builder =
          newCommandHandlerWithReplyBuilder();

      builder.forNullState().onCommand(CreateAccount.class, this::createAccount);

      builder
          .forStateType(OpenedAccount.class)
          .onCommand(Deposit.class, this::deposit)
          .onCommand(Withdraw.class, this::withdraw)
          .onCommand(GetBalance.class, this::getBalance)
          .onCommand(CloseAccount.class, this::closeAccount);

      builder
          .forStateType(ClosedAccount.class)
          .onAnyCommand(() -> Effect().unhandled().thenNoReply());

      return builder.build();
    }

    private ReplyEffect<Account> createAccount(CreateAccount command) {
      return Effect()
          .persist(new OpenedAccount())
          .thenReply(command.replyTo, account2 -> StatusReply.ack());
    }

    private ReplyEffect<Account> deposit(OpenedAccount account, Deposit command) {
      return Effect()
          .persist(account.makeDeposit(command.amount))
          .thenReply(command.replyTo, account2 -> StatusReply.ack());
    }

    // #reply
    private ReplyEffect<Account> withdraw(OpenedAccount account, Withdraw command) {
      if (!account.canWithdraw(command.amount)) {
        return Effect()
            .reply(
                command.replyTo,
                StatusReply.error("not enough funds to withdraw " + command.amount));
      } else {
        return Effect()
            .persist(account.makeWithdraw(command.amount))
            .thenReply(command.replyTo, account2 -> StatusReply.ack());
      }
    }

    // #reply

    private ReplyEffect<Account> getBalance(OpenedAccount account, GetBalance command) {
      return Effect().reply(command.replyTo, new CurrentBalance(account.balance));
    }

    private ReplyEffect<Account> closeAccount(OpenedAccount account, CloseAccount command) {
      if (account.balance.equals(BigDecimal.ZERO)) {
        return Effect()
            .persist(account.closedAccount())
            .thenReply(command.replyTo, account2 -> StatusReply.ack());
      } else {
        return Effect()
            .reply(command.replyTo, StatusReply.error("balance must be zero for closing account"));
      }
    }
  }

  // #account-entity
}
