<?php

namespace Vanderbilt\REDCap\Classes\Fhir\DataMart;

use Vanderbilt\REDCap\Classes\Queue\Queue;
use Vanderbilt\REDCap\Classes\Queue\Message;
use Vanderbilt\REDCap\Classes\Parcel\PostMaster;
use Vanderbilt\REDCap\Classes\SystemMonitors\ResourceMonitor;
use Vanderbilt\REDCap\Classes\Traits\CanMakeDateTimeFromInterval;

class DataMartBackgroundRunner
{

    use CanMakeDateTimeFromInterval;

    /**
     * DataMart instance
     *
     * @var DataMart
     */
    private $datamart;

    /**
     * collect procesing errors
     *
     * @var \Exception[]
     */
    private $errors = [];

    /**
     * total MRN processed
     *
     * @var integer
     */
    private $mrnCounter = 0;

    /**
     *
     * @param DataMart $datamart
     */
    function __construct($datamart)
    {
        $this->datamart = $datamart;
    }

    const MAX_EXECUTION_TIME = '30 minutes';

    /**
     *
     * @param int $userId
     * @param DataMartRevision $revision
     * @param boolean $hasMore
     * @return void
     */
    public function sendBackgroundProcessCompleted($userId, $revision, $hasMore)
    {
        $project_id = $revision->project_id;
        $projectInfo = $this->datamart->getProjectInfo($project_id);
        $project_title = $projectInfo->app_title;
        $project_creator = $projectInfo->created_by;
        
        $toID = $userId ?: $project_creator;
        $title = "[Data Mart notification - PID {$project_id}]";

        $userInfo = \User::getUserInfoByUiid($toID);
        $to = $userInfo['username'] ?? '';

        $totalProcessedMrns = $this->getMrnCounter();
        
        $msg = '[This message was automatically generated by REDCap]'.PHP_EOL;
        $msg .= sprintf('The Data Mart revision ID `%u` has been processed in the project `%s` (ID %u).', $revision->id, $project_title, $project_id);
        if($this->hasErrors()) {
            $msg .= PHP_EOL."The following errors were logged:";
            foreach ($this->getErrors() as $error) {
                $msg .= sprintf("\t- %s".PHP_EOL, $error->getMessage());
            }
        }
        $pluralForm = ($totalProcessedMrns===1) ? '' : 's';
        $msg .= sprintf(PHP_EOL.'Data has been fetched for %u MRN%s.', $totalProcessedMrns, $pluralForm);
        if($hasMore) {
            $msg .= "Please note:\nthe process has been splitted for performance reasons; you will get another message as soon as the next chunk is completed.";
        }
        // $from = $project_creator;

        $postMaster = new PostMaster();
        $postMaster->sendParcel($to, $from='REDCap - DataMart', $subject=$title, $body=$msg);
        return;
    }

    public function incrementCounter()
    {
        $this->mrnCounter++;
    }

    public function getMrnCounter()
    {
        return $this->mrnCounter;
    }

    /**
     *
     * @param array $errors
     * @return void
     */
    public function addError($error) {
        $this->errors[] = $error;
    }

    public function getErrors()
    {
        return $this->errors;
    }

    /**
     *
     * @return boolean
     */
    public function hasErrors()
    {
        return count($this->errors)>0;
    }

    /**
     * schedule a process that will run a DataMart revision in background
     *
     * @param DataMartRevision $revision
     * @param array $mrn_list
     * @param boolean $sendFeedback
     * @return void
     * @throws \Exception
     */
    public function schedule($revision, $mrn_list=null, $sendFeedback=false)
    {
        $revisionId = $revision->id;
        $project_id = $revision->project_id;
        
        $queue = new Queue();
        $messageKey = sprintf('DataMart-%u (%u)', $project_id, $revisionId);
        
        // get any user in the project with a valid id if the one provided is not valid 
        $userID = $this->datamart->getUserId();

        $callable = function() use($userID, $revisionId, $mrn_list, $sendFeedback) {
            $datamart = new DataMart($userID);
            $bgRunner = new DataMartBackgroundRunner($datamart);
            $bgRunner->process($revisionId, $mrn_list, $sendFeedback);
        };
        
        // create a key to identify the message in the queue (different name for each project/revision)
        return $queue->addMessage($callable, $messageKey, $description='DataMart background fetch');
    }

    public function process($revisionId, $mrn_list=null, $sendFeedback=false) {
        $revision = DataMartRevision::get($revisionId);
        if($revision==false) throw new \Exception(sprintf('No revision found with the ID \'%u\'.', $revisionId), 404);

        // Determine if a single MRN was provided or if we should infer it during processing.
        $inferNext = false;
        if ($mrn_list === null || is_string($mrn_list)) {
            $inferNext = true;
            $mrn_list = $mrn_list === null ? [] : [$mrn_list]; // Convert single MRN to an array.
        } elseif (!is_array($mrn_list)) {
            throw new \Exception('Invalid MRN list format.'); // Handle cases where the provided MRN list is not valid.
        }

        $datamart = $this->datamart;
        $userId = $datamart->getUserId();
        $hasMore = false;
        
        $resourceMonitor = ResourceMonitor::create(['time' => self::MAX_EXECUTION_TIME]);
        
        do {
            $mrn = array_shift($mrn_list);
            $result = $datamart->runRevision($revision->id, $mrn);
            if($inferNext) {
                $nextMrn = $result['metadata']['next_mrn'] ?? null; // get the next MRN
                if($nextMrn) $mrn_list[] = $nextMrn;
            }

            // collect errors
            $hasErrors = $result['has_errors'] ?? false;
            if($hasErrors) {
                $errors = $result['errors'] ?? [];
                foreach ($errors as $error) $this->addError($error);
            }

            $resourcesOk = $resourceMonitor->checkResources();
            $shouldStop = !$resourcesOk;
            $hasMore = count($mrn_list)>0;
            if($mrn) $this->incrementCounter();
            if($shouldStop && $hasMore) {

                /**
                 * schedule a new task if the process is taking
                 * too much time and there are more MRNs to process
                 */
                $remainingOrNext = $inferNext ? $nextMrn : $mrn_list;
                $this->schedule($revision, $remainingOrNext, $sendFeedback);
            }
        } while ($hasMore && !$shouldStop);
        if($sendFeedback) {
            $this->sendBackgroundProcessCompleted($userId, $revision, $hasMore);
        }
    }
    
}

