package org.fenixedu.academic.ui.spring.controller;

import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.servlet.ServletContext;

import org.fenixedu.academic.domain.Person;
import org.fenixedu.academic.domain.accounting.AccountingTransaction;
import org.fenixedu.academic.domain.accounting.Event;
import org.fenixedu.academic.domain.accounting.Exemption;
import org.fenixedu.academic.domain.accounting.Refund;
import org.fenixedu.academic.domain.accounting.calculator.AccountingEntry;
import org.fenixedu.academic.domain.accounting.calculator.CreditEntry;
import org.fenixedu.academic.domain.accounting.calculator.Debt;
import org.fenixedu.academic.domain.accounting.calculator.DebtEntry;
import org.fenixedu.academic.domain.accounting.calculator.DebtInterestCalculator;
import org.fenixedu.academic.domain.accounting.calculator.ExcessRefund;
import org.fenixedu.academic.domain.accounting.report.EventStats;
import org.fenixedu.academic.domain.exceptions.DomainException;
import org.fenixedu.academic.ui.spring.service.AccountingManagementAccessControlService;
import org.fenixedu.academic.ui.spring.service.AccountingManagementService;
import org.fenixedu.academic.ui.spring.service.AccountingManagementService.AccountingEntrySummary;
import org.fenixedu.academic.util.Money;
import org.fenixedu.bennu.core.domain.User;
import org.joda.time.DateTime;
import org.springframework.context.MessageSource;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.google.common.base.Strings;

import pt.ist.fenixframework.DomainObject;
import pt.ist.fenixframework.FenixFramework;

/**
 * Created by Sérgio Silva ([email protected]).
 */
public abstract class AccountingController {

    public static final Comparator<Event> MOST_RECENT_EVENTS_FIRST = Comparator.comparing(Event::getWhenOccured).reversed();
    protected final AccountingManagementService accountingManagementService;
    protected final AccountingManagementAccessControlService accessControlService;
    protected final ServletContext servletContext;
    protected final MessageSource messageSource;

    public AccountingController(AccountingManagementService accountingManagementService, AccountingManagementAccessControlService accountingManagementAccessControlService, ServletContext servletContext, MessageSource messageSource) {
        this.accountingManagementService = accountingManagementService;
        this.accessControlService = accountingManagementAccessControlService;
        this.servletContext = servletContext;
        this.messageSource = messageSource;
    }

    public abstract String entrypointUrl();

    public abstract String getEventDetailsUrl(Event event);

    @RequestMapping("{person}")
    public String events(@PathVariable Person person, Model model, User loggedUser) {
        final User user = person.getUser();
        
        if (loggedUser == user || accessControlService.isPaymentManager(loggedUser)) {
            model.addAttribute("person", person);

            Set<Event> events = person.getEventsSet();
            HashMap<Event, String> invalidEventsMap = getInvalidEventsMap(events);
            Set<Event> validEvents = events.stream().filter(event -> !invalidEventsMap.keySet().contains(event)).collect(Collectors.toSet());

            model.addAttribute("openEvents", validEvents.stream().filter(Event::isOpen).sorted(MOST_RECENT_EVENTS_FIRST).collect(Collectors.toList()));
            model.addAttribute("otherEvents", validEvents.stream().filter(e -> !e.isOpen()).sorted(MOST_RECENT_EVENTS_FIRST).collect(Collectors.toList()));
            model.addAttribute("invalidEvents", invalidEventsMap);
            model.addAttribute("isPaymentManager", accessControlService.isPaymentManager(loggedUser));
            model.addAttribute("stats", new EventStats(validEvents));

            return view("events");
        }
        throw new UnsupportedOperationException("Unauthorized");
    }

    @RequestMapping("{event}/details")
    public String details(@PathVariable Event event, @RequestParam(value = "date", defaultValue = "#{new org.joda.time.DateTime()}") DateTime date,  Model model, User loggedUser) {
        accessControlService.checkEventOwnerOrPaymentManager(event, loggedUser);

        final DebtInterestCalculator debtInterestCalculator = event.getDebtInterestCalculator(date);

        model.addAttribute("entrypointUrl", entrypointUrl());

        model.addAttribute("transactions", accountingManagementService.getAccountingEntries(debtInterestCalculator));
        model.addAttribute("event", event);
        model.addAttribute("eventDetailsUrl", getEventDetailsUrl(event));
        model.addAttribute("eventId", event.getExternalId());
        model.addAttribute("currentDate", date.toLocalDate());

        model.addAttribute("debts", debtInterestCalculator.getDebtsOrderedByDueDate());
        model.addAttribute("eventTotalAmountToPay", debtInterestCalculator.getTotalDueAmount());
        model.addAttribute("eventPaidUnusedAmount", debtInterestCalculator.getPaidUnusedAmount());
        model.addAttribute("eventDebtAmountToPay", debtInterestCalculator.getDueAmount());
        model.addAttribute("eventInterestAmountToPay", debtInterestCalculator.getDueInterestAmount());
        model.addAttribute("eventFineAmountToPay", debtInterestCalculator.getDueFineAmount());
        model.addAttribute("eventOriginalAmountToPay", event.getOriginalAmountToPay());

        model.addAttribute("payedDebtAmount", debtInterestCalculator.getPaidDebtAmount());

        model.addAttribute("isEventOwner", accessControlService.isEventOwner(event, loggedUser));
        model.addAttribute("isPaymentManager", accessControlService.isPaymentManager(event, loggedUser));
        model.addAttribute("isAdvancedPaymentManager", accessControlService.isAdvancedPaymentManager(event, loggedUser));

        return view("event-details");
    }

