package performa.utils;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.search.*;
import oneit.appservices.config.ConfigMgr;
import oneit.appservices.documents.HTML2PDF;
import oneit.components.*;
import oneit.email.EmailFetcher;
import oneit.logging.*;
import oneit.objstore.*;
import oneit.objstore.services.*;
import oneit.security.SecUser;
import oneit.utils.*;
import oneit.utils.parsers.FieldException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import performa.orm.*;
import performa.orm.types.ApplicationStatus;


public class PerformaEmailFetcher implements Runnable, InitialisationParticipant
{
    private static final String         SERVER_NAME             =   ConfigMgr.getKeyfileString("imap.server.name");
    private static final String         SERVER_PORT             =   ConfigMgr.getKeyfileString("imap.server.port");
    private static final String         SERVER_PROTOCOL         =   ConfigMgr.getKeyfileString("imap.server.protocol", "imaps");
    private static final String         ACC_USER_NAME           =   ConfigMgr.getKeyfileString("imap.email.acc.username");
    private static final String         ACC_PASSWORD            =   ConfigMgr.getKeyfileString("imap.email.acc.password");
    private static final boolean        SYNC_ALL_EXISTING_EMAILS    =   ConfigMgr.getKeyfileBoolean("sync.all.existing.emails", false);
    private static final Integer        SYNC_EMAILS_SINCE_MINUTES   =   ConfigMgr.getKeyfileInt("sync.emails.since.minutes", 12*24*60);   //Applicable only when SYNC_ALL_EXISTING_EMAILS is false.
    public  static final LoggingArea    LOG =   LoggingArea.createLoggingArea("PerformaEmailFetcher");

    @Override
    public void run() 
    {
        LogMgr.log(LOG, LogLevel.PROCESSING1, "Inside Run of PerformaEmailFetcher");
        
        List<String>    visitedNodes    =   new ArrayList<>();
        
        runEmailFetcher(visitedNodes);
    }

