package org.apereo.cas.web.report;

import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.util.ISOStandardDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

/**
 * SSO Report web controller that produces JSON data for the view.
 *
 * @author Misagh Moayyed
 * @author Dmitriy Kopylenko
 * @since 4.1
 */
public class SingleSignOnSessionsReportController extends BaseCasMvcEndpoint {

    private static final String VIEW_SSO_SESSIONS = "monitoring/viewSsoSessions";
    private static final String STATUS = "status";
    private static final String TICKET_GRANTING_TICKET = "ticketGrantingTicket";
    private static final Logger LOGGER = LoggerFactory.getLogger(SingleSignOnSessionsReportController.class);

    private final CasConfigurationProperties casProperties;

    private enum SsoSessionReportOptions {
        ALL("all"),
        PROXIED("proxied"),
        DIRECT("direct");

        private final String type;

        /**
         * Instantiates a new Sso session report options.
         *
         * @param type the type
         */
        SsoSessionReportOptions(final String type) {
            this.type = type;
        }

        public String getType() {
            return this.type;
        }

        @Override
        public String toString() {
            return this.type;
        }
    }

    /**
     * The enum Sso session attribute keys.
     */
    private enum SsoSessionAttributeKeys {
        AUTHENTICATED_PRINCIPAL("authenticated_principal"),
        PRINCIPAL_ATTRIBUTES("principal_attributes"),
        AUTHENTICATION_DATE("authentication_date"),
        AUTHENTICATION_DATE_FORMATTED("authentication_date_formatted"),
        TICKET_GRANTING_TICKET("ticket_granting_ticket"),
        AUTHENTICATION_ATTRIBUTES("authentication_attributes"),
        PROXIED_BY("proxied_by"),
        AUTHENTICATED_SERVICES("authenticated_services"),
        IS_PROXIED("is_proxied"),
        NUMBER_OF_USES("number_of_uses");

        private final String attributeKey;

        /**
         * Instantiates a new Sso session attribute keys.
         *
         * @param attributeKey the attribute key
         */
        SsoSessionAttributeKeys(final String attributeKey) {
            this.attributeKey = attributeKey;
        }

        @Override
        public String toString() {
            return this.attributeKey;
        }
    }

    private final CentralAuthenticationService centralAuthenticationService;

    public SingleSignOnSessionsReportController(final CentralAuthenticationService centralAuthenticationService,
                                                final CasConfigurationProperties casProperties) {
        super("ssosessions", "/ssosessions", casProperties.getMonitor().getEndpoints().getSingleSignOnReport(), casProperties);
        this.centralAuthenticationService = centralAuthenticationService;
        this.casProperties = casProperties;
    }

