<?php declare(strict_types=1);

require_once __DIR__ . '/QrbLanguageChecker.php';
require_once __DIR__ . '/QrbPdfProcessor.php';
require_once __DIR__ . '/QrbMailProcessor.php';
require_once __DIR__ . '/QrbProcessStatus.php';
require_once __DIR__ . '/QrbProtocol.php';

require_once __DIR__ . '/../common/QrbDateTime.php';
require_once __DIR__ . '/../common/QrbTranslator.php';

require_once __DIR__ . '/../data/QrbProfileConfiguration.php';
require_once __DIR__ . '/../data/QrbProtocolEntry.php';

require_once __DIR__ . '/../storage/QrbStorageFactory.php';
require_once __DIR__ . '/../storage/QrbStorage.php';

// enumeration for the 3 process modes
enum QrbProcessMode
{
    case DryRun;
    case TestRun;
    case ProcessRun;
}

// Creates the QR bill PDFs and sends them via e-mail to the recipients.
final class QrbProcessor
{
    private QrbStorage              $qrbStorage;
    private QrbProfileConfiguration $qrbCfg;
    private QrbProcessStatus        $qrbStatus;
    private QrbTranslator           $qrbXl;
    
    // Creates an instance using the current environment and configuration.
    //
    // locale: if given the translation uses this locale, otherwise defaults to "en"
    public function __construct(?string $locale)
    {
        // Set Error Handler
        set_error_handler(
            function($errno, $errstr, $errfile, $errline)
            {
                throw new ErrorException($errstr, $errno, 0, $errfile, $errline);     
            }
        );
        
        $this->qrbStorage = QrbStorageFactory::getStorage();
        $this->qrbCfg     = $this->qrbStorage->getProfileConfiguration();
        $this->qrbStatus  = new QrbProcessStatus($this->qrbStorage);
        $this->qrbXl      = new QrbTranslator();

        if ($locale) $this->qrbXl->setLocale($locale);
    }
    
    // Processes all QR bills by creating but not storing them and preparing but not 
    // sending them via e-mail.
    public function dryRun() : void
    {
        $this->process(QrbProcessMode::DryRun);
    }

    // Processes one QR bill of a special RecipientsTestRun.csv by creating and storing it 
    // and sending it via e-mail to the given e-mail address.
    //
    // testEmailAddress: valid e-mail address
    public function testRun(string $testEmailAddress) : void
    {
        $this->process(QrbProcessMode::TestRun, $testEmailAddress);
    }

    // Processes all QR bills by creating and storing them and sending them via e-mail.
    public function processRun() : void
    {
        $this->process(QrbProcessMode::ProcessRun);
    }

