<?php

namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_port.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';

use classes\edge_gateway_manager;
use classes\shelly_transport_resolver;
use interfaces\shelly_transport_i;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_log_action;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\helpers\selfserve_lane_status;
use modules\shelly\helpers\shelly_device_switch;

trait selfserve_lane_port_controller_t
{
    private const DEMO_RELAY_ID_PREFIX = 'demo-';
    private const DEFAULT_PORT_OPEN_TOGGLE_AFTER_SECONDS = 1;
    private const MAX_PORT_OPEN_TOGGLE_AFTER_SECONDS = 5;

    /**
     * Open the lane port
     * @param selfserve_lane_port $port The port to open (ENTRANCE or EXIT)
     * @param int|null $toggle_after_seconds Seconds before the relay flips back off
     * @return bool True if the port was successfully opened, false otherwise
     * @throws \Exception If an invalid port is specified, or if the lane is not in a state to open the port
     */
    public function open(selfserve_lane_port $port, ?int $toggle_after_seconds = null): bool
    {
        // Require department lane object
        if (empty($this->department_lane)) throw new \Exception("Department lane object not found for lane ID {$this->id}");
        // Check lane status (ensure initialization via getter)
        if ($this->getLaneStatus()->equals(selfserve_lane_status::CLOSED)) throw new \Exception("Cannot open port on CLOSED lane");
        if ($this->getLaneStatus()->equals(selfserve_lane_status::MAINTENANCE)) throw new \Exception("Cannot open port on MAINTENANCE lane");
        if ($this->getLaneStatus()->equals(selfserve_lane_status::FAULT)) throw new \Exception("Cannot open port on FAULT lane");
        // Get the relay ID based on the port
        $relay_id = match ($port) {
            selfserve_lane_port::ENTRANCE => $this->department_lane->relay_in_id->value(),
            selfserve_lane_port::EXIT => $this->department_lane->relay_out_id->value(),
            default => throw new \Exception("Invalid port specified, must be ENTRANCE or EXIT"),
        };
        // Make sure relay ID is valid
        if (empty($relay_id)) {
            throw new \Exception("Invalid relay ID for port {$port->name}");
        }
        // Mark the lane state as opening the port
        $new_state = match ($port) {
            selfserve_lane_port::ENTRANCE => selfserve_lane_state::ENTRANCE_PORT_OPEN_QUEUED,
            selfserve_lane_port::EXIT => selfserve_lane_state::EXIT_PORT_OPEN_QUEUED,
            default => throw new \Exception("Invalid port specified, must be ENTRANCE or EXIT"),
        };
        $this->setLaneState($new_state);
        // Log the port open event
        $this->logLaneAction(selfserve_lane_log_action::OPEN_PORT, 200, ['port' => $port->name]);
        // Open the relay
        return $this->shellyOpenPort($port, $toggle_after_seconds);
    }

    /**
     * Open the Shelly relay for the specified port
     * @param selfserve_lane_port $port The port to open (ENTRANCE or EXIT)
     * @param int|null $toggle_after_seconds Seconds before the relay flips back off
     * @return bool True if the relay was successfully opened
     * @throws \Exception If an invalid port is specified or if the relay ID is invalid
     */
    public function shellyOpenPort(selfserve_lane_port $port, ?int $toggle_after_seconds = null): bool
    {
        // Queue the relay switch command to be executed asynchronously
        $relay_id = match ($port) {
            selfserve_lane_port::ENTRANCE => $this->department_lane->relay_in_id->value(),
            selfserve_lane_port::EXIT => $this->department_lane->relay_out_id->value(),
            default => throw new \Exception("Invalid port specified, must be ENTRANCE or EXIT"),
        };
        // If the relay ID indicates a demo port, skip the Shelly switch call but keep the logging and state changes
        if ($this->isDemoPortRelayId($relay_id)) {
            return true;
        }

        $device = $this->createShellySwitchDevice();
        $device->id = (string)$relay_id;
        $device->toggle_after = $this->normalizePortOpenToggleAfter($toggle_after_seconds);
        $contextPayload = [
            'id' => (string)$relay_id,
            'on' => true,
            'toggle_after' => $device->toggle_after,
        ];
        $context = method_exists($this, 'buildSelfServeRelayActionContext')
            ? $this->buildSelfServeRelayActionContext('/v2/devices/api/set/switch', $contextPayload, [
                'reason' => 'Open self-serve ' . $port->name . ' gate relay',
                'relay_role' => $port->name,
            ])
            : [
                'module' => 'selfserve',
                'reason' => 'Open self-serve ' . $port->name . ' gate relay',
                'relay_id' => (string)$relay_id,
                'relay_role' => $port->name,
            ];

        return edge_gateway_manager::withRelayActionContext($context, function () use ($device): bool {
            $device->switch(true);
            return true;
        });
    }

    private function normalizePortOpenToggleAfter(?int $toggle_after_seconds): int
    {
        if ($toggle_after_seconds !== null && $toggle_after_seconds > 0) {
            return min($toggle_after_seconds, self::MAX_PORT_OPEN_TOGGLE_AFTER_SECONDS);
        }

        return self::DEFAULT_PORT_OPEN_TOGGLE_AFTER_SECONDS;
    }

    private function isDemoPortRelayId(string $relay_id): bool
    {
        $normalized = strtolower(trim($relay_id));
        return $normalized !== '' && str_starts_with($normalized, self::DEMO_RELAY_ID_PREFIX);
    }

    protected function createShellySwitchDevice(): shelly_device_switch
    {
        return (new shelly_device_switch())->setRequestSender(
            fn(array $parameters): array|object|null => $this
                ->createShellyTransport()
                ->sendPostRequest(
                    '/v2/devices/api/set/switch',
                    $parameters,
                    $this->resolveShellyTransportDepartmentId()
                )
        );
    }

    /**
     * @throws \Exception
     */
    protected function createShellyTransport(): shelly_transport_i
    {
        return (new shelly_transport_resolver())->resolveForDepartment($this->resolveShellyTransportDepartmentId());
    }

    /**
     * @throws \Exception
     */
    protected function resolveShellyTransportDepartmentId(): int
    {
        if (empty($this->department_lane)) {
            throw new \Exception("Department lane object not found for lane ID {$this->id}");
        }

        $department_id = (int)$this->department_lane->department->value();
        if ($department_id <= 0) {
            throw new \Exception('Unable to resolve department for Shelly transport');
        }

        return $department_id;
    }
}
