<?php

namespace objects;

use classes\db;
use classes\object_property;
use classes\selfserve;
use classes\selfserve_schema_bootstrap;
use Exception;
use modules\selfserve\classes\selfserve_config_versioning;
use traits\db_object_t;

class department_lanes_o extends db
{
    use db_object_t;

    public object_property $department; // The department id
    public object_property $name; // The lane name
    public object_property $relay_in_id; // The Shelly relay for the entrance port (if applicable)
    public object_property $relay_out_id; // The Shelly relay for the exit port (if applicable)
    public object_property $relay_machine_id; // The Shelly relay for the machine (if applicable)
    public object_property $relay_machine_program_picker_id; // The Shelly relay for the machine program picker (if applicable)
    public object_property $relay_machine_cleaner_id; // The Shelly relay for the machine cleaner (if applicable)
    public object_property $dynamic_image_id; // The dynamic image id for the lane (if applicable)
    public object_property $machine_type_id; // The reusable self-serve machine type for the lane (if applicable)
    public object_property $selfserve_enabled; // Whether this lane can be used for self-serve when department self-serve is enabled
    public object_property $created_at;
    public object_property $updated_at;
    public object_property $deleted_at;


    public function structure(): void
    {
        selfserve_schema_bootstrap::ensureTables();
        $this->setTable('department_lanes');
    }

    public function getLaneStatus(): \modules\selfserve\helpers\selfserve_lane_status
    {
        self::requireSelected();
        $selfserve = new selfserve();
        $lane = $selfserve->lane((int)$this->id);
        return $lane->getLaneStatus();
    }

    /**
     * Add a department lane
     * @param int $department The department id
     * @param string $name The lane name
     * @param string|null $relay_in_id The Shelly relay for the entrance port (if applicable)
     * @param string|null $relay_out_id The Shelly relay for the exit port (if applicable)
     * @param string|null $relay_machine_id The Shelly relay for the machine (if applicable)
     * @param int|null $dynamic_image_id The dynamic image id for the lane (if applicable)
     * @return department_lanes_o
     * @throws Exception If the object was not created successfully
     */
    public function add(int $department, string $name, string $relay_in_id = null, string $relay_out_id = null, string $relay_machine_id = null, string $relay_machine_program_picker_id = null, string $relay_machine_cleaner_id = null, int $dynamic_image_id = null, ?int $machine_type_id = null, bool $selfserve_enabled = true): department_lanes_o
    {
        global /** @var db $db */
        $db;
        // Sanitize the input
        $department = (int)$department;
        $name = $db->escape_string($name);
        if (!is_null($relay_in_id)) {
            $relay_in_id = $db->escape_string($relay_in_id);
        }
        if (!is_null($relay_out_id)) {
            $relay_out_id = $db->escape_string($relay_out_id);
        }
        if (!is_null($relay_machine_id)) {
            $relay_machine_id = $db->escape_string($relay_machine_id);
        }
        if (!is_null($relay_machine_program_picker_id)) {
            $relay_machine_program_picker_id = $db->escape_string($relay_machine_program_picker_id);
        }
        if (!is_null($relay_machine_cleaner_id)) {
            $relay_machine_cleaner_id = $db->escape_string($relay_machine_cleaner_id);
        }
        if (!is_null($dynamic_image_id)) {
            $dynamic_image_id = (int)$dynamic_image_id;
            if ($dynamic_image_id <= 0) {
                throw new Exception('dynamic_image_id must be a positive integer');
            }
        }
        if (!is_null($machine_type_id)) {
            $machine_type_id = (int)$machine_type_id;
            if ($machine_type_id <= 0) {
                throw new Exception('machine_type_id must be a positive integer');
            }
        }
        // Add the object
        $tmp_id = self::add_object([
            'department' => $department,
            'name' => $name,
            ...(!is_null($relay_in_id) ? ['relay_in_id' => $relay_in_id] : []), // If the relay_in_id is null, it will be set to null in the database
            ...(!is_null($relay_out_id) ? ['relay_out_id' => $relay_out_id] : []), // If the relay_out_id is null, it will be set to null in the database
            ...(!is_null($relay_machine_id) ? ['relay_machine_id' => $relay_machine_id] : []), // If the relay_machine_id is null, it will be set to null in the database
            ...(!is_null($relay_machine_program_picker_id) ? ['relay_machine_program_picker_id' => $relay_machine_program_picker_id] : []),
            ...(!is_null($relay_machine_cleaner_id) ? ['relay_machine_cleaner_id' => $relay_machine_cleaner_id] : []),
            ...(!is_null($dynamic_image_id) ? ['dynamic_image_id' => $dynamic_image_id] : []), // If the dynamic_image_id is null, it will be set to null in the database
            ...(!is_null($machine_type_id) ? ['machine_type_id' => $machine_type_id] : []),
            'selfserve_enabled' => $selfserve_enabled ? 1 : 0,
        ]);
        $this->id = $tmp_id;
        self::getObjectProperties();
        self::objectChanged();
        return $this;
    }