    // Processes all QR bills by creating them and sending them via e-mail.
    //
    // QrbProcessMode:   DryRun     = do not store nor send the created QR bill PDFs
    //                   TestRun    = ignore Recipients.csv, just use the special 
    //                                RecipientsTestRun.csv and send the e-mail to testEmailAddress
    //                   ProcessRun = actual processing
    // testEmailAddress: must be not NULL if QrbProcessMode is TestRun
    private function process(QrbProcessMode $processMode, ?string $testEmailAddress = NULL) : void
    {
        $this->qrbStorage->writeLog("Starting QR bills processing with mode {$processMode->name} ...");
        $this->qrbStorage->writeLog("Configuration: {$this->qrbCfg}");
        
        $runName = QrbDateTime::createTimeStamp(new DateTimeImmutable());
        $this->qrbStorage->initializeNewRun($runName, 
                                            $processMode == QrbProcessMode::TestRun,
                                            $processMode == QrbProcessMode::DryRun);
        $protocol = new QrbProtocol($this->qrbStorage);
        
        try
        {
            // consistency check
            if ( (! $this->qrbCfg->getCreatePdf()) && (! $this->qrbCfg->getSendMail()) )
                throw new QrbException(static::class . " - Nothing to do: createPdf and sendMail are both false!",
                                       "exception.process.nothingToDo");
            if ( ($processMode == QrbProcessMode::TestRun) && ($testEmailAddress == NULL) )
                throw new QrbException(static::class . " - testEmailAddress not provided for TestRun",
                                       "exception.process.testEmailAddressNotProvided");
            
            $this->logDryRun($processMode);
            
            if ($processMode == QrbProcessMode::TestRun)
                $this->checkAndLogEmailAddress($testEmailAddress);
            if ($processMode == QrbProcessMode::DryRun)
                $this->checkLanguage();
        
            // create processors
            $pdf  = new QrbPdfProcessor($this->qrbStorage, $this->qrbCfg, 
                                        $processMode == QrbProcessMode::DryRun); 
            $pdf->init();
            $mail = new QrbMailProcessor($this->qrbStorage, $this->qrbCfg, 
                                         $runName,
                                         $processMode == QrbProcessMode::DryRun);
            $mail->init();

            // read recipients
            $recipients = $this->readRecipients($testEmailAddress);
            
            // start
            $msgPrefix = "[ {$this->qrbStorage->getCurrentProfile()} ] - ";
            $this->qrbStatus->write($msgPrefix . $this->qrbXl->xl('execution.start') . count($recipients));

            // for each recipient
            foreach ($recipients as $recipient)
            {
                $this->qrbStorage->writeLog("-- start of processing recipient #{$recipient->getIndex()}");
                $protcolEntry = $protocol->createEntry($recipient);
                
                $language = $this->evaluateLanguage($this->qrbCfg, $recipient);
                
                $pdfOutput = $this->processPdf($pdf, $recipient, $language);
                $protcolEntry->markPdfWritten($pdfOutput != NULL);
                
                $mailSent = $this->processMail($mail, $recipient, $language, $pdfOutput);
                $protcolEntry->markMailSent($mailSent);
                
                $protocol->write(); // write protocol for each entry (in case of a crash)
                $this->qrbStorage->writeLog("-- end of processing recipient #{$recipient->getIndex()}");
                
                if ($mailSent)
                {
                    $sleepTime = $this->qrbCfg->getSleepAfterSend();
                    $this->qrbStorage->writeLog("-- will sleep for {$sleepTime} microseconds");
                    usleep($sleepTime); // wait a bit
                }
            }

            $this->qrbStatus->write($msgPrefix . $this->qrbXl->xl('execution.success'));
        }
        catch (QrbException $e)
        {
            $msg = $this->qrbXl->xl($e->getLocalizeKey());
            $this->qrbStatus->write("<strong>{$msg}</strong> " . $e->getAdditionalInformation());
            $this->qrbStorage->writeLog("{$e}");
        }
        catch (\Throwable $th)
        {
            $msg1 = $this->qrbXl->xl("exception.unexpectedError1");
            $msg2 = $this->qrbXl->xl("exception.unexpectedError2");
            $this->qrbStatus->write($msg1 . $th->getMessage() . $msg2);
            $this->qrbStorage->writeLog("{$th}");
        }
        finally
        {
            $this->finalizeProtocol($protocol);
            $this->createZipLink($processMode);
            $this->qrbStorage->deleteOutputOfOldRuns();
            $this->qrbStorage->writeLog("processing finished");
        }
    }

    // Writes a special mark to the log if DryRun.
    private function logDryRun(QrbProcessMode $processMode) : void
    {
        if ($processMode == QrbProcessMode::DryRun)
        {
            $msg = $this->qrbXl->xl("execution.dryRun.start");
            $this->qrbStatus->write($msg);
        }
    }

    // If the given testEmailAddress is not NULL, checks whether testEmailAddress is a 
    // valid e-mail address and writes it to the status.
    private function checkAndLogEmailAddress(?string $testEmailAddress) : void
    {
        if ($testEmailAddress)
        {
            if (! filter_var($testEmailAddress, FILTER_VALIDATE_EMAIL) )
                throw new QrbException(static::class . " - Invalid e-mail address: '{$testEmailAddress}'");

            $this->qrbStatus->write($this->qrbXl->xl("execution.testRun.mailAddress") . "'{$testEmailAddress}'");
        }
    }

    // Checks whether all languages found in a recipient are present in all input files.
    private function checkLanguage() : void
    {
        $qrbLangCheck = new QrbLanguageChecker($this->qrbStorage, $this->qrbCfg);
        $qrbLangCheck->checkLanguage($this->qrbStatus, $this->qrbXl);
    }
    
    // returns recipients, array of QrbRecipient
    private function readRecipients(?string $testEmailAddress = NULL) : array // of QrbRecipient
    {
        $qrbDataType        = $testEmailAddress ? QrbDataType::TEST_RCPNTS : QrbDataType::RECIPIENTS;
        $amountMandatory    = $this->qrbCfg->getPrintAmount() == QrbProfileConfiguration::INDIVIDUAL_AMOUNT;
        $createPdf          = $this->qrbCfg->getCreatePdf();
        $referenceMandatory = $this->qrbCfg->getPrintPaymentSlip();    // in fact not true if no QR IBAN ...
        $sendMail           = $this->qrbCfg->getSendMail();

        $recipients = $this->qrbStorage->readRecipients($qrbDataType, 
                                                        $amountMandatory, 
                                                        $createPdf, 
                                                        $referenceMandatory, 
                                                        $sendMail);
        
        if ($testEmailAddress)
            $recipients[0]->setMailAddress($testEmailAddress);
        
        $this->qrbStorage->writeLog("Using recipients: ", $recipients);
        return $recipients;
    }
    
