package com.company.silentlogin.service;

import com.company.silentlogin.dao.SilentLoginDAO;
import com.company.silentlogin.SilentLoginProfile;
import com.company.security.service.TicketService;
import com.company.security.service.SecurityService;
import com.company.security.User;
import com.company.menu.service.MenuElementService;
import org.apache.log4j.Logger;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.InitializingBean;

import java.security.MessageDigest;
import java.util.*;

/**
 * This class provides an implementation for silent login trust services.
 *
 * The portal redirects to an outside vendor, but before doing so generates a ticket
 * based upon an app key and username which can be used for authentication by the
 * vendor when establishing a session.
 *
 * The vendor must digest the ticket with their app key and initiate authentication
 * through a url or a service endpoint submitting the username along with the digested
 * key.
 *
 * Capable of generating payload data through token identifiers (e.g. menu, roles).  
 * This data is returned by a view; presumably in XML format.
 */
public class SilentLoginServiceImpl implements SilentLoginService, InitializingBean
{
   private static final Logger LOGGER = Logger.getLogger("SILENTTICKETLOG");
   private static final String PREFIX = "SILENT";
   private static final int TICKET_LENGTH = 32;
   private static final String DEFAULT_ALGORITHM = "SHA";
   private static final String MENUS_TOKEN = "menus";
   private static final String ROLES_TOKEN = "roles";
   private static final String PROFILE_TOKEN = "profile";

   private String digestAlgorithm = DEFAULT_ALGORITHM;
   private SilentLoginDAO dao;
   private TicketService ticketGenerator;
   private MenuElementService menuElementService;
   private SecurityService securityService;
   private MessageDigest digester;

   /**
    * Sets the silent login ticket data access object.
    *
    * @param dao the DAO for silent login tickets.
    */
   public void setSilentLoginDAO(SilentLoginDAO dao)
   {
      this.dao = dao;
   }

   /**
    * Sets the ticket service used to generate tickets.
    *
    * @param ticketGenerator the ticket generator.
    */
   public void setTicketGenerator(TicketService ticketGenerator)
   {
      this.ticketGenerator = ticketGenerator;
   }

   /**
    * Sets the menu element service.
    *
    * @param menuElementService the menu element service implementation.
    */
   public void setMenuElementService(MenuElementService menuElementService)
   {
      this.menuElementService = menuElementService;
   }

   /**
    * Sets the security service implementation.
    *
    * @param securityService the security service implementation.
    */
   public void setSecurityService(SecurityService securityService)
   {
      this.securityService = securityService;
   }

   /**
    * Sets the digest algorithm.  Should be either MD5 or SHA.  This
    * must be consistent for all apps.
    *
    * @param algorithm the algorithm to set.
    */
   public void setDigestAlgorithm(String algorithm)
   {
      this.digestAlgorithm = algorithm;
   }

   /**
    * Establishes the digester with the configured algorithm.
    *
    * @throws Exception if the algorithm cannot be found.
    */
   public void afterPropertiesSet() throws Exception
   {
      digester = MessageDigest.getInstance(digestAlgorithm);
   }

   /**
    * @see SilentLoginService#register(String, String)
    */
   public String register(String appKey, String username)
   {
      String ticket = ticketGenerator.generateTicket(PREFIX, TICKET_LENGTH);
      dao.store(createDigestedKey(appKey, ticket), username);
      LOGGER.info(assembleRegistrationLogMessage("Registering", appKey, username, ticket));
      return ticket;
   }

   /**
    * @see SilentLoginService#authenticate(String, String)
    */
   public boolean authenticate(String digestedKey, String username)
   {
      String retrievedUsername = dao.find(digestedKey);
      if (retrievedUsername != null) {
         dao.remove(digestedKey);
      }
      boolean result = (retrievedUsername != null && retrievedUsername.equals(username));
      String action = (result ? "Successful" : "Failed") + " authentication for";
      LOGGER.info(assembleAuthenticationLogMessage(action, digestedKey, username));
      return result;
   }

   /**
    * Generates the payload for menus and roles tokens.  These two tokens would each
    * render lists respectively.  For menus, a list of MenuElements results.  For roles,
    * a list of string role names results.  These are all included in a map keyed by
    * the token name.
    *
    * @param locale the locale of the user.
    * @param username the username of the user.
    * @param payloadTokens the set of tokens corresponding to data being requested.
    * @return a Map keyed by token name and containing Lists.
    */
   public Map generatePayload(Locale locale, String username, Set payloadTokens)
   {
      Map payload = new HashMap();

      if (payloadTokens.contains(MENUS_TOKEN)) {
         payload.put(MENUS_TOKEN, menuElementService.getElementsForUser(locale, username));
      }

      if (payloadTokens.contains(ROLES_TOKEN)) {
         payload.put(ROLES_TOKEN, securityService.getAuthorizations(User.createUser(username)));
      }

      if (payloadTokens.contains(PROFILE_TOKEN)) {
         SilentLoginProfile profile = 
               SilentLoginProfile.create(securityService.getUser(User.createUser(username)));
         payload.put(PROFILE_TOKEN, profile);
      }      

      return payload;
   }

   /**
    * Creates a digest key from the application key and a ticket.
    *
    * @param appKey the application key.
    * @param ticket the one-time use ticket.
    * @return the String form of the digest key.
    */
   private String createDigestedKey(String appKey, String ticket)
   {
      String combined = appKey + ticket;
      return new String(Base64.encodeBase64(digester.digest(combined.getBytes())));
   }

   /**
    * Assembles a consistent log string given an action, an app key, a username,
    * and a ticket.
    *
    * @param action the action (e.g. Registering, Authenticating)
    * @param appKey the app key as subject of the action.
    * @param username the username as subject of the action.
    * @param ticket the ticket used.
    * @return a String used to output to the log.
    */
   private String assembleRegistrationLogMessage(String action, String appKey, String username, String ticket)
   {
      return action
            + "App Key ["
            + appKey
            + "] Username ["
            + username
            + "] with ticket ["
            + ticket
            + "]";
   }

   /**
    * Assembles a consistent log string given an action, an app key, a username,
    * and a ticket.
    *
    * @param action the action (e.g. Registering, Authenticating)
    * @param digestKey the digest key as subject of the action.
    * @param username the username as subject of the action.
    * @return a String used to output to the log.
    */
   private String assembleAuthenticationLogMessage(String action, String digestKey, String username)
   {
      return action
            + "Digest Key ["
            + digestKey
            + "] Username ["
            + username
            + "]";
   }   
}