    @RequestMapping("{event}/transaction/{id}/details")
    public String transactionDetails(@PathVariable Event event, @PathVariable String id, @RequestParam(value = "date",
            defaultValue = "#{new org.joda.time.DateTime()}") DateTime date, Model model, User loggedUser) {

        DebtInterestCalculator debtInterestCalculator = event.getDebtInterestCalculator(date);

        AccountingEntry accountingEntry = debtInterestCalculator.getAccountingEntry(id).orElseThrow
                (UnsupportedOperationException::new);

        if (accountingEntry instanceof DebtEntry) {
            return debtEntryDetails(event, (DebtEntry) accountingEntry, model, loggedUser);
        }

        if (accountingEntry instanceof CreditEntry) {
            return creditEntryDetails(event, debtInterestCalculator, (CreditEntry) accountingEntry, model, loggedUser);
        }

        throw new UnsupportedOperationException("Can't find resolver for transaction with id " + id);
    }

    private String debtEntryDetails(Event event, DebtEntry debtEntry, Model model, User loggedUser) {
        accessControlService.checkEventOwnerOrPaymentManager(event, loggedUser);

        final List<AccountingManagementService.AccountingEntrySummary> paymentSummaries = accountingManagementService.createAccountingEntrySummaries(debtEntry);
        
        paymentSummaries.sort(Comparator.comparing(AccountingManagementService.AccountingEntrySummary::getCreated)
                .thenComparing(AccountingEntrySummary::getDate)
                .thenComparing(AccountingEntrySummary::getId).reversed());

        model.addAttribute("eventId", event.getExternalId());
        model.addAttribute("event", event);
        model.addAttribute("eventDetailsUrl", getEventDetailsUrl(event));
        model.addAttribute("eventDescription", event.getDescription());
        model.addAttribute("payments", paymentSummaries);
        model.addAttribute("isEventOwner", accessControlService.isEventOwner(event, loggedUser));

        if (debtEntry instanceof Debt) {
            model.addAttribute("debt", debtEntry);
            return view("event-debt-details");
        }

        if (debtEntry instanceof ExcessRefund) {
            final ExcessRefund excessRefund = (ExcessRefund) debtEntry;
            final String paymentId = excessRefund.getTargetPaymentId();
            if (!Strings.isNullOrEmpty(paymentId)) {
                AccountingTransaction accountingTransaction = FenixFramework.getDomainObject(paymentId);
                model.addAttribute("targetPaymentId", accountingTransaction);
            }
            model.addAttribute("excessRefund", excessRefund);
            model.addAttribute("refundDomainObject", FenixFramework.getDomainObject(excessRefund.getId()));
            return view("event-excessRefund-details");
        }

        throw new UnsupportedOperationException("Unknown debt entry type " + debtEntry.getClass().getName());
    }

    private String creditEntryDetails(Event event, DebtInterestCalculator calculator, CreditEntry creditEntry, Model model, User
            loggedUser) {
        accessControlService.checkEventOwnerOrPaymentManager(event, loggedUser);

        model.addAttribute("eventDetailsUrl", getEventDetailsUrl(event));
        model.addAttribute("eventId", event.getExternalId());
        model.addAttribute("creditEntry", creditEntry);
        model.addAttribute("processedDate", creditEntry.getCreated());
        model.addAttribute("registeredDate", creditEntry.getDate());
        model.addAttribute("amount", creditEntry.getAmount());
        model.addAttribute("payments", accountingManagementService.createAccountingEntrySummaries(creditEntry));

        final DomainObject domainCredit = FenixFramework.getDomainObject(creditEntry.getId());
        if (domainCredit instanceof Refund) {
            model.addAttribute("refund", domainCredit);
            return view("event-refund-details");
        } else if (domainCredit instanceof AccountingTransaction) {
            model.addAttribute("sourceRefund",
                    Optional.ofNullable(((AccountingTransaction) domainCredit).getRefund()).orElse(null));
            model.addAttribute("transactionDetail", domainCredit);
            return view("event-payment-details");
        }
        else if (domainCredit instanceof Exemption){
            model.addAttribute("exemption", domainCredit);
            model.addAttribute("exemptionValue", ((Exemption) domainCredit).getExemptionAmount(new Money(calculator.getDebtAmount())));
            return view("event-exemption-details");
        }
        else {
            throw new UnsupportedOperationException("Unknown transaction type: not a payment or exemption");
        }

    }

    @RequestMapping(value = "{event}/depositAdvancement", method = RequestMethod.POST)
    public String depositAdvancement(final @PathVariable Event event, final User user, final Model model,
            @RequestParam final Event eventToRefund) {
        accessControlService.checkEventOwnerOrPaymentManager(eventToRefund, user);

        try {
            accountingManagementService.depositAdvancement(event, eventToRefund, user);
        }
        catch (DomainException e) {
            e.printStackTrace();
            model.addAttribute("error", e.getLocalizedMessage());
            return depositAdvancementInput(event, user, model);
        }

        return redirectToEventDetails(event);
    }

    protected abstract String depositAdvancementInput(Event event, User user, Model model);

    protected abstract String redirectToEventDetails(@PathVariable Event event);

    private HashMap<Event, String> getInvalidEventsMap(Set<Event> events) {
        HashMap<Event, String> invalidEventsMap = new HashMap<>();
        events.forEach(event -> {
            try {
                event.getDebtInterestCalculator(new DateTime());
                event.getDescription();
            }
            catch (Throwable e){
                invalidEventsMap.put(event, e.getLocalizedMessage());
            }
        });
        return invalidEventsMap;
    }

    protected String view(String view) {
        return "fenixedu-academic/accounting/" + view;
    }

}