// external inputs:
// User data (name and list of groups)
// List of PublisherDid's as param to some app-function (multi-cutout, merge)
// Database connection (from Settings)

// For Non-authenticated requests two behaviours possible:
// A, setups with configured security: return only PUBLIC data
// B, setups wihtout need of security: access all data
// Currently B supported: Vlkb security filters will always set UsePrincipal.
// Security filters could reserve 'anonymous' user for non-authenticated requests, if needed.
// So missing UserPrincipal is interpreted as setup without security filters - full access allowed.


import java.util.logging.Logger;

import java.io.PrintWriter;
import java.security.Principal;

import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.ListIterator;



public class AuthPolicy
{
   private static final Logger LOGGER = Logger.getLogger(AuthPolicy.class.getName());

   enum Access { PUBLIC_ONLY, PUBLIC_AND_AUTHORIZED_PRIVATE };
   private Access access;

   private String   userName;
   private String[] userGroups;
   private boolean userGroupsValid;

   private String dbConnUrl;
   private String dbUserName;
   private String dbPassword;


   public AuthPolicy(String userName, String[] userGroups)
   {
      this.userName   = userName;
      this.userGroups = userGroups;
      this.userGroupsValid = true;

      access = Access.PUBLIC_AND_AUTHORIZED_PRIVATE;

      LOGGER.finer("User [Groups]: " + userName + " [ " + String.join(" ", userGroups) + " ]" );
   }




   public AuthPolicy(Principal principal)
   {
      if(principal == null)
      {
         access = Access.PUBLIC_ONLY;
         userName = null;
         userGroups = null;
         userGroupsValid = false;
         LOGGER.finer("Non authenticated request (UserPrincipal null in HttpServletRequest)");
      }
      else
      {
         if(principal instanceof VlkbUser)
         {
            VlkbUser vlkbUser = (VlkbUser) principal;

            userName   = vlkbUser.getName();
            userGroups = vlkbUser.getGroupsAsArray();
            userGroupsValid = true;

            access = Access.PUBLIC_AND_AUTHORIZED_PRIVATE;

            LOGGER.finer("User [Groups]: " + userName + " [ " + String.join(" ", userGroups) + " ]" );
         }
         else
         {
            userName = principal.getName();
            LOGGER.finer("DBG principal not instance of VlkbUser, but has user-name: " + userName);
            userGroups = new String[]{""};//{"VLKB.groupA", "AllPrivate"}; // was for shiro
            userGroupsValid = true;
            access = Access.PUBLIC_AND_AUTHORIZED_PRIVATE;
            //throw new IllegalArgumentException("UserPrincipal is not of expected type");
         }
      }
   }



   public String getUserName()
   {
      return userName;
   }

   public boolean getUserGroupsValid()
   {
      return userGroupsValid;
   }


   public String[] getUserGroups()
   {
      return userGroups;
   }

   public String getUserGroupsSqlFormat()
   {
      if( (userGroups != null) && (userGroups.length > 0) )
      {
         return "\"" + String.join("\",\"" , userGroups) + "\"";
      }
      else
      {
         return null;
      }
   }

   public String getUserGroupsAsString(String separator)
   {
      if( (userGroups != null) && (userGroups.length > 0) )
      {
         return String.join(separator, userGroups);
      }
      else
      {
         return null;
      }
   }




   public String getAccessPolicy()
   {
      return access.name(); // returns enum as string
   }



   public void toXML(PrintWriter writer)
   {
      writer.println("<AccessPolicy>" + this.getAccessPolicy() + "</AccessPolicy>");
      String ug = getUserGroupsAsString(" ");
      if(userName   != null) writer.println("<UserName>" + userName + "</UserName>");
      if(ug         != null) writer.println("<GroupNames>" + ug + "</GroupNames>");
   }



   public String[] filterAuthorized(String[] pubdidArr, String dbConnUrl, String dbUserName, String dbPassword)
   {
      //this.dbConnUrl = dbConnUrl;
      this.dbUserName = dbUserName;
      this.dbPassword = dbPassword;

      LOGGER.finer("with String[] trace");
      return filterAuthorized(new ArrayList<String>(Arrays.asList(pubdidArr)), dbConnUrl);
   }

   private String[] filterAuthorized(ArrayList<String> pubdidList, String dbConnUrl)
   {
      LOGGER.fine("with List <String> trace");
      switch(access)
      {
         case PUBLIC_ONLY :
            filterNotPublic(pubdidList, dbConnUrl);
            break;

         case PUBLIC_AND_AUTHORIZED_PRIVATE :
            filterNotAuthorized(pubdidList, dbConnUrl);
            break;

         default :
            assert false : "Unrecoginzed  access : " + access;
      }
      return pubdidList.toArray(new String[0]); 
   }