    public function getObjectProperties(): void
    {
        $this->department = new object_property($this->table, $this->id, 'department', 'int', false);
        $this->name = new object_property($this->table, $this->id, 'name', 'string', false);
        $this->relay_in_id = new object_property($this->table, $this->id, 'relay_in_id', 'string', false);
        $this->relay_out_id = new object_property($this->table, $this->id, 'relay_out_id', 'string', false);
        $this->relay_machine_id = new object_property($this->table, $this->id, 'relay_machine_id', 'string', false);
        $this->relay_machine_program_picker_id = new object_property($this->table, $this->id, 'relay_machine_program_picker_id', 'string', false);
        $this->relay_machine_cleaner_id = new object_property($this->table, $this->id, 'relay_machine_cleaner_id', 'string', false);
        $this->dynamic_image_id = new object_property($this->table, $this->id, 'dynamic_image_id', 'int', false);
        $this->machine_type_id = new object_property($this->table, $this->id, 'machine_type_id', 'int', false);
        $this->selfserve_enabled = new object_property($this->table, $this->id, 'selfserve_enabled', 'bool', false, true);
        $this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
        $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
        $this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
    }

    public function objectChanged(): void
    {
        //TODO: Add cache invalidation
    }

    public function asArray(): array
    {
        $status = (string)$this->getLaneStatus()->name;
        $machine_status_audit = $this->getMachineStatusAudit();
        $selfserve_configuration_warnings = $this->getSelfServeConfigurationWarnings();

        return [
            'id' => (int)$this->id,
            'department' => (int)$this->department->value(),
            'name' => (string)$this->name->value(),
            'relay_in_id' => (string)$this->relay_in_id->value(),
            'relay_out_id' => (string)$this->relay_out_id->value(),
            'relay_machine_id' => (string)$this->relay_machine_id->value(),
            'relay_machine_program_picker_id' => (string)$this->relay_machine_program_picker_id->value(),
            'relay_machine_cleaner_id' => (string)$this->relay_machine_cleaner_id->value(),
            'dynamic_image_id' => (function($v){ return $v === null ? null : (int)$v; })($this->dynamic_image_id->value()),
            'machine_type_id' => (function($v){ return $v === null ? null : (int)$v; })($this->machine_type_id->value()),
            'selfserve_enabled' => $this->isSelfServeEnabled(),
            // Status of the lane
            'status' => $status,
            'machine_status_enabled' => self::isOperationalStatusName($status),
            'machine_status_audit' => $machine_status_audit,
            'machine_status_modified_at' => $machine_status_audit['modified_at'] ?? null,
            'machine_status_modified_by' => $machine_status_audit['modified_by_name'] ?? null,
            'machine_status_modified_by_user_id' => $machine_status_audit['modified_by_user_id'] ?? null,
            'selfserve_configured' => $selfserve_configuration_warnings === [],
            'dognvask_configured' => $selfserve_configuration_warnings === [],
            'dognvask_configuration_warnings' => $selfserve_configuration_warnings,
            // Timestamps
            'created_at' => (string)$this->created_at->value(),
            'updated_at' => (string)$this->updated_at->value(),
        ];
    }

    private function getMachineStatusAudit(): ?array
    {
        try {
            $lane = (new selfserve())->lane((int)$this->id);
            if (!method_exists($lane, 'getLaneStatusAudit')) {
                return null;
            }

            $audit = $lane->getLaneStatusAudit();
            return is_array($audit) ? $audit : null;
        } catch (\Throwable) {
            return null;
        }
    }

    public function getSelfServeConfigurationWarnings(): array
    {
        self::requireSelected();

        $required_fields = [
            'relay_in_id' => 'Indgangsrelæ',
            'relay_out_id' => 'Udgangsrelæ',
            'relay_machine_id' => 'Maskinrelæ',
            'relay_machine_program_picker_id' => 'Programvælgerrelæ',
            'relay_machine_cleaner_id' => 'Vaskerelæ',
            'dynamic_image_id' => 'Maskinstatusbillede',
            'machine_type_id' => 'Maskintype',
        ];

        $warnings = [];
        foreach ($required_fields as $field => $label) {
            if ($this->hasConfiguredFieldValue($field)) {
                continue;
            }

            $warnings[] = [
                'field' => $field,
                'label' => $label,
                'message' => $label . ' mangler',
            ];
        }

        return $warnings;
    }

