<?php
use Cake\ORM\TableRegistry;
use Cake\Filesystem\File;
use Cake\Http\Exception\NotFoundException;

require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php'); 

class LocalToolInboxProcessor extends GenericInboxProcessor
{
    protected $scope = 'LocalTool';
    protected $action = 'not-specified'; //overriden when extending
    protected $description = ''; // overriden when extending
    protected $registeredActions = [
        'IncomingConnectionRequest',
        'AcceptedRequest',
        'DeclinedRequest',
    ];
    protected $processingTemplate = 'LocalTool/GenericRequest';
    protected $Broods;
    protected $LocalTools;

    public function __construct($loadFromAction=false)
    {
        parent::__construct($loadFromAction);
        $this->Broods = TableRegistry::getTableLocator()->get('Broods');
        $this->LocalTools = TableRegistry::getTableLocator()->get('LocalTools');
    }

    public function create($requestData)
    {
        return parent::create($requestData);
    }

    protected function updateProcessingTemplate($request)
    {
        $connectorName = $request->connector['connector'];
        $processingTemplatePath = sprintf('%s/%s/%s.php', $this->scope, $connectorName, $this->action);
        $file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath);
        if ($file->exists()) {
            $this->processingTemplate = str_replace('.php', '', $processingTemplatePath);
        }
        $file->close();
    }

    protected function validateConnectorName($requestData)
    {
        if (empty($requestData['data']['connectorName'])) {
            throw new NotFoundException('Error while validating request data. Connector name is missing.');
        }
        $connector = $this->getConnectorFromClassname($requestData['data']['connectorName']);
        if (is_null($connector)) {
            throw new NotFoundException(__('Error while validating request data. Unkown connector `{0}`', $requestData['data']['connectorName']));
        }
    }

    protected function getIssuerBrood($request)
    {
        $brood = $this->Broods->find()
            ->where(['url' => $request['origin']])
            ->first();
        return $brood;
    }

    protected function getConnection($requestData)
    {
        $local_tool_id = $requestData['remote_tool_id']; // local_tool_id is actually the remote_tool_id for the sender
        $connection = $this->LocalTools->find()->where(['id' => $local_tool_id])->first();
        return $connection;
    }

    protected function filterAlignmentsForBrood($individual, $brood)
    {
        foreach ($individual->alignments as $i => $alignment) {
            if ($alignment->organisation_id != $brood->organisation_id) {
                unset($individual->alignments[$i]);
            }
        }
        return $individual;
    }

    protected function getConnector($request)
    {
        try {
            $connectorClasses = $this->LocalTools->getConnectors($request->local_tool_connector_name);
            if (!empty($connectorClasses)) {
                $connector = array_values($connectorClasses)[0];
            }
        } catch (NotFoundException $e) {
            $connector = null;
        }
        return $connector;
    }

    protected function getConnectorMeta($request)
    {
        try {
            $className = $request->local_tool_connector_name;
            $connector = $this->getConnectorFromClassname($className);
            $connectorMeta = $this->LocalTools->extractMeta([$className => $connector])[0];
        } catch (NotFoundException $e) {
            $connectorMeta = [];
        }
        return $connectorMeta;
    }

    protected function getConnectorFromClassname($className)
    {
        try {
            $connectorClasses = $this->LocalTools->getConnectors($className);
            if (!empty($connectorClasses)) {
                $connector = array_values($connectorClasses)[0];
            }
        } catch (NotFoundException $e) {
            $connector = null;
        }
        return $connector;
    }

    protected function getConnectorMetaFromClassname($className)
    {
        try {
            $connector = $this->getConnectorFromClassname($className);
            $connectorMeta = $this->LocalTools->extractMeta([$className => $connector])[0];
        } catch (NotFoundException $e) {
            $connectorMeta = [];
        }
        return $connectorMeta;
    }

    protected function attachRequestAssociatedData($request)
    {
        $request->brood = $this->getIssuerBrood($request);
        $request->connector = $this->getConnectorMeta($request);
        $request->individual = $request->user->individual;
        $request->individual = $this->filterAlignmentsForBrood($request->individual, $request->brood);
        return $request;
    }

    protected function genBroodParam($remoteCerebrate, $connection, $connector, $requestData)
    {
        $local_tool_id = $requestData['remote_tool_id']; // local_tool_id is actually the remote_tool_id for the sender
        $remote_tool_id = $requestData['local_tool_id']; // remote_tool_id is actually the local_tool_id for the sender
        $remote_org = $this->Broods->Organisations->find()->where(['id' => $remoteCerebrate->organisation_id])->first();
        return [
            'remote_tool' => [
                'id' => $remote_tool_id,
                'connector' => $connector->connectorName,
                'name' => $requestData['tool_name'],
            ],
            'remote_org' => $remote_org,
            'remote_tool_data' => $requestData,
            'remote_cerebrate' => $remoteCerebrate,
            'connection' => $connection,
            'connector' => [$connector->connectorName => $connector],
        ];
    }

    protected function addBaseValidatorRules($validator)
    {
        return $validator
            ->requirePresence('connectorName')
            ->notEmptyString('connectorName', 'The connector name must be provided')
            ->requirePresence('cerebrateURL')
            ->notEmptyString('cerebrateURL', 'A url must be provided')
            ->requirePresence('local_tool_id')
            ->numeric('local_tool_id', 'A local_tool_id must be provided')
            ->requirePresence('remote_tool_id')
            ->numeric('remote_tool_id', 'A remote_tool_id must be provided');
            // ->add('url', 'validFormat', [
            //     'rule' => 'url',
            //     'message' => 'URL must be valid'
            // ]);
    }
}

class IncomingConnectionRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI {
    public $action = 'IncomingConnectionRequest';
    protected $description;

    public function __construct() {
        parent::__construct();
        $this->severity = $this->Inbox::SEVERITY_WARNING;
        $this->description = __('Handle Phase I of inter-connection when another cerebrate instance performs the request.');
    }

    protected function addValidatorRules($validator)
    {
        return $this->addBaseValidatorRules($validator);
    }
    
    public function create($requestData) {
        $this->validateConnectorName($requestData);
        $this->validateRequestData($requestData);
        $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']);
        $requestData['title'] = __('Request for {0} Inter-connection', $connectorMeta['name']);
        return parent::create($requestData);
    }

    public function getViewVariables($request)
    {
        $request = $this->attachRequestAssociatedData($request);
        return [
            'request' => $request,
            'progressStep' => 0,
        ];
    }

    public function process($id, $requestData, $inboxRequest)
    {
        /**
         * /!\ Should how should sent message be? be fire and forget? Only for delined?
         */
        $interConnectionResult = [];
        $remoteCerebrate = $this->getIssuerBrood($inboxRequest);
        $connector = $this->getConnector($inboxRequest);
        if (!empty($requestData['is_discard'])) { // -> declined
            $connectorResult = $this->declineConnection($connector, $remoteCerebrate, $inboxRequest['data']); // Fire-and-forget?
            $connectionSuccessfull = !empty($connectorResult['success']);
            $resultTitle = __('Could not sent declined message to `{0}`\'s  for {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
            $errors = [];
            if ($connectionSuccessfull) {
                $resultTitle = __('Declined message successfully sent to `{0}`\'s for {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
                $this->discard($id, $inboxRequest);
            }
        } else {
            $errors = [];
            $connectorResult = [];
            $thrownErrorMessage = '';
            try {
                $connectorResult = $this->acceptConnection($connector, $remoteCerebrate, $inboxRequest['data']);
                $connectionSuccessfull = !empty($connectorResult['success']);
            } catch (\Throwable $th) {
                $connectionSuccessfull = false;
                $thrownErrorMessage = $th->getMessage();
            }
            $resultTitle = $connectorResult['message'] ?? __('Could not inter-connect `{0}`\'s {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
            $errors = $connectorResult['errors'] ?? $thrownErrorMessage;
            if ($connectionSuccessfull) {
                $resultTitle = __('Interconnection for `{0}`\'s {1} created', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
            }
            if ($connectionSuccessfull || !empty($connectorResult['placed_in_outbox'])) {
                $this->discard($id, $inboxRequest);
            }
        }
        return $this->genActionResult(
            $connectorResult,
            $connectionSuccessfull,
            $resultTitle,
            $errors
        );
    }

    public function discard($id, $requestData)
    {
        return parent::discard($id, $requestData);
    }

    protected function acceptConnection($connector, $remoteCerebrate, $requestData)
    {
        $connection = $this->getConnection($requestData);
        $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData);
        $connectorResult = $connector->acceptConnectionWrapper($params);
        $response = $this->sendAcceptedRequestToRemote($params, $connectorResult);
        return $response;
    }

    protected function declineConnection($connector, $remoteCerebrate, $requestData)
    {
        $connection = $this->getConnection($requestData);
        $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData);
        $connectorResult = $connector->declineConnectionWrapper($params);
        $response = $this->sendDeclinedRequestToRemote($params, $connectorResult);
        return $response;
    }

    protected function sendAcceptedRequestToRemote($params, $connectorResult)
    {
        $response = $this->Broods->sendLocalToolAcceptedRequest($params, $connectorResult);
        return $response;
    }

    protected function sendDeclinedRequestToRemote($remoteCerebrate, $connectorResult)
    {
        $response = $this->Broods->sendLocalToolDeclinedRequest($params, $connectorResult);
        return $response;
    }
}

class AcceptedRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI {
    public $action = 'AcceptedRequest';
    protected $description;

    public function __construct() {
        parent::__construct();
        $this->severity = $this->Inbox::SEVERITY_WARNING;
        $this->description = __('Handle Phase II of inter-connection when initial request has been accepted by the remote cerebrate.');
    }

    protected function addValidatorRules($validator)
    {
        return $this->addBaseValidatorRules($validator);
    }
    
    public function create($requestData) {
        $this->validateConnectorName($requestData);
        $this->validateRequestData($requestData);
        $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']);
        $requestData['title'] = __('Inter-connection for {0} has been accepted', $connectorMeta['name']);
        return parent::create($requestData);
    }

    public function getViewVariables($request)
    {
        $request = $this->attachRequestAssociatedData($request);
        return [
            'request' => $request,
            'progressStep' => 1,
        ];
    }

    public function process($id, $requestData, $inboxRequest)
    {
        $connector = $this->getConnector($inboxRequest);
        $remoteCerebrate = $this->getIssuerBrood($inboxRequest);

        $errors = [];
        $connectorResult = [];
        $thrownErrorMessage = '';
        try {
            $connectorResult = $this->finaliseConnection($connector, $remoteCerebrate, $inboxRequest['data']);
            $connectionSuccessfull = !empty($connectorResult['success']);
        } catch (\Throwable $th) {
            $connectionSuccessfull = false;
            $errors = $th->getMessage();
        }
        $resultTitle = __('Could not finalise inter-connection for `{0}`\'s {1}', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
        $errors = $connectorResult['errors'] ?? $thrownErrorMessage;
        if ($connectionSuccessfull) {
            $resultTitle = __('Interconnection for `{0}`\'s {1} finalised', $inboxRequest['origin'], $inboxRequest['local_tool_name']);
            $this->discard($id, $requestData);
        }
        return $this->genActionResult(
            $connectorResult,
            $connectionSuccessfull,
            $resultTitle,
            $errors
        );
    }

    public function discard($id, $requestData)
    {
        return parent::discard($id, $requestData);
    }

    protected function finaliseConnection($connector, $remoteCerebrate, $requestData)
    {
        $connection = $this->getConnection($requestData);
        $params = $this->genBroodParam($remoteCerebrate, $connection, $connector, $requestData);
        $connectorResult = $connector->finaliseConnectionWrapper($params);
        return [
            'success' => true
        ];
    }
}

class DeclinedRequestProcessor extends LocalToolInboxProcessor implements GenericInboxProcessorActionI {
    public $action = 'DeclinedRequest';
    protected $description;

    public function __construct() {
        parent::__construct();
        $this->severity = $this->Inbox::SEVERITY_WARNING;
        $this->description = __('Handle Phase II of MISP inter-connection when initial request has been declined by the remote cerebrate.');
    }

    protected function addValidatorRules($validator)
    {
        return $this->addBaseValidatorRules($validator);
    }
    
    public function create($requestData) {
        $this->validateConnectorName($requestData);
        $this->validateRequestData($requestData);
        $connectorMeta = $this->getConnectorMetaFromClassname($requestData['data']['connectorName']);
        $requestData['title'] = __('Declined inter-connection for {0}', $connectorMeta['name']);
        return parent::create($requestData);
    }

    public function getViewVariables($request)
    {
        $request = $this->attachRequestAssociatedData($request);
        return [
            'request' => $request,
            'progressStep' => 1,
            'progressVariant' => 'danger',
            'steps' => [
                1 => ['icon' => 'times', 'text' => __('Request Declined'), 'confirmButton' => __('Clean-up')],
                2 => ['icon' => 'trash', 'text' => __('Clean-up')],
            ]
        ];
    }

    public function process($id, $requestData, $inboxRequest)
    {
        $connectionSuccessfull = false;
        $interConnectionResult = [];
        if ($connectionSuccessfull) {
            $this->discard($id, $requestData);
        }
        return $this->genActionResult(
            $interConnectionResult,
            $connectionSuccessfull,
            $connectionSuccessfull ? __('Interconnection for `{0}`\'s {1} finalised', $requestData['origin'], $requestData['local_tool_name']) : __('Could not inter-connect `{0}`\'s {1}', $requestData['origin'], $requestData['local_tool_name']),
            []
        );
    }
    public function discard($id, $requestData)
    {
        return parent::discard($id, $requestData);
    }
}