    /**
     * Gets sso sessions.
     *
     * @param option the option
     * @return the sso sessions
     */
    private Collection<Map<String, Object>> getActiveSsoSessions(final SsoSessionReportOptions option) {
        final Collection<Map<String, Object>> activeSessions = new ArrayList<>();
        final ISOStandardDateFormat dateFormat = new ISOStandardDateFormat();

        getNonExpiredTicketGrantingTickets().stream().map(TicketGrantingTicket.class::cast)
                .filter(tgt -> !(option == SsoSessionReportOptions.DIRECT && tgt.getProxiedBy() != null)).forEach(tgt -> {
            final Authentication authentication = tgt.getAuthentication();
            final Principal principal = authentication.getPrincipal();
            final Map<String, Object> sso = new HashMap<>(SsoSessionAttributeKeys.values().length);
            sso.put(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString(), principal.getId());
            sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE.toString(), authentication.getAuthenticationDate());
            sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE_FORMATTED.toString(),
                    dateFormat.format(DateTimeUtils.dateOf(authentication.getAuthenticationDate())));
            sso.put(SsoSessionAttributeKeys.NUMBER_OF_USES.toString(), tgt.getCountOfUses());
            sso.put(SsoSessionAttributeKeys.TICKET_GRANTING_TICKET.toString(), tgt.getId());
            sso.put(SsoSessionAttributeKeys.PRINCIPAL_ATTRIBUTES.toString(), principal.getAttributes());
            sso.put(SsoSessionAttributeKeys.AUTHENTICATION_ATTRIBUTES.toString(), authentication.getAttributes());
            if (option != SsoSessionReportOptions.DIRECT) {
                if (tgt.getProxiedBy() != null) {
                    sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.TRUE);
                    sso.put(SsoSessionAttributeKeys.PROXIED_BY.toString(), tgt.getProxiedBy().getId());
                } else {
                    sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.FALSE);
                }
            }
            sso.put(SsoSessionAttributeKeys.AUTHENTICATED_SERVICES.toString(), tgt.getServices());
            activeSessions.add(sso);
        });
        return activeSessions;
    }

    /**
     * Gets non expired ticket granting tickets.
     *
     * @return the non expired ticket granting tickets
     */
    private Collection<Ticket> getNonExpiredTicketGrantingTickets() {
        return this.centralAuthenticationService.getTickets(ticket -> ticket instanceof TicketGrantingTicket && !ticket.isExpired());
    }

    /**
     * Endpoint for getting SSO Sessions in JSON format.
     *
     * @param type     the type
     * @param request  the request
     * @param response the response
     * @return the sso sessions
     */
    @GetMapping(value = "/getSsoSessions")
    @ResponseBody
    public WebAsyncTask<Map<String, Object>> getSsoSessions(@RequestParam(defaultValue = "ALL") final String type,
                                                            final HttpServletRequest request,
                                                            final HttpServletResponse response) {
        ensureEndpointAccessIsAuthorized(request, response);

        final Callable<Map<String, Object>> asyncTask = () -> {
            final Map<String, Object> sessionsMap = new HashMap<>(1);
            final SsoSessionReportOptions option = SsoSessionReportOptions.valueOf(type);

            final Collection<Map<String, Object>> activeSsoSessions = getActiveSsoSessions(option);
            sessionsMap.put("activeSsoSessions", activeSsoSessions);

            long totalTicketGrantingTickets = 0;
            long totalProxyGrantingTickets = 0;
            long totalUsageCount = 0;

            final Set<String> uniquePrincipals = new HashSet<>();

            for (final Map<String, Object> activeSsoSession : activeSsoSessions) {

                if (activeSsoSession.containsKey(SsoSessionAttributeKeys.IS_PROXIED.toString())) {
                    final Boolean isProxied = Boolean.valueOf(activeSsoSession.get(SsoSessionAttributeKeys.IS_PROXIED.toString()).toString());
                    if (isProxied) {
                        totalProxyGrantingTickets++;
                    } else {
                        totalTicketGrantingTickets++;
                        final String principal = activeSsoSession.get(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
                        uniquePrincipals.add(principal);
                    }
                } else {
                    totalTicketGrantingTickets++;
                    final String principal = activeSsoSession.get(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
                    uniquePrincipals.add(principal);
                }
                totalUsageCount += Long.parseLong(activeSsoSession.get(SsoSessionAttributeKeys.NUMBER_OF_USES.toString()).toString());

            }

            sessionsMap.put("totalProxyGrantingTickets", totalProxyGrantingTickets);
            sessionsMap.put("totalTicketGrantingTickets", totalTicketGrantingTickets);
            sessionsMap.put("totalTickets", totalTicketGrantingTickets + totalProxyGrantingTickets);
            sessionsMap.put("totalPrincipals", uniquePrincipals.size());
            sessionsMap.put("totalUsageCount", totalUsageCount);
            return sessionsMap;
        };
        return new WebAsyncTask<>(casProperties.getHttpClient().getAsyncTimeout(), asyncTask);
    }

    /**
     * Endpoint for destroying a single SSO Session.
     *
     * @param ticketGrantingTicket the ticket granting ticket
     * @param request              the request
     * @param response             the response
     * @return result map
     */
    @PostMapping(value = "/destroySsoSession")
    @ResponseBody
    public Map<String, Object> destroySsoSession(@RequestParam final String ticketGrantingTicket,
                                                 final HttpServletRequest request,
                                                 final HttpServletResponse response) {
        ensureEndpointAccessIsAuthorized(request, response);

        final Map<String, Object> sessionsMap = new HashMap<>(1);
        try {
            this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
            sessionsMap.put(STATUS, HttpServletResponse.SC_OK);
            sessionsMap.put(TICKET_GRANTING_TICKET, ticketGrantingTicket);
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
            sessionsMap.put(STATUS, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            sessionsMap.put(TICKET_GRANTING_TICKET, ticketGrantingTicket);
            sessionsMap.put("message", e.getMessage());
        }
        return sessionsMap;
    }

    /**
     * Endpoint for destroying SSO Sessions.
     *
     * @param type     the type
     * @param request  the request
     * @param response the response
     * @return result map
     */
    @PostMapping(value = "/destroySsoSessions")
    @ResponseBody
    public Map<String, Object> destroySsoSessions(@RequestParam(defaultValue = "ALL") final String type,
                                                  final HttpServletRequest request,
                                                  final HttpServletResponse response) {
        ensureEndpointAccessIsAuthorized(request, response);

        final Map<String, Object> sessionsMap = new HashMap<>();
        final Map<String, String> failedTickets = new HashMap<>();

        final SsoSessionReportOptions option = SsoSessionReportOptions.valueOf(type);
        final Collection<Map<String, Object>> collection = getActiveSsoSessions(option);
        collection.stream().map(sso -> sso.get(SsoSessionAttributeKeys.TICKET_GRANTING_TICKET.toString()).toString()).forEach(ticketGrantingTicket -> {
            try {
                this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
            } catch (final Exception e) {
                LOGGER.error(e.getMessage(), e);
                failedTickets.put(ticketGrantingTicket, e.getMessage());
            }
        });

        if (failedTickets.isEmpty()) {
            sessionsMap.put(STATUS, HttpServletResponse.SC_OK);
        } else {
            sessionsMap.put(STATUS, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            sessionsMap.put("failedTicketGrantingTickets", failedTickets);
        }
        return sessionsMap;
    }

    /**
     * Show sso sessions.
     *
     * @param request  the request
     * @param response the response
     * @return the model and view where json data will be rendered
     * @throws Exception thrown during json processing
     */
    @GetMapping
    public ModelAndView showSsoSessions(final HttpServletRequest request,
                                        final HttpServletResponse response) throws Exception {
        ensureEndpointAccessIsAuthorized(request, response);

        return new ModelAndView(VIEW_SSO_SESSIONS);
    }
}
