<?php

namespace idoit\Module\SyneticsJdisc\Service;

use idoit\Module\SyneticsJdisc\Graphql\Connector;
use idoit\Module\SyneticsJdisc\Graphql\Query\IdentifyDevice;
use idoit\Module\SyneticsJdisc\Graphql\Type\DeviceInputType;
use isys_cmdb_dao_category_g_jdisc_device_information;
use isys_component_dao_logbook;
use isys_component_database_proxy;
use isys_import_handler_cmdb;
use isys_module_synetics_jdisc;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Device archiver
 *
 * Archive outdated devices
 *
 * @copyright i-doit GmbH
 * @license   http://www.i-doit.com/license
 */
class Archiver
{
    /**
     * @var OutputInterface|null
     */
    private ?OutputInterface $logger;

    /**
     * @var Connector
     */
    private Connector $connector;

    /**
     * @var isys_component_database_proxy
     */
    private isys_component_database_proxy $db;

    /**
     * @var int
     */
    private int $threshold;

    /**
     * @var string
     */
    private string $thresholdType;

    /**
     * @param isys_component_database_proxy $db
     * @param isys_module_synetics_jdisc             $jdisc
     * @param OutputInterface               $logger
     *
     * @throws Exception
     */
    public function __construct(isys_component_database_proxy $db, isys_module_synetics_jdisc $jdisc, ?OutputInterface $logger = null)
    {
        $serverId = $jdisc->getServerId();
        if ($jdisc->isGraphqlAvailable($serverId)) {
            $this->connector = Connector::instance($serverId);
            $this->connector->connect();
        } else {
            throw new \Exception('GraphQL support is required!');
        }

        $this->db     = $db;
        $this->logger = $logger;
    }

    /**
     * @param int    $threshold
     * @param string $thresholdType
     *
     * @return void
     */
    public function setArchiveThreshold(int $threshold, string $thresholdType): void
    {
        $this->threshold     = $threshold;
        $this->thresholdType = $thresholdType;
    }

    /**
     * @return void
     */
    public function archive(): void
    {
        $archiveThreshold = $this->calculateArchiveThreshold();

        $devicesResource = $this->getDevicesToProcess($archiveThreshold);
        $devicesQuantity = $this->db->num_rows($devicesResource);

        if (!$devicesResource || !$devicesQuantity) {
            $this->log('There are no devices to archive.');
            return;
        }
        $this->log("There are {$devicesQuantity} outdated devices found.");

        while ($device = $devicesResource->fetch_assoc()) {
            $this->processAndArchiveDevice($device);
        }
    }

    /**
     * @return string
     */
    private function calculateArchiveThreshold(): string
    {
        return date(
            'Y-m-d H:i:s',
            strtotime("-{$this->threshold} {$this->thresholdType}")
        );
    }

    /**
     * @param string $archiveThreshold
     *
     * @return mysqli_result|null
     */
    private function getDevicesToProcess(string $archiveThreshold): ?\mysqli_result
    {
        $archiveThreshold = $this->db->escape_string($archiveThreshold);
        $status           = (int) C__RECORD_STATUS__NORMAL;

        return $this->db->query(
            $this->buildQuery($archiveThreshold, $status)
        );
    }

    /**
     * @param string $archiveThreshold
     * @param int    $status
     *
     * @return string
     */
    private function buildQuery(string $archiveThreshold, int $status): string
    {
        return <<<SQL
            SELECT
                device_list.isys_catg_jdisc_device_information_list__isys_obj__id AS obj_id,
                device_list.isys_catg_jdisc_device_information_list__last_seen AS last_seen,
                model_list.isys_catg_model_list__serial AS serialnumber,
                GROUP_CONCAT(mac_list.isys_catg_port_list__mac) AS all_macs
            FROM
                isys_catg_jdisc_device_information_list device_list
            JOIN
                isys_obj obj ON obj.isys_obj__id = device_list.isys_catg_jdisc_device_information_list__isys_obj__id
            LEFT JOIN
                isys_catg_model_list model_list ON model_list.isys_catg_model_list__isys_obj__id = device_list.isys_catg_jdisc_device_information_list__isys_obj__id
            LEFT JOIN
                isys_catg_port_list mac_list ON mac_list.isys_catg_port_list__isys_obj__id = device_list.isys_catg_jdisc_device_information_list__isys_obj__id
                AND isys_catg_port_list__mac IS NOT NULL
                AND isys_catg_port_list__mac <> ''
            WHERE
                device_list.isys_catg_jdisc_device_information_list__last_seen < '{$archiveThreshold}'
                AND obj.isys_obj__status = '{$status}'
            GROUP BY
                obj_id, last_seen, serialnumber;
SQL;
    }