   private void filterNotPublic(ArrayList<String> pubdids, String dbConnUrl)
   {
      LOGGER.fine("trace");
      assert pubdids != null;
      LOGGER.finer("PublisherDID list original : " + String.join(" ", pubdids));

      List<AuthPolicyDb.PubdidGroups> privateUniqPubdids = db_queryPrivateUniqPubdidGroups(dbConnUrl, pubdids);
      List<String> notAuthorizedUniqPubdids = pubdidsNotPublic(privateUniqPubdids, userGroups);

      LOGGER.finest("AuthZ removes: " + String.join(" ", notAuthorizedUniqPubdids));

      removeNotAuthorized(pubdids, notAuthorizedUniqPubdids);

      LOGGER.finest("PublisherDID list filtered : " + (pubdids.isEmpty() ? "" : String.join(" ", pubdids)));
   }


   private List<String> pubdidsNotPublic(List<AuthPolicyDb.PubdidGroups> pubdidList, String[] userGroups)
   {
      LOGGER.fine("trace");
      LOGGER.finer("userGroups: " + String.join(" ",userGroups));

      List<String> pubdidsNotAuthorizedList = new LinkedList<String>();
      ListIterator<AuthPolicyDb.PubdidGroups> it = pubdidList.listIterator();

      while (it.hasNext())
      {
         AuthPolicyDb.PubdidGroups pubdidGroups = it.next();

         LOGGER.finest(pubdidGroups.pubdid + " : " + String.join(" ",pubdidGroups.groups));

         if( true )// isIntersectionEmpty(pubdidGroups.groups, userGroups) )
         {
            pubdidsNotAuthorizedList.add(pubdidGroups.pubdid);
         }
      }

      return pubdidsNotAuthorizedList;
   }



   private void filterNotAuthorized(ArrayList<String> pubdids, String dbConnUrl)
   {
      LOGGER.fine("trace");
      assert pubdids != null;
      LOGGER.finer("PublisherDID list original : " + String.join(" ", pubdids));

      List<AuthPolicyDb.PubdidGroups> privateUniqPubdids = db_queryPrivateUniqPubdidGroups(dbConnUrl, pubdids);
      List<String> notAuthorizedUniqPubdids = pubdidsNotAuthorized(privateUniqPubdids, userGroups);

      LOGGER.finest("AuthZ removes: " + String.join(" ", notAuthorizedUniqPubdids));

      removeNotAuthorized(pubdids, notAuthorizedUniqPubdids);

      LOGGER.finest("PublisherDID list filtered : " + (pubdids.isEmpty() ? "" : String.join(" ", pubdids)));
   }



   private void removeNotAuthorized(ArrayList<String> pubdids, List<String> notAuthorizedUniqPubdids)
   {
      ListIterator<String> itr = pubdids.listIterator();
      while (itr.hasNext())
      {
         String pubdid = itr.next();

         for(String notAuthPubdid : notAuthorizedUniqPubdids)
         {
            if (pubdid.equals(notAuthPubdid)) itr.remove();
         }
      }

      return;
   }



   private List<AuthPolicyDb.PubdidGroups> db_queryPrivateUniqPubdidGroups(String dbConnUrl, List<String> pubdids)
   {
      AuthPolicyDb adb;
      synchronized(AuthPolicyDb.class)
      {
         //AuthPolicyDb.dbConnUrl  = this.dbConnUrl;
         AuthPolicyDb.dbUserName = this.dbUserName;
         AuthPolicyDb.dbPassword = this.dbPassword;

         adb = new AuthPolicyDb();
      }

      Set<String> uniqPubdids = new HashSet<String>(pubdids);

      if(uniqPubdids.isEmpty())
      {
         List<AuthPolicyDb.PubdidGroups> privatePubdidGroups = Collections.emptyList();
         return privatePubdidGroups;
      }
      else
      {
         // FIXME handle DB-exceptions
         List<AuthPolicyDb.PubdidGroups> privatePubdidGroups = adb.queryGroupsPrivateOnly(uniqPubdids);
         return privatePubdidGroups;
      }
   }



   private List<String> pubdidsNotAuthorized(List<AuthPolicyDb.PubdidGroups> pubdidList, String[] userGroups)
   {
      LOGGER.fine("trace");
      //LOGGER.finer("userGroups: " + String.join(" ",userGroups));

      List<String> pubdidsNotAuthorizedList = new LinkedList<String>();
      ListIterator<AuthPolicyDb.PubdidGroups> it = pubdidList.listIterator();

      while (it.hasNext())
      {
         AuthPolicyDb.PubdidGroups pubdidGroups = it.next();

         LOGGER.finest(pubdidGroups.pubdid + " : " + String.join(" ",pubdidGroups.groups));

         if( isIntersectionEmpty(pubdidGroups.groups, userGroups) )
         {
            pubdidsNotAuthorizedList.add(pubdidGroups.pubdid);
         }
      }

      return pubdidsNotAuthorizedList;
   }



   private boolean isIntersectionEmpty(String[] stringsA, String[] stringsB)
   {
      for(String strA : stringsA)
         for(String strB : stringsB)
         {
            if(strA.equals(strB))
            {
               return false;
            }
         }
      return true;
   }



}