<?php

namespace App\Model\Table;

use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use Migrations\Migrations;
use Cake\Filesystem\Folder;
use Cake\Filesystem\File;
use Cake\Http\Exception\NotFoundException;

class LocalToolsTable extends AppTable
{

    const HEALTH_CODES = [
        0 => 'UNKNOWN',
        1 => 'OK',
        2 => 'ISSUES',
        3 => 'ERROR',
    ];

    public $exposedFunctions = [];

    private $currentConnector = null;

    private $connectors = null;

    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->addBehavior('AuditLog');
        $this->addBehavior('Timestamp');
    }

    public function validationDefault(Validator $validator): Validator
    {
        return $validator->add('settings', 'validSettings', [
                'rule' => 'isValidSettings',
                'provider' => 'table',
                'message' => __('Invalid settings'),
                'on' => function ($context) {
                    return !empty($context['data']['settings']);
                }
            ]);
    }

    public function loadConnector(string $connectorName): void
    {
        if (empty($currentConnector) || get_class($currentConnector) !== $connectorName) {
            $connectors = $this->getConnectors($connectorName);
            if (empty($connectors[$connectorName])) {
                throw new NotFoundException(__('Invalid connector module requested.'));
            } else {
                $this->currentConnector = $connectors[$connectorName];
            }
        }
    }

    public function action(int $user_id, string $connectorName, string $actionName, array $params, \Cake\Http\ServerRequest $request): array
    {
        $this->loadConnector($connectorName);
        $params['request'] = $request;
        $params['user_id'] = $user_id;
        return $this->currentConnector->{$actionName}($params);
    }

    public function getActionDetails(string $actionName): array
    {
        if (!empty($this->currentConnector->exposedFunctions[$actionName])) {
            return $this->currentConnector->exposedFunctions[$actionName];
        }
        throw new NotFoundException(__('Invalid connector module action requested.'));
    }

    public function getActionFilterOptions(string $connectorName, string $actionName): array
    {
        $this->loadConnector($connectorName);
        if (!empty($this->currentConnector->exposedFunctions[$actionName])) {
            return $this->currentConnector->exposedFunctions[$actionName]['params'] ?? [];
        }
        throw new NotFoundException(__('Invalid connector module action requested.'));
    }

    public function getConnectorByToolName($toolName): array
    {
        $toolName = sprintf('%sConnector', ucfirst(strtolower($toolName)));
        return $this->getConnectors($toolName);
    }

    public function getConnectors(string $name = null): array
    {
        $connectors = [];
        $dirs = [
            ROOT . '/src/Lib/default/local_tool_connectors',
            ROOT . '/src/Lib/custom/local_tool_connectors'
        ];
        foreach ($dirs as $dir) {
            $dir = new Folder($dir);
            $files = $dir->find('.*Connector\.php');
            foreach ($files as $file) {
                require_once($dir->pwd() . '/'. $file);
                $className = substr($file, 0, -4);
                $classNamespace = '\\' . $className . '\\' . $className;
                if (empty($name) || $name === $className) {
                    $connectors[$className] = new $classNamespace;
                }
            }
        }
        return $connectors;
    }

    public function getInterconnectors(string $name = null): array
    {
        $connectors = [];
        $dirs = [
            ROOT . '/src/Lib/default/local_tool_interconnectors',
            ROOT . '/src/Lib/custom/local_tool_interconnectors'
        ];
        foreach ($dirs as $dir) {
            $dir = new Folder($dir);
            $files = $dir->find('.*Interconnector\.php');
            foreach ($files as $file) {
                require_once($dir->pwd() . '/'. $file);
                $className = substr($file, 0, -4);
                $classNamespace = '\\' . $className . '\\' . $className;
                $tempClass = new $classNamespace;
                if (empty($name) || $tempClass->getConnectors()[0] === $name) {
                    $connectors[$tempClass->getConnectors()[0]][] = new $classNamespace;
                }
            }
        }
        return $connectors;
    }

    public function extractMeta(array $connector_classes, bool $includeConnections = false): array
    {
        $connectors = [];
        foreach ($connector_classes as $connector_type => $connector_class) {
            $connector = [
                'name' => $connector_class->name,
                'connector' => $connector_type,
                'connector_version' => $connector_class->version,
                'connector_description' => $connector_class->description,
                'connector_settings' => $connector_class->settings ?? [],
                'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
            ];
            if ($includeConnections) {
                $connector['connections'] = $this->healthCheck($connector_type, $connector_class, true);
            }
            $connectors[] = $connector;
        }
        return $connectors;
    }

    public function healthCheck(string $connector_type, Object $connector_class, bool $includeDiagnostics = false): array
    {
        $query = $this->find();
        $query->where([
            'connector' => $connector_type
        ]);
        $connections = $query->all()->toList();
        foreach ($connections as &$connection) {
            $temp = $this->healthCheckIndividual($connection);
            if ($includeDiagnostics && !empty($temp['health']) && $temp['health'] === 1) {
                $temp['diagnostics'] = $this->diagnosticCheckIndividual($connection);
            }
            $connection = $temp;
        }
        return $connections;
    }

    public function diagnosticCheckIndividual(Object $connection): array
    {
        $connector_class = $this->getConnectors($connection->connector);
        if (empty($connector_class[$connection->connector])) {
            return [];
        }
        $connector_class = $connector_class[$connection->connector];
        return $connector_class->diagnostics([
            'connection' => $connection,
            'softError' => 1
        ]);
    }

    public function healthCheckIndividual(Object $connection): array
    {
        $connector_class = $this->getConnectors($connection->connector);
        if (empty($connector_class[$connection->connector])) {
            return [];
        }
        $connector_class = $connector_class[$connection->connector];
        $health = $connector_class->health($connection);
        return $connection = [
            'name' => $connection->name,
            'health' => $health['status'],
            'message' => $health['message'],
            'url' => '/localTools/view/' . $connection['id']
        ];
    }

    public function getConnectorByConnectionId($id): array
    {
        $connection = $this->find()->where(['id' => $id])->first();
        if (empty($connection)) {
            throw new NotFoundException(__('Invalid connection.'));
        }
        return $this->getConnectors($connection->connector);
    }

    public function getChildParameters($id): array
    {
        $connectors = $this->getConnectorByConnectionId($id);
        if (empty($connectors)) {
            throw new NotFoundException(__('Invalid connector.'));
        }
        $connector = array_values($connectors)[0];
        $children = [];
        foreach ($connector->exposedFunctions as $functionName => $function) {
            if ($function['type'] === 'index') {
                $children[] = $functionName;
            }
        }
        return $children;
    }

    public function getRemoteToolById($params) {
        $broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods');
        $tools = $broods->queryLocalTools($params['cerebrate_id']);
        $remoteTool = [];
        foreach ($tools as $tool) {
            if ($tool['id'] === intval($params['remote_tool_id'])) {
                $remoteTool = $tool;
            }
        }
        if (empty($remoteTool)) {
            throw new NotFoundException(__('Invalid remote tool specified.'));
        }
        return $remoteTool;
    }

    public function encodeConnectionChoice(array $params): array
    {
        $remoteTool = $this->getRemoteToolById($params);
        $connections = $this->find()->where(['connector' => $remoteTool['connector']])->toArray();
        $results = [];
        foreach ($connections as $connection) {
            $results[] = [
                'id' => $connection->id,
                'name' => $connection->name
            ];
        }
        return $results;
    }

    public function encodeConnection(array $params): array
    {
        $params = $this->buildConnectionParams($params);
        $localResult = $params['connector'][$params['remote_tool']['connector']]->initiateConnectionWrapper($params);
        $inboxResult = $this->sendEncodedConnection($params, $localResult);
        return [
            'inboxResult' => $inboxResult,
            'localResult' => $localResult
        ];
    }

    public function buildConnectionParams(array $params): array
    {
        $remote_tool = $this->getRemoteToolById($params);
        $broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods');
        $remote_cerebrate = $broods->find()->where(['id' => $params['cerebrate_id']])->first();
        $connector = $this->getConnectors($remote_tool['connector']);
        $connection = $this->find()->where(['id' => $params['local_tool_id']])->first();
        $remote_org = $broods->Organisations->find()->where(['id' => $remote_cerebrate['organisation_id']])->first();
        if (empty($connector[$remote_tool['connector']])) {
            throw new NotFoundException(__('No valid connector found for the remote tool.'));
        }
        return [
            'remote_cerebrate' => $remote_cerebrate,
            'remote_org' => $remote_org,
            'remote_tool' => $remote_tool,
            'connector' => $connector,
            'connection' => $connection,
            //'message' =>
        ];
    }

    public function appendLocalToolConnections(int $brood_id, array $tool): array
    {
        $remoteToolConnections = \Cake\ORM\TableRegistry::getTableLocator()->get('RemoteToolConnections');
        $connections = $remoteToolConnections->find()->where(['remote_tool_id' => $tool['id'], 'brood_id' => $brood_id])->toArray();
        $local_tools = [];
        foreach ($connections as $k => $connection) {
            $temp = $this->find()->where(['id' => $connection['local_tool_id']])->select(['id', 'name'])->enableHydration(false)->first();
            $temp['status'] = $connection['status'];
            $local_tools[] = $temp;
        }
        return $local_tools;
    }

    public function sendEncodedConnection($params, $encodedConnection)
    {
        $this->Broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods');
        $jsonReply = $this->Broods->sendLocalToolConnectionRequest($params, $encodedConnection);
        return $jsonReply;
    }