    // Returns the recipient's language if supported by the configured additional languages 
    // or the configured default language.
    //
    // qrbCfg:    holds additionalLanguages and defaultLanguage
    // recipient: holds recipient's language
    //
    // Returns language to use.
    // public for unit test
    public function evaluateLanguage(QrbProfileConfiguration $qrbCfg, 
                                     QrbRecipient     $recipient) : string
    {
        foreach ($qrbCfg->getAdditionalLanguages() as $language)
        {
            if ($recipient->getLanguage() == $language)
            {
                return $language;
            }
        }
        
        return $qrbCfg->getDefaultLanguage();
    }

    // Creates a QR bill for the given recipient and returns the PDF.
    //
    // Returns NULL if the PDF was not written.
    private function processPdf(QrbPdfProcessor       $pdf, 
                                QrbRecipient $recipient, 
                                string          $language) : ?string
    {
        $logPrefix = "#{$recipient->getIndex()}.a) ";
        
        if (! $this->qrbCfg->getCreatePdf())
        {
            $this->qrbStatus->write($logPrefix . $this->qrbXl->xl("execution.pdfCreatingSuppressed"));
            return NULL;
        }

        $pdfOutput = $pdf->process($recipient, $language);

        if (! $pdfOutput)
        {
            $this->qrbStatus->write($logPrefix . $this->qrbXl->xl("execution.pdfWritingSuppressed"));
            return NULL;
        }

        $this->qrbStatus->write($logPrefix 
                                . $this->qrbXl->xl("execution.pdfCreated1") 
                                . "'{$recipient->getLastName()} {$recipient->getFirstName()}'" 
                                . $this->qrbXl->xl("execution.pdfCreated2"));
        return $pdfOutput;
    }
    
    // Sends a mail with the given mail data to the given recipient, optionally with the 
    // given PDF as attachment (attachment only if pdfOutput is not NULL).
    private function processMail(QrbMailProcessor      $mail, 
                                 QrbRecipient $recipient, 
                                 string          $language, 
                                 ?string         $pdfOutput) : bool
    {
        $logPrefix = "#{$recipient->getIndex()}.b) ";
        
        if (! $this->qrbCfg->getSendMail())
        {
            $this->qrbStatus->write($logPrefix . $this->qrbXl->xl("execution.sendMailSuppressed"));
            return false;
        }
        
        if ($recipient->getMailAddress() === "")
        {
            $this->qrbStatus->write($logPrefix . $this->qrbXl->xl("execution.sendMailAddressEmpty"));
            return false;
        }

        $mailSent = $mail->send($recipient, $language, $pdfOutput);
        
        if (! $mailSent)
        {
            $this->qrbStatus->write($logPrefix . $this->qrbXl->xl("execution.sendMailSendSuppressed"));
            return false;
        }

        $this->qrbStatus->write($logPrefix 
                                . $this->qrbXl->xl("execution.sendMailSuccess")
                                . $recipient->getMailAddress());
        return $mailSent;
    }
    
    // Writes the protocol to disk and writes a link to the protocol file into the log.
    // This method must not throw as it is called by the finalize clause of the global try-catch statement. 
    private function finalizeProtocol(QrbProtocol $protocol) : void
    {
        try
        {
            $protocolLink = $protocol->write();
            
            if ($protocolLink)  // protocol is not stored if dryRun or protocol is empty
            {
                $msg1 = $this->qrbXl->xl("execution.protocolDownloadLink1");
                $msg2 = $this->qrbXl->xl("execution.protocolDownloadLink2");
                $this->qrbStatus->write($msg1 . $protocolLink . $msg2);
            }
        }
        catch (\Throwable $th)
        {
            $msg1 = $this->qrbXl->xl("exception.unexpectedError1");
            $msg2 = $this->qrbXl->xl("exception.unexpectedError2");
            $this->qrbStatus->write($msg1 . $th->getMessage() . $msg2);
            $this->qrbStorage->writeLog("{$th}");
        }
    }
    
    // Zips the result of the processing and writes a link to the created ZIP file into the log.
    // This method must not throw as it is called by the finalize clause of the global try-catch statement. 
    private function createZipLink(QrbProcessMode $processMode) : void
    {
        if ($processMode == QrbProcessMode::DryRun)  // no PDFs created during dry run
            return;

        if (!$this->qrbCfg->getCreatePdf())          // no PDFs created due to configuration setting
            return;

        try
        {
            $zipLink = $this->qrbStorage->zipCurrentRun();
            
            if ($zipLink)   // ZIP is not created if there were no files to zip
            {
                $msg1 = $this->qrbXl->xl("execution.zipDownloadLink1");
                $msg2 = $this->qrbXl->xl("execution.zipDownloadLink2");
                $this->qrbStatus->write($msg1 . $zipLink . $msg2);
            }
        }
        catch (\Throwable $th)
        {
            $msg1 = $this->qrbXl->xl("exception.unexpectedError1");
            $msg2 = $this->qrbXl->xl("exception.unexpectedError2");
            $this->qrbStatus->write($msg1 . $th->getMessage() . $msg2);
            $this->qrbStorage->writeLog("{$th}");
        }
    }
}

?>