    public function isSelfServeConfigured(): bool
    {
        return $this->getSelfServeConfigurationWarnings() === [];
    }

    public static function isOperationalStatusName(string $status): bool
    {
        return in_array(strtoupper(trim($status)), ['AVAILABLE', 'OCCUPIED', 'RESERVED'], true);
    }

    public function isSelfServeEnabled(): bool
    {
        self::requireSelected();
        try {
            $value = $this->selfserve_enabled->value();
        } catch (\Throwable) {
            return true;
        }

        if ($value === null || $value === '') {
            return true;
        }
        if (is_bool($value)) {
            return $value;
        }
        if (is_numeric($value)) {
            return (int)$value === 1;
        }

        return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }

    public static function normalizeSelfServeEnabledValue(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_numeric($value)) {
            return (int)$value === 1;
        }

        return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }

    private function hasConfiguredFieldValue(string $field): bool
    {
        if (!isset($this->{$field}) || !is_object($this->{$field}) || !method_exists($this->{$field}, 'value')) {
            return false;
        }

        $value = $this->{$field}->value();
        if ($value === null) {
            return false;
        }

        if (is_string($value)) {
            $value = trim($value);
            return $value !== '' && $value !== '0' && strtolower($value) !== 'null';
        }

        if (is_numeric($value)) {
            return (int)$value > 0;
        }

        return (bool)$value;
    }

    public static function disableSelfServeRelaysBestEffort(int $lane_id): void
    {
        if ($lane_id <= 0) {
            return;
        }

        try {
            $lane = (new selfserve())->lane($lane_id);
        } catch (\Throwable) {
            return;
        }

        self::setLaneRelayOffIfConfigured($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
            $lane->setMachineProgramPickerRelayStatusHard(false);
        });
        self::setLaneRelayOffIfConfigured($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
            $lane->setMachineCleanerRelayStatusHard(false);
        });
        self::setLaneRelayOffIfConfigured($lane, 'relay_machine_id', static function () use ($lane): void {
            $lane->setMachineRelayStatusHard(false);
        });
    }

    private static function setLaneRelayOffIfConfigured(object $lane, string $relay_property, callable $callback): void
    {
        if (
            empty($lane->department_lane)
            || !isset($lane->department_lane->{$relay_property})
            || !is_object($lane->department_lane->{$relay_property})
            || !method_exists($lane->department_lane->{$relay_property}, 'value')
            || trim((string)$lane->department_lane->{$relay_property}->value()) === ''
        ) {
            return;
        }

        try {
            $callback();
        } catch (\Throwable) {
            // Best effort only; toggling lane self-serve should not fail on relay I/O.
        }
    }

    /**
     * Get the self-serve lane products available for this lane
     * @return array An array of product ids available for this lane
     * @throws Exception
     */
    public function getSelfServeLaneProducts(): array
    {
        self::requireSelected();
        $publishedProducts = $this->getPublishedSelfServeLaneProducts();
        if ($publishedProducts !== []) {
            return $publishedProducts;
        }

        return department_selfserve_tasks_o::getLaneProducts((int)$this->id);
    }

    /**
     * @return int[]
     */
    private function getPublishedSelfServeLaneProducts(): array
    {
        try {
            $published = (new selfserve_config_versioning())->getPublishedV2Config((int)$this->department->value());
        } catch (\Throwable) {
            return [];
        }

        $config = is_array($published['config'] ?? null) ? $published['config'] : null;
        if ($config === null) {
            return [];
        }

        $laneId = (int)$this->id;
        $products = [];
        foreach ((array)($config['tasks'] ?? []) as $task) {
            if (!is_array($task)) {
                continue;
            }

            $taskLane = (int)($task['lane'] ?? 0);
            if ($taskLane !== 0 && $taskLane !== $laneId) {
                continue;
            }

            $productId = (int)($task['product'] ?? 0);
            if ($productId > 0 && !in_array($productId, $products, true)) {
                $products[] = $productId;
            }
        }

        sort($products, SORT_NUMERIC);
        return $products;
    }

    /**
     * @throws Exception
     * @return department_lanes_o[] An array of department lane objects for the specified department
     */
    public function getDepartmentLanes(int $department_id): array
    {
        $lanes = [];
        $department_lanes = self::getFieldsWhere(
            [
                'department' => $department_id,
                'deleted_at' => null,
            ],
            [
                'id',
            ],
        );
        foreach ( $department_lanes as $department_lane ) {
            $lanes[] = (new department_lanes_o())->select((int)$department_lane['id']);
        }
        return $lanes;
    }
}
