<?php

namespace modules\selfserve\classes;

require_once WD . '/classes/selfserve.php';
require_once WD . '/modules/selfserve/classes/selfserve_config_versioning.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane_command_arguments.php';
require_once WD . '/modules/selfserve/classes/selfserve_studio_actions.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_command.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_port.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_relay.php';

use classes\selfserve;
use modules\selfserve\helpers\selfserve_lane_command;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_relay;

class selfserve_studio_action_runner
{
    /**
     * @param int|object $lane Lane id or selfserve lane instance.
     * @param array<string,mixed> $context
     * @return array<int,array<string,mixed>>
     */
    public function executeForLaneEvent(int|object $lane, string $event, string $washMode = selfserve_studio_actions::MODE_BOTH, array $context = []): array
    {
        $laneObject = is_int($lane) ? (new selfserve())->lane($lane) : $lane;
        $departmentId = $this->departmentIdForLane($laneObject);
        if ($departmentId <= 0) {
            return [];
        }

        $published = (new selfserve_config_versioning())->getPublishedV2Config($departmentId);
        $config = is_array($published['config'] ?? null) ? (array)$published['config'] : [];
        $actions = $this->matchingActions($config, $laneObject, $event, $washMode, $context);
        $results = [];

        foreach ($actions as $action) {
            $results[] = $this->executeAction($laneObject, $action);
        }

        return $results;
    }

    /**
     * @param array<string,mixed> $config
     * @param object $lane
     * @param array<string,mixed> $context
     * @return array<int,array<string,mixed>>
     */
    public function matchingActions(array $config, object $lane, string $event, string $washMode, array $context = []): array
    {
        $event = strtolower(trim($event));
        $washMode = strtolower(trim($washMode));
        $laneId = (int)($lane->id ?? 0);
        $departmentId = $this->departmentIdForLane($lane);
        $machineTypeId = $this->nullableInt($context['machine_type_id'] ?? null) ?? $this->machineTypeIdForLane($lane);
        $productId = $this->nullableInt($context['product'] ?? $context['product_id'] ?? $context['vehicle_type_id'] ?? null);
        $conditionResults = is_array($context['condition_results'] ?? null) ? (array)$context['condition_results'] : null;

        $actions = [];
        foreach ((array)($config['actions'] ?? []) as $row) {
            if (!is_array($row)) {
                continue;
            }
            $action = selfserve_studio_actions::normalize((array)$row);
            if (!$action['enabled'] || $action['event'] !== $event) {
                continue;
            }
            if (!in_array($action['wash_mode'], [selfserve_studio_actions::MODE_BOTH, $washMode], true)) {
                continue;
            }
            if ((int)$action['department'] !== 0 && (int)$action['department'] !== $departmentId) {
                continue;
            }
            if ((int)$action['lane'] !== 0 && (int)$action['lane'] !== $laneId) {
                continue;
            }
            if ((int)$action['product'] !== 0 && ($productId === null || (int)$action['product'] !== $productId)) {
                continue;
            }
            if ($action['machine_type_id'] !== null && (int)$action['machine_type_id'] !== $machineTypeId) {
                continue;
            }
            $conditionId = $action['condition_id'];
            if ($conditionId !== null && (($conditionResults[$conditionId] ?? false) !== true)) {
                continue;
            }
            $actions[] = $action;
        }

        usort($actions, static fn(array $left, array $right): int => ((int)$left['order_priority'] <=> (int)$right['order_priority']) ?: ((int)$left['id'] <=> (int)$right['id']));
        return $actions;
    }

    /**
     * @param object $lane
     * @param array<string,mixed> $action
     * @return array<string,mixed>
     */
    private function executeAction(object $lane, array $action): array
    {
        $options = is_array($action['options'] ?? null) ? (array)$action['options'] : [];
        $attempts = max(1, min(4, ((int)($options['retry_count'] ?? 0)) + 1));
        $delayMs = max(0, min(10000, (int)($options['delay_ms'] ?? 0)));
        $lastError = null;

        for ($attempt = 1; $attempt <= $attempts; $attempt++) {
            try {
                if ($delayMs > 0) {
                    usleep($delayMs * 1000);
                }
                $this->dispatchAction($lane, $action);
                return [
                    'action_id' => (int)$action['id'],
                    'name' => (string)$action['name'],
                    'event' => (string)$action['event'],
                    'operation' => (string)$action['operation'],
                    'status' => 'sent',
                    'attempts' => $attempt,
                ];
            } catch (\Throwable $e) {
                $lastError = $e;
            }
        }

        $result = [
            'action_id' => (int)$action['id'],
            'name' => (string)$action['name'],
            'event' => (string)$action['event'],
            'operation' => (string)$action['operation'],
            'status' => 'failed',
            'attempts' => $attempts,
            'error' => $lastError?->getMessage(),
        ];

        if (($options['failure_policy'] ?? selfserve_studio_actions::FAILURE_CONTINUE) === selfserve_studio_actions::FAILURE_BLOCK) {
            throw new \RuntimeException('Self-serve studio action failed: ' . (string)$action['name'], 0, $lastError);
        }

        return $result;
    }

    /**
     * @param object $lane
     * @param array<string,mixed> $action
     */
    private function dispatchAction(object $lane, array $action): void
    {
        $toggleAfter = $this->nullableInt($action['options']['toggle_after_seconds'] ?? null);
        switch ((string)$action['operation']) {
            case selfserve_studio_actions::OP_OPEN_PROPERTY_ENTRANCE_GATE:
                $lane->execute(selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE, new selfserve_lane_command_arguments());
                return;
            case selfserve_studio_actions::OP_OPEN_PROPERTY_EXIT_GATE:
                $lane->execute(selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE, new selfserve_lane_command_arguments());
                return;
            case selfserve_studio_actions::OP_OPEN_LANE_ENTRANCE_PORT:
                $lane->open(selfserve_lane_port::ENTRANCE, $toggleAfter);
                return;
            case selfserve_studio_actions::OP_OPEN_LANE_EXIT_PORT:
                $lane->open(selfserve_lane_port::EXIT, $toggleAfter);
                return;
            case selfserve_studio_actions::OP_SET_CLEANER_RELAY:
                $lane->setRelayStatusHard(selfserve_lane_relay::MACHINE_CLEANER, (bool)$action['relay_state']);
                return;
            case selfserve_studio_actions::OP_SET_MACHINE_RELAY:
                $lane->setRelayStatusHard(selfserve_lane_relay::MACHINE, (bool)$action['relay_state']);
                return;
            case selfserve_studio_actions::OP_SET_PROGRAM_PICKER_RELAY:
                $lane->setRelayStatusHard(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, (bool)$action['relay_state']);
                return;
        }

        throw new \RuntimeException('Unsupported self-serve studio action operation: ' . (string)$action['operation']);
    }

    private function departmentIdForLane(object $lane): int
    {
        try {
            return empty($lane->department_lane) || empty($lane->department_lane->department)
                ? 0
                : (int)$lane->department_lane->department->value();
        } catch (\Throwable) {
            return 0;
        }
    }

    private function machineTypeIdForLane(object $lane): int
    {
        try {
            return empty($lane->department_lane) || empty($lane->department_lane->machine_type_id)
                ? 0
                : (int)$lane->department_lane->machine_type_id->value();
        } catch (\Throwable) {
            return 0;
        }
    }

    private function nullableInt(mixed $value): ?int
    {
        if ($value === null || $value === '' || $value === 'null') {
            return null;
        }
        $intValue = (int)$value;
        return $intValue <= 0 ? null : $intValue;
    }
}