/*
    public function findConnectable($local_tool): array
    {
        $connectors = $this->getInterconnectors($local_tool['connector']);
        $validTargets = [];
        if (!empty($connectors)) {
            foreach ($connectors[$local_tool['connector']] as $connector) {
                $validTargets[$connector['connects'][1]] = 1;
            }
        }
    }
*/

    public function fetchConnection($id): object
    {
        $connection = $this->find()->where(['id' => $id])->first();
        if (empty($connection)) {
            throw new NotFoundException(__('Local tool not found.'));
        }
        return $connection;
    }

    public function isValidSettings($settings, array $context)
    {
        $settings = json_decode($settings, true);
        $validationErrors = $this->getLocalToolsSettingValidationErrors($context['data']['connector'], $settings);
        return $this->getValidationMessage($validationErrors);
    }

    public function getValidationMessage($validationErrors)
    {
        $messages = [];
        foreach ($validationErrors as $key => $errors) {
            $messages[] = sprintf('%s: %s', $key, implode(', ', $errors));
        }
        return empty($messages) ? true : implode('; ', $messages);
    }

    public function getLocalToolsSettingValidationErrors($connectorName, array $settings): array
    {
        $connector = array_values($this->getConnectors($connectorName))[0];
        $errors = [];
        if (method_exists($connector, 'addSettingValidatorRules')) {
            $validator = new Validator();
            $validator = $connector->addSettingValidatorRules($validator);
            $errors = $validator->validate($settings);
        }
        return $errors;
    }
}
