package org.initialde.yakasave.Domain.Entities;

import lombok.Getter;
import org.initialde.yakasave.Domain.Enums.TypeSavingsFund;
import org.initialde.yakasave.Domain.Exceptions.*;
import org.initialde.yakasave.Domain.ValueObject.Amount;

import java.time.LocalDate;
import java.util.Collection;
import java.util.Objects;
import java.util.UUID;

@Getter
public class SavingsFund {
    private final UUID id;
    private final String reference;
    private final String title;
    private final String description;
    private boolean isActive;
    private final User owner;
    private final TypeSavingsFund type;
    private final Amount goalAmount;
    private final Amount balance;
    private final LocalDate launchDate;
    private final LocalDate deadline;
    private final boolean needsApproval;
    private final int maxAllowedMembers;
    private final Collection<User> contributors;
    private final Collection<WithdrawalApproval> receiptApprovals;
    private final Collection<Contribution> contributions;

    public SavingsFund(SavingsFundBuilder builder)
    {
        MinimumGoalAmountException.throwIfInvalid(builder.getGoalAmount());

        if(builder.getDeadline().isBefore(builder.getLaunchDate())){
            throw new InvalidDeadlineException();
        }
        this.id = builder.getId();
        this.reference = builder.getReference();
        this.isActive = builder.isActive();
        this.title = builder.getGoalTitle();
        this.goalAmount = builder.getGoalAmount();
        this.owner = builder.getOwner();
        this.balance = builder.getBalance();
        this.description = builder.getGoalDescription();
        this.launchDate = builder.getLaunchDate();
        this.deadline = builder.getDeadline();
        this.type = builder.getType();
        this.needsApproval = builder.isNeedsApproval();
        this.maxAllowedMembers = builder.getMaxAllowedMembers();
        this.contributors = builder.getContributors();
        this.receiptApprovals = builder.getReceiptApprovals();
        this.contributions = builder.getContributions();
    }

    public void addContributor(User contributor) {
        if (contributors.size() == maxAllowedMembers) {
            throw new ExceedMaxMembersAllowedException();
        }
        if (isContributor(contributor)){
            throw new AlreadyContributorException();
        }
        this.contributors.add(contributor);
    }

    public void removeContributor(User contributor) {
        if (isOwner(contributor)) {
            throw new CannotRemoveOwnerException();
        }

        if (!isContributor(contributor)){
            throw new IsNotContributorException();
        }

        if (hasContribute(contributor)) {
            throw new CannotRemoveContributorException();
        }

        this.contributors.remove(contributor);
    }

    private boolean isContributor(User contributor) {
        return this.contributors.stream().anyMatch(u -> u.equals(contributor));
    }

    public boolean isOwner(User owner) {
        return this.owner.equals(owner);
    }

    public void addContribution(User contributor, Amount contribution) {
        if (!isContributor(contributor)){
            throw new IsNotContributorException();
        }
        SavingsFundClosedException.throwIfIsClosed(this.isActive);
        ContributionAmountLessThanMinimumAuthorizedException.throwIfInvalid(contribution);
        this.balance.plus(contribution);
        this.contributions.add(new Contribution(contributor, contribution.toDouble()));
    }

    public void withDrawMoney(User owner, Amount moneyToWithDraw) {
        AmountWithDrawLessThanMinimumAuthorizedException.throwIfInvalid(moneyToWithDraw);

        if(moneyToWithDraw.isGreaterThan(balance)){
            throw new AmountWithDrawGreaterThanBalanceException();
        }

       if(!this.canWithdrawMoney()) {
           throw new NotAuthorizeToWithdrawFromSavingsFundException();
       }

        this.balance.minus(moneyToWithDraw);
    }

    public void approveWithdraw(User approvedBy) {
        if (isOwner(approvedBy)) {
            throw new OwnerCannotGiveApprovalException();
        }

        if(!hasContribute(approvedBy)){
            throw new NotYetContributedException();
        }

        if(hasApproved(approvedBy)){
            throw new AlreadyApprovedWithdrawException();
        }

        this.receiptApprovals.add(new WithdrawalApproval(approvedBy));
    }

    private boolean canWithdrawMoney() {

        if(this.isNeedsApproval()){
            var totalContributorsWhoHaveContribute = contributors
                    .stream()
                    .filter(c -> !c.equals(owner))
                    .filter(this::hasContribute)
                    .toList();
            int totalContributorsWithoutOwner = (int) Math.ceil((double) totalContributorsWhoHaveContribute.size() / 2);
            return !totalContributorsWhoHaveContribute.isEmpty() && totalContributorsWithoutOwner <= receiptApprovals.size();
        }
        return true;
    }

    public void close() {
        if(!this.isActive) {
            throw new AlreadyClosedSavingsFunException();
        }
        NotAuthorizeToCloseSavingsFundException.throwIfNotEqualToZero(this.balance);
        this.isActive = false;
    }

    private boolean hasContribute(User contributor) {
        return this.contributions.stream().anyMatch(c -> c.getContributor().equals(contributor));
    }

    private boolean hasApproved(User by) {
        return this.receiptApprovals.stream().anyMatch(c -> c.getApprovedBy().equals(by));
    }

    public static SavingsFund restoreFromSnapshot(SavingsFundSnapshot snapshot) {
        return SavingsFund.builder()
                .id(snapshot.getId())
                .reference(snapshot.getReference())
                .goalTitle(snapshot.getGoalTitle())
                .goalDescription(snapshot.getGoalDescription())
                .goalAmount(snapshot.getGoalAmount())
                .isActive(snapshot.isActive())
                .balance(Amount.of(snapshot.getBalance()))
                .launchDate(snapshot.getLaunchDate())
                .deadline(snapshot.getDeadline())
                .type(snapshot.getType())
                .owner(snapshot.getOwner())
                .needsApproval(snapshot.isHasRequiredApproval())
                .maxAllowedMembers(snapshot.getMaxAllowedMembers())
                .contributors(snapshot.getContributors().stream().toList())
                .contributions(snapshot.getContributions().stream().toList())
                .receiptApprovals(snapshot.getReceiptsApprovals().stream().toList())
                .build();
    }

    public SavingsFundSnapshot takeSnapshot() {
        return new SavingsFundSnapshot.SavingsFundSnapshotBuilder()
                .id(id)
                .reference(reference)
                .goalTitle(title)
                .goalAmount(goalAmount.toDouble())
                .owner(owner)
                .type(type)
                .balance(balance.toDouble())
                .launchDate(launchDate)
                .deadline(deadline)
                .goalDescription(description)
                .hasRequiredApproval(needsApproval)
                .maxAllowedMembers(maxAllowedMembers)
                .contributors((contributors.stream().toList()))
                .contributions(contributions.stream().toList())
                .receiptsApprovals(receiptApprovals.stream().toList())
                .isActive(isActive)
                .build();
    }

    public static SavingsFundBuilder builder() {
        return new SavingsFundBuilder();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SavingsFund savingsFund = (SavingsFund) o;
        return Objects.equals(reference, savingsFund.getReference());
    }
}