    public static void runEmailFetcher(List<String> visitedNodes)
    {
        LogMgr.log(LOG, LogLevel.PROCESSING1, "Executing email fetcher Name:", SERVER_NAME, " Port:"+ SERVER_PORT, " Protocol:"+ SERVER_PROTOCOL, " UserName:", ACC_USER_NAME);
        LogMgr.log(LOG, LogLevel.PROCESSING1, "Email syncing details:: Sync All:", SYNC_ALL_EXISTING_EMAILS, " Sync from last:"+ SYNC_EMAILS_SINCE_MINUTES);
        
        if(StringUtils.subBlanks(SERVER_NAME) != null && StringUtils.subBlanks(SERVER_PORT) != null && 
            StringUtils.subBlanks(ACC_USER_NAME) != null && StringUtils.subBlanks(ACC_PASSWORD) != null)
        {
            final   Object[]  dummyContainer  =   new Object[1];
            Store   store   =   null;
                    
            try
            {
                TransactionServices.run(new TransactionTask()
                {
                    @Override
                    public void run(ObjectTransaction objTran) throws FieldException, StorageException
                    {
                        EmailMessage[]  emailMessages   =   EmailMessage.searchAll(objTran); 
                        
                        dummyContainer[0]   =   EmailMessage.pipesEmailMessage(emailMessages).toMessageId().uniqueVals();
                    }
                });
                
                Set<String> messageIDs  =   new HashSet();
                
                if(dummyContainer[0] != null)
                {
                    messageIDs  =   (Set<String>)dummyContainer[0];
                }
                
                Session session =   Session.getInstance(getEMailProperties());

                store   =   session.getStore(SERVER_PROTOCOL);
                store.connect(SERVER_NAME, ACC_USER_NAME, ACC_PASSWORD);

                LogMgr.log(LOG, LogLevel.PROCESSING1, "Mail connection done successfully");
                
                Folder[] folders = store.getDefaultFolder().list("*");
                
                for (Folder folder : folders) 
                {
                    if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0 && folder.getFullName().equalsIgnoreCase("INBOX")) 
                    {
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Processing folder " + folder.getFullName());
                        visitedNodes.add(folder.getFullName());
                        
                        processFolder(folder, messageIDs, visitedNodes);
                    }
                    else
                    {
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Skipping folder " + folder.getFullName());
                    }
                }
                
                LogMgr.log(LOG, LogLevel.PROCESSING1, "Email fetcher batch completed successfully.");
            }
            catch (Exception e)
            {
                LogMgr.log(LOG, LogLevel.SYSTEMERROR1, e, "Email Fetcher : error occurred");
            }
            finally
            {
                try
                {
                    if (store != null)
                    {
                        store.close();
                    }
                }
                catch (Exception e2)
                {
                    LogMgr.log(LOG, LogLevel.BUSINESS2, "Email Fetcher : Problem closing email store", e2);
                }
            }
        }
        else
        {
            LogMgr.log(LOG, LogLevel.PROCESSING1, "Email fetcher batch skipped as server propertied not defined.");
        }
    }
    
    private static Properties getEMailProperties()
    {
        Properties  properties  =   System.getProperties();

        // Mail-server-properties will be configured based on given emailServerProtocol i.e. imap(s) or pop3(s)
        properties.put("mail.store.protocol", SERVER_PROTOCOL);
        properties.put("mail." + SERVER_PROTOCOL +".host", SERVER_NAME);
        properties.put("mail." + SERVER_PROTOCOL +".port", SERVER_PORT);
        properties.put("mail." + SERVER_PROTOCOL +".starttls.enable", "true");

        return properties;
    }
    
    private static void processFolder(Folder folder, Set<String> messageIDs, List<String> visitedNodes) throws Exception
    {
        LogMgr.log(LOG, LogLevel.PROCESSING1, "processFolder called for ", folder != null ? folder.getName() : "");
        
        if(folder != null)
        {
            try
            {
                readMessagesInFolder(folder, messageIDs);

                for(Folder subfolder : folder.list())
                {
                    visitedNodes.add(subfolder.getFullName());
                    processFolder(subfolder, messageIDs, visitedNodes);
                }
            }
            catch (Exception e)
            {
                LogMgr.log(LOG, LogLevel.SYSTEMERROR1, e, "Email Fetcher : error occurred");
                throw e;
            }
            finally
            {
                try
                {
                    folder.close(true);
                }
                catch (Exception ex2)
                {
                    LogMgr.log(LOG, LogLevel.BUSINESS2, "Email Fetcher : Problem closing email store", ex2);
                }
            }
        }
        
        LogMgr.log(LOG, LogLevel.PROCESSING1, "processFolder completed for ", folder != null ? folder.getName() : "");
    }
    
    private static void readMessagesInFolder(Folder folder, Set<String> messageIDs) throws MessagingException
    {
        LogMgr.log(LOG, LogLevel.PROCESSING1, "readMessagesInFolder called for ", folder.getName());
        
        UIDFolder   uidFolder   =   (UIDFolder)folder;
        
        try
        {
            folder.open(Folder.READ_ONLY);
        }
        catch(Exception ex)
        {
            LogMgr.log(LOG, LogLevel.PROCESSING1, "Skipping messages for folder ", folder.getName(), " as unable to open same.");
            return;
        }
        
        javax.mail.Message[]   messages;
        SearchTerm  searchTerm  =   getSearchTerm();
        
        if(searchTerm == null)
        {
            messages    =   folder.getMessages();
        }
        else
        {
            messages    =   folder.search(searchTerm);
        }
        
        LogMgr.log(LOG, LogLevel.PROCESSING1, "Number of messages to be processed in ", folder.getName(), ": ", messages.length);
        
        for(javax.mail.Message message : messages)
        {
            processMessage(message, uidFolder, messageIDs);
        }
        LogMgr.log(LOG, LogLevel.PROCESSING1, "readMessagesInFolder completed for ", folder.getName());
    }
    
    private static SearchTerm getSearchTerm ()
    {
        if(!SYNC_ALL_EXISTING_EMAILS)
        {
            SearchTerm  receivedFrom    =   new ReceivedDateTerm(ComparisonTerm.GE, getSyncingFromDate());
            SearchTerm  sentFrom        =   new SentDateTerm(ComparisonTerm.GE, getSyncingFromDate());

            return new AndTerm(receivedFrom, sentFrom);
        }
        return null;
    }
    
    public static void processMessage(javax.mail.Message message, UIDFolder uidFolder, final Set<String> messageIDs)
    {
        LogMgr.log(LOG, LogLevel.PROCESSING2 , "processMessage called");
        
        String  tmpFromAdress   =   null;
        String  tmpFromPerson   =   null;
        String  tmpReplyTo      =   null;
        String  tmpFirstName    =   null;
        String  tmpLastName     =   null;
        String  tmpEmailText;
        String  tmpSubject;
        Date    tmpSentDate;
        String  tmpRecipient    =   null;
        String  tmpJobId        =   null;
        String  tmpMessageID    =   null;
        String  tmpMessageBody  =   null;
        
        List<FileBinaryContent> tmpContents;

        final String fromAddress;
        final String firstName;
        final String lastName;
        final String replyTo;
        final String recipient;
        final String subject;
        final Date  sentDate;
        final String emailText;
        final String jobIdentifier;
        final String messageID;
        final String messageBody;
        final List<FileBinaryContent> contents;

        try
        {
            if(message.getFrom() != null && message.getFrom().length > 0)
            {
                tmpFromAdress   =   ((InternetAddress) message.getFrom()[0]).getAddress();
            }
            
            tmpSubject      =   message.getSubject();
            tmpSentDate     =   message.getReceivedDate();
            tmpMessageID    =   ((MimeMessage)message).getMessageID();
            tmpMessageBody  =   EmailFetcher.getText(message, new ArrayList());

            if(message.getReplyTo() != null && message.getReplyTo().length > 0)
            {
                tmpReplyTo      =   ((InternetAddress) message.getReplyTo()[0]).getAddress();
                tmpFromPerson   =   ((InternetAddress) message.getReplyTo()[0]).getPersonal(); 
                
                if(tmpReplyTo != null && tmpReplyTo.equals("noreply@jobapplications.seek.com.au"))
                {
                    tmpFromPerson   =    getApplicantName(tmpFromPerson);
                    
                    Document    document        =   Jsoup.parse(tmpMessageBody);
                    Element     atag            =   document.select("a[title*=Email]").first();

                    tmpReplyTo      =   atag.text();
                }
                
                tmpFirstName    =   formatName(getFirstName(tmpFromPerson));
                tmpLastName     =   formatName(getLastName(tmpFromPerson));
            }
            
            LogMgr.log(LOG, LogLevel.PROCESSING2 , "Mail Subject:" + tmpSubject, " Address:", tmpFromAdress, " MessageId:", tmpMessageID);
            LogMgr.log(LOG, LogLevel.PROCESSING2 , "Mail Details: Received ", message.getReceivedDate(), " Sent:", message.getSentDate());
            
            if(message.getAllRecipients() != null && message.getAllRecipients().length > 0)
            {
                for(Address receipientAddress : message.getAllRecipients())
                {
                    tmpRecipient    =   ((InternetAddress)receipientAddress).getAddress();
                    
                    if(StringUtils.subBlanks(tmpRecipient) != null && tmpRecipient.contains("job"))
                    {
                        tmpJobId    =   getJobIdentifierFromEmail(tmpRecipient);
                        
                        if(tmpJobId != null && !tmpJobId.isEmpty() && StringUtils.isNumber(tmpJobId))
                        {
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Identified Job ID: " , tmpJobId);
                            break;
                        }
                    }
                }
            }            
            
            tmpEmailText    =   EmailFetcher.getText(message, new ArrayList<>());
            tmpContents     =   getAttachments(message);
        }
        catch (MessagingException | IOException ex)
        {
            LogMgr.log(LOG, LogLevel.PROCESSING2 , "Batch Email Fetcher : Mail message composing error", ex);
            throw new NestedException(ex);
        }

        fromAddress         =   tmpFromAdress;
        firstName           =   tmpFirstName;
        lastName            =   tmpLastName;
        replyTo             =   tmpReplyTo;
        subject             =   tmpSubject;
        sentDate            =   tmpSentDate;
        jobIdentifier       =   tmpJobId;
        recipient           =   tmpRecipient;
        contents            =   tmpContents;
        emailText           =   tmpEmailText;
        messageID           =   tmpMessageID;
        messageBody         =   tmpMessageBody;
        
        try
        {
            if(!messageIDs.contains(messageID))
            {
                TransactionServices.run(new TransactionTask()
                {
                    @Override
                    public void run(ObjectTransaction objTran) throws FieldException, StorageException
                    {
                        if (jobIdentifier != null && !jobIdentifier.isEmpty() && StringUtils.isNumber(jobIdentifier))
                        {
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Starting to process message for job id: " , jobIdentifier);
                            
                            Job     job =   Job.getJobByID(objTran, Long.parseLong(jobIdentifier));

                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Retrived Job :" , job);
                            
                            if (job != null)
                            {
                                createEmailMessage(objTran, job);
                            
                                if(fromAddress.contains("indeedemail.com"))
                                {
                                    handleIndeedEmail(objTran, job);
                                }
                                else if(fromAddress.contains("seek.com.au"))
                                {
                                    // handle seek email
                                    if(replyTo == null || replyTo.isEmpty())
                                    {
                                        // email body is checked - retriving a tag with title="Email applicant name"
                                        return;
                                    }

                                    handleSeekEmail(objTran, job);
                                }
                            }
                        }
                    }

                    private void handleSeekEmail(ObjectTransaction objTran, Job job) throws StorageException, FieldException 
                    {
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Starting to handle Seek email");
                        
                        SecUser secUser =   SecUser.searchNAME(objTran, replyTo);
                        boolean newUser =   false;
                        
                        if(secUser == null)
                        {
                            newUser =   true;
                            secUser =   SecUser.createSecUser(objTran);
                            
                            secUser.setUserName(replyTo);
                            secUser.setEmail(replyTo);
                            secUser.setAttribute("md5:" + SecUser.FIELD_Password, CompanyUser.DEFAULT_PASSWORD);
                            secUser.addRole(Utils.getRole(Utils.ROLE_APPLICANT, objTran));
                            secUser.setFirstName(firstName);
                            secUser.setLastName(lastName);
                            
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Created new sec user : " , secUser);
                        }
                        
                        Candidate   candidate   =   secUser.getExtensionOrCreate(Candidate.REFERENCE_Candidate);
                        
                        if(newUser)
                        {
                            candidate.setIsEmailIngest(true);
                        }
                        
                        JobApplication  jobApplication  =   JobApplication.createNewApplication(candidate, job);
                        
                        jobApplication.setApplicationStatus(ApplicationStatus.POST_INGEST);
                        jobApplication.setIsEmailIngest(true);
                        
                        if(contents.size() > 0)
                        {
                            FileBinaryContent   cv  =   contents.get(0);

                            if(cv.getContentType() != null && Utils.isValidContentType(cv.getContentType().toLowerCase()))
                            {
                                jobApplication.setCV(cv);
                                LogMgr.log(LOG, LogLevel.PROCESSING1, "Attaching cv of content type : " , cv.getContentType() , " for job application : ", jobApplication);
                            }
                        }
                        
                        if(contents.size() > 1)
                        {
                            FileBinaryContent   coverLetter =   contents.get(1);

                            if(coverLetter.getContentType() != null && Utils.isValidContentType(coverLetter.getContentType().toLowerCase()))
                            {
                                jobApplication.setCoverLetter(contents.get(1));
                                LogMgr.log(LOG, LogLevel.PROCESSING1, "Attaching cover letter of content type : " , coverLetter.getContentType() , " for job application : ", jobApplication);
                            }
                        }
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Finished handling Seek email");
                    }

                    private void handleIndeedEmail(ObjectTransaction objTran, Job job) throws StorageException, FieldException 
                    {
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Starting to handle indeed email");
                        // handle indeed email
                        Candidate   candidate   =   Candidate.searchAlias(objTran, fromAddress);
                        SecUser     secUser     =   SecUser.searchNAME(objTran, fromAddress);
                        boolean     newUser     =   false;
                        
                        if(candidate != null)
                        {
                            secUser =   candidate.getUser();
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Candidate exists. Candidate :", candidate);
                        }
                        
                        if(secUser == null)
                        {
                            secUser =   SecUser.createSecUser(objTran);
                            newUser =   true;
                            
                            secUser.setUserName(fromAddress);
                            secUser.setEmail(fromAddress);
                            secUser.setAttribute("md5:" + SecUser.FIELD_Password, CompanyUser.DEFAULT_PASSWORD);
                            secUser.addRole(Utils.getRole(Utils.ROLE_APPLICANT, objTran));
                            secUser.setFirstName(firstName);
                            secUser.setLastName(lastName);
                            
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Created new sec user : " , secUser);
                        }
                        
                        if(candidate == null)
                        {
                            candidate   =   secUser.getExtensionOrCreate(Candidate.REFERENCE_Candidate);
                        }
                        
                        if(newUser)
                        {
                            candidate.setIsEmailIngest(true);
                            candidate.setIsMaskedEmail(true);
                        }
                        
                        JobApplication  jobApplication  =   JobApplication.createNewApplication(candidate, job);
                        
                        jobApplication.setApplicationStatus(ApplicationStatus.POST_INGEST);
                        jobApplication.setIsEmailIngest(true);
                        
                        if(contents.size() > 0 )
                        {
                            FileBinaryContent   cv  =   contents.get(0);

                            if(cv.getContentType() != null && Utils.isValidContentType(cv.getContentType().toLowerCase()))
                            {
                                jobApplication.setCV(cv);
                                LogMgr.log(LOG, LogLevel.PROCESSING1, "Attaching cv of content type : " , cv.getContentType());
                            }
                        }
                        
                        Document document = Jsoup.parse(messageBody);
                        
                        Element table = document.select("table").size() > 14 ? document.select("table").get(14) : null;
                        
                        if(table != null)
                        {
                            Element                 firstTR =   table.select("tr").first();
                            ByteArrayOutputStream   out     =   new ByteArrayOutputStream();

                            HTML2PDF.HTML2PDF(firstTR.toString() , out, "");
                            
                            FileBinaryContent   binaryContent   =   new FileBinaryContent("application/pdf", out.toByteArray(), "CoverLetter");

                            jobApplication.setCoverLetter(binaryContent);
                            
                            LogMgr.log(LOG, LogLevel.PROCESSING1, "Created cover letter for job application : ", jobApplication);
                        }
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Finished handling indeed email");
                    }

                    private void createEmailMessage(ObjectTransaction objTran, Job job) throws StorageException, FieldException 
                    {
                        EmailMessage    emailMessage    =   EmailMessage.createEmailMessage(objTran);
                        
                        emailMessage.setMessageId(messageID);
                        emailMessage.setEmailFrom(fromAddress);
                        emailMessage.setEmailTo(recipient);
                        emailMessage.setSubject(subject);
                        emailMessage.setDescription(emailText);
                        emailMessage.setReceivedDate(sentDate);
                        emailMessage.setJob(job);
                        
                        for (FileBinaryContent content : contents)
                        {
                            Attachment  attachment  =   Attachment.createAttachment(objTran);
                            
                            attachment.setAttachmentFile(content);
                            emailMessage.addToAttachments(attachment);
                        }
                        
                        LogMgr.log(LOG, LogLevel.PROCESSING1, "Created Email Message : " , emailMessage);
                    }
                });
                
                LogMgr.log(LOG, LogLevel.PROCESSING2 , "processMessage completed successfully");
                messageIDs.add(messageID);
            }
            else
            {
                LogMgr.log(LOG, LogLevel.PROCESSING2 , "Message already synced or doesnt have valid sent time ", sentDate);
            }
        }
        catch (Exception e)
        {
            LogMgr.log(LOG, LogLevel.PROCESSING2 , e, "Batch Email Fetcher : Error in creating the email message ");
        }
    }

    private static String getFirstName(String name) 
    {
        return ((name.split("\\w+").length > 1) &&  (name.lastIndexOf(' ') > 0 )) ? name.substring(0, name.lastIndexOf(' ')) : name;
    }
    
    private static String getLastName(String name) 
    {
        return ((name.split("\\w+").length > 1) &&  (name.lastIndexOf(' ') > 0 )) ? name.substring(name.lastIndexOf(" ")+1) : "";
    }
    
    private static String formatName(String name)
    {
        return name.length() > 1 ? name.substring(0,1).toUpperCase() + name.substring(1).toLowerCase() : name;
    }
    
    public static String getJobIdentifierFromEmail(String strReceipient)
    {
        int index = strReceipient.indexOf('@');

        return strReceipient.substring(0, index).replace("job", "");
    }
    
    public static String getApplicantName(String strPerson)
    {
        return strPerson.replace("via SEEK", "").trim();
    }
    
    private static List<FileBinaryContent> getAttachments(javax.mail.Message message) throws IOException, MessagingException
    {
        List<FileBinaryContent> contents    =   new ArrayList<>();
        
        if(message.getContentType() != null && message.getContentType().contains("multipart"))
        {
            Multipart               multipart   =   (Multipart)message.getContent();
            byte[]                  bytes       =   null;

            for (int j = 0; j < multipart.getCount(); j++) 
            {
                BodyPart bodyPart = multipart.getBodyPart(j);

                if(Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) && bodyPart.getFileName() != null && !bodyPart.getFileName().isEmpty()) 
                {
                    bytes   =   IOUtils.readInputStreamToBytes(bodyPart.getInputStream());

                    FileBinaryContent binaryContent = new FileBinaryContent(bodyPart.getContentType(), bytes, bodyPart.getFileName());
                    contents.add(binaryContent);
                }
            }
        }
        return contents;
    }
    
    private static Date getSyncingFromDate()
    {
        return DateDiff.before(new Date(), SYNC_EMAILS_SINCE_MINUTES, Calendar.MINUTE);
    }
    
    
    @Override
    public void init (ParticipantInitialisationContext context) throws InitialisationException
    {   
        
    }    

    public static final void main (String[] args) throws Exception
    {
        Properties props = System.getProperties();
        props.setProperty("mail.store.protocol", "imap");
        props.setProperty("mail.imap.starttls.enable", "true");
        Session session = Session.getDefaultInstance(props, null);
        Store store = session.getStore("imap");
        
        session.setDebug(true);
        store.connect("imap.oneit.com.au", args[0], args[1]);
        //store.connect("p3plcpnl0293.prod.phx3.secureserver.net", 143, args[0], "password123");
        
        System.out.println(store);

        Folder[] f = store.getDefaultFolder().list();
        for(Folder fd:f)
        {
            System.out.println(">> "+fd.getName());    
        }
        
        BufferedReader  br = new BufferedReader(new InputStreamReader(System.in));
        
        while (true)
        {
            String folderName = br.readLine();
            
            folderName = folderName.trim();
            
            Folder folder = store.getFolder(folderName);
            
            for(Folder fd:folder.list())
            {
                System.out.println(">> "+fd.getName());    
            }
        }
    }
}