    /**
     * @param array $device
     *
     * @return void
     */
    private function processAndArchiveDevice(array $device): void
    {
        $jdiscDevice = $this->identifyDevice($device);

        if (!$jdiscDevice || strtotime($jdiscDevice['lastSeenTime']) <= strtotime("-{$this->threshold} {$this->thresholdType}")) {
            $this->archiveDevice($device);
        } else {
            $this->updateLastSeen($device, $jdiscDevice);
        }
    }

    /**
     * @param array $device
     *
     * @return array
     */
    private function identifyDevice(array $device): array
    {
        $jdiscDevice = [];
        if ($device['serialnumber'] || $device['all_macs']) {
            $networkInterfaces = [];
            if ($device['all_macs']) {
                $macs = explode(',', $device['all_macs']);
                foreach ($macs as $mac) {
                    $networkInterfaces[] = ['physicalAddress' => $mac];
                }
            }

            $query = new IdentifyDevice();
            $query->setSelections([
                'id',
                'lastSeenTime',
            ]);
            $query->setParameters([
                new DeviceInputType(
                    'device',
                    [
                        'serialNumber'      => $device['serialnumber'],
                        'networkInterfaces' => $networkInterfaces,
                    ]
                ),
            ]);

            $data = [];
            try {
                $data = $this->connector->query($query);
            } catch (\Exception $e) {
                $this->log("Failed to identify device #{$device['obj_id']}: {$e->getMessage()}");
            }

            if (!empty($data['device'])) {
                $jdiscDevice = $data['device'];
            }
        }

        if (!$jdiscDevice) {
            $this->log("Can't identify device with Object-ID #{$device['obj_id']}. Forced to archive.");
        }

        return $jdiscDevice;
    }

    /**
     * @param array $device
     *
     * @return void
     */
    private function archiveDevice(array $device): void
    {
        isys_cmdb_dao_category_g_jdisc_device_information::instance($this->db)->rank_record(
            $device['obj_id'],
            C__CMDB__RANK__DIRECTION_DELETE,
            'isys_obj'
        );
        $this->log("Device with Object-ID #{$device['obj_id']} was archived.");
    }

    /**
     * @param array $device
     * @param array $jdiscDevice
     *
     * @return void
     */
    private function updateLastSeen(array $device, array $jdiscDevice): void
    {
        $lastSeen = date(
            'Y-m-d H:i:s',
            strtotime($jdiscDevice['lastSeenTime'])
        );

        $dao = isys_cmdb_dao_category_g_jdisc_device_information::instance($this->db);
        $result = $dao->sync(
            [
                'data_id' => $jdiscDevice['id'],
                'properties' => [
                    'lastSeen' => [
                        C__DATA__VALUE => $lastSeen,
                    ],
                ],
            ],
            $device['obj_id'],
            isys_import_handler_cmdb::C__UPDATE
        );

        isys_component_dao_logbook::instance($this->db)
            ->set_entry(
                'C__LOGBOOK_EVENT__CATEGORY_CHANGED',
                'Import',
                date('Y-m-d H:i:s'),
                defined_or_default(C__LOGBOOK__ALERT_LEVEL__0, 1),
                $device['obj_id'],
                $dao->get_obj_name_by_id_as_string($device['obj_id']),
                $dao->get_obj_type_name_by_obj_id($device['obj_id']),
                'LC__CATG__JDISC_DEVICE_INFORMATION',
                defined_or_default(C__LOGBOOK_SOURCE__JDISC, null),
                serialize([
                    'isys_cmdb_dao_category_g_jdisc_device_information::lastSeen' => [
                        'from' => $device['last_seen'],
                        'to'   => $lastSeen,
                    ],
                ])
            );
        $this->log("LastSeen for device with Object-ID #{$device['obj_id']} was updated.");
    }

    /**
     * @param string $message
     *
     * @return void
     */
    private function log(string $message): void
    {
        if ($this->logger) {
            $this->logger->writeln($message, true);
        }
    }
}
