<?php

namespace routes;

use classes\bird;
use classes\redis;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use objects\department_gates_o;
use objects\departments_o;
use traits\bird_route_helpers_t;
use traits\route_t;

class birdVoiceWebhooksRoute
{
    use route_t, bird_route_helpers_t;

    private const IVR_STATE_TTL_SECONDS = 1200;
    private const IVR_STATE_PREFIX = 'bird_ivr_state:';
    private const IVR_LOCK_PREFIX = 'bird_ivr_lock:';
    private const IVR_LOCK_TTL_SECONDS = 180;
    private const IVR_STAGE_DEPARTMENT_SELECT = 'department_select';
    private const IVR_STAGE_GATE_TYPE_SELECT = 'gate_type_select';
    private const IVR_GATE_TYPE_ENTRANCE = 'entrance';
    private const IVR_GATE_TYPE_EXIT = 'exit';
    private const DEFAULT_WAIT_TIMEOUT = 'PT10M';
    private const DEFAULT_GATHER_RETRIES = 3;
    private const DEFAULT_GATHER_TIMEOUT_SECONDS = 30;
    private const DEFAULT_GATHER_END_KEY = '#';
    private const DEFAULT_GATHER_SAY_LOCALE = 'en-US';
    private const DEFAULT_GATHER_SAY_VOICE = 'female';
    private const RESPONSE_MODE_COMMAND = 'command';
    private const RESPONSE_MODE_FLOW = 'flow';

    public function run(): void
    {
        $this->post('/bird/voice/calls/webhook/inbound', function (): void {
            global $response;

            self::requirePermission('modules_bird_voice_call_webhooks_trigger');
            $client = $this->resolveBirdClient();
            $payload = $this->readInboundWebhookPayload();

            try {
                if (!$this->isSupportedInboundRequest($payload)) {
                    throw new InvalidArgumentException('Malformed inbound Bird webhook payload');
                }

                $result = $this->handleInboundCallLifecycle(
                    $client,
                    $payload,
                    $this->resolveInboundCallId($payload),
                    $this->resolveInboundWorkspaceId($client, $payload),
                    $this->resolveInboundChannelId($client, $payload),
                );

                $response->rawJson($result, (int)($result['statusCode'] ?? 200));
            } catch (InvalidArgumentException $e) {
                $body = $this->buildTransportErrorResponse($payload, 400, $e->getMessage());
                $response->rawJson($body, 400);
            } catch (\Throwable $e) {
                $body = $this->buildTransportErrorResponse($payload, 500, $e->getMessage());
                $response->rawJson($body, 500);
            }
        }, [
            'modules_bird_voice_call_webhooks_trigger' => 'Trigger a voice call via Bird webhooks',
        ]);
    }

    public function handleInboundCallLifecycle(
        bird $client,
        array $payload,
        string $callId,
        string $workspaceId = '',
        string $channelId = ''
    ): array {
        $callId = $this->normalizeOptionalString($callId);
        if ($callId === '') {
            throw new InvalidArgumentException('Missing required parameter: callId');
        }

        $requestId = $this->extractRequestIdFromPayload($payload);
        if ($requestId === '') {
            $requestId = $this->generateUuidV4();
        }

        $resolvedWorkspaceId = $this->normalizeOptionalString($workspaceId);
        if ($resolvedWorkspaceId === '') {
            $resolvedWorkspaceId = $this->getConfiguredWorkspaceId($client);
        }

        $resolvedChannelId = $this->normalizeOptionalString($channelId);
        if ($resolvedChannelId === '') {
            $resolvedChannelId = $this->getConfiguredChannelId($client);
        }

        if ($resolvedWorkspaceId === '' || $resolvedChannelId === '') {
            throw new InvalidArgumentException('Missing required parameters: workspaceId, channelId');
        }

        $state = $this->normalizeState($this->readIvrState($callId) ?? []);
        $responseMode = $this->responseModeForPayload($payload, $state);

        if (!$this->acquireIvrLock($callId)) {
            return $this->buildLifecycleCompletionResponse(
                $responseMode,
                $callId,
                [
                    'request_id' => $requestId,
                    'workspace_id' => $resolvedWorkspaceId,
                    'channel_id' => $resolvedChannelId,
                ],
                'ignored',
                'ignored',
                'Call is already being processed.',
                false,
                null,
                null,
                null,
                'lock_not_acquired',
            );
        }

        try {
            if ($this->shouldBootstrapIvrState($payload, $state)) {
                $state = $this->buildInitialState(
                    $callId,
                    $resolvedWorkspaceId,
                    $resolvedChannelId,
                    $payload,
                    $requestId,
                    $responseMode,
                );

                $this->acceptInboundCall($client, $resolvedWorkspaceId, $resolvedChannelId, $callId);

                $result = $this->bootstrapIvrState($state);
                $state = $result['state'];

                if (($result['action'] ?? '') === 'complete') {
                    $this->clearIvrState($callId);

                    return $this->buildLifecycleCompletionResponse(
                        $responseMode,
                        $callId,
                        $state,
                        'completed',
                        'no_action',
                        (string)($result['message'] ?? 'No phone-controlled gates are configured.'),
                        false,
                        null,
                        null,
                        null,
                        (string)($result['reason'] ?? 'no_eligible_departments'),
                    );
                }

                if (($result['action'] ?? '') === 'open_gate') {
                    return $this->openSelectedGateAndComplete($callId, $state, $responseMode);
                }

                $this->saveIvrState($callId, $state);

                return $this->buildLifecycleGatherResponse(
                    $responseMode,
                    $callId,
                    $state,
                    $this->buildPromptTextForState($state),
                    false,
                );
            }

            if ($state === [] || $this->normalizeOptionalString($state['stage'] ?? '') === '') {
                return $this->buildLifecycleCompletionResponse(
                    $responseMode,
                    $callId,
                    [
                        'request_id' => $requestId,
                        'workspace_id' => $resolvedWorkspaceId,
                        'channel_id' => $resolvedChannelId,
                    ],
                    'ignored',
                    'ignored',
                    'No active IVR state was found for this call.',
                    false,
                    null,
                    null,
                    null,
                    'state_not_found',
                );
            }

            $state['request_id'] = $this->normalizeOptionalString($state['request_id'] ?? '') !== ''
                ? $state['request_id']
                : $requestId;
            $state['workspace_id'] = $resolvedWorkspaceId;
            $state['channel_id'] = $resolvedChannelId;

            $rawInput = $this->extractGatherKeysFromEvent($payload);
            if ($rawInput === null) {
                $rawInput = $this->extractDtmfInput($payload);
            }

            $selection = $this->normalizeMenuDigitInput($rawInput);
            if ($selection === null) {
                $isInvalid = $rawInput !== null && trim($rawInput) !== '';
                if ($isInvalid) {
                    $state['last_invalid_input'] = $rawInput;
                    $state['invalid_selection_count'] = max(0, (int)($state['invalid_selection_count'] ?? 0)) + 1;
                }

                $this->saveIvrState($callId, $state);

                return $this->buildLifecycleGatherResponse(
                    $responseMode,
                    $callId,
                    $state,
                    $this->buildPromptTextForState($state, $isInvalid),
                    true,
                );
            }

            $result = $this->applyMenuSelection($state, $selection);
            $state = $result['state'];

            if (($result['action'] ?? '') === 'open_gate') {
                return $this->openSelectedGateAndComplete($callId, $state, $responseMode);
            }

            if (($result['action'] ?? '') === 'complete') {
                $this->clearIvrState($callId);

                return $this->buildLifecycleCompletionResponse(
                    $responseMode,
                    $callId,
                    $state,
                    'completed',
                    'no_action',
                    (string)($result['message'] ?? 'No phone-controlled gates are configured.'),
                    false,
                    $this->selectedDepartmentId($state),
                    $this->selectedGateType($state),
                    $this->selectedGateId($state),
                    (string)($result['reason'] ?? 'no_eligible_gates_for_department'),
                );
            }

            if (($result['action'] ?? '') === 'invalid_selection') {
                $state['last_invalid_input'] = $selection;
                $state['invalid_selection_count'] = max(0, (int)($state['invalid_selection_count'] ?? 0)) + 1;
            } else {
                $state['last_invalid_input'] = '';
                $state['invalid_selection_count'] = 0;
            }

            $this->saveIvrState($callId, $state);

            return $this->buildLifecycleGatherResponse(
                $responseMode,
                $callId,
                $state,
                $this->buildPromptTextForState($state, ($result['action'] ?? '') === 'invalid_selection'),
                true,
            );
        } finally {
            $this->releaseIvrLock($callId);
        }
    }
    protected function buildInitialState(
        string $callId,
        string $workspaceId,
        string $channelId,
        array $payload,
        string $requestId,
        string $responseMode
    ): array {
        return [
            'call_id' => $callId,
            'workspace_id' => $workspaceId,
            'channel_id' => $channelId,
            'request_id' => $requestId,
            'response_mode' => $responseMode,
            'gather_template' => $this->normalizeGatherTemplate($this->extractInitialGatherTemplate($payload)),
            'resume_action' => $this->extractResumeActionFromPayload($payload) ?: 'continue',
            'wait_timeout' => $this->extractWaitTimeoutFromPayload($payload) ?: self::DEFAULT_WAIT_TIMEOUT,
            'invalid_selection_count' => 0,
            'last_invalid_input' => '',
        ];
    }

    protected function bootstrapIvrState(array $state): array
    {
        $eligibleDepartments = $this->loadEligibleDepartmentSummaries();
        if ($eligibleDepartments === []) {
            return [
                'action' => 'complete',
                'reason' => 'no_eligible_departments',
                'message' => 'No phone-controlled gates are configured.',
                'state' => $state,
            ];
        }

        $departmentOptions = $this->buildDepartmentOptionMap($eligibleDepartments);
        if ($departmentOptions === []) {
            return [
                'action' => 'complete',
                'reason' => 'no_eligible_departments',
                'message' => 'No phone-controlled gates are configured.',
                'state' => $state,
            ];
        }

        $state['stage'] = self::IVR_STAGE_DEPARTMENT_SELECT;
        $state['department_options'] = $departmentOptions;
        $state['selected_department_id'] = null;
        $state['selected_department_name'] = '';
        $state['selected_gate_type'] = '';
        $state['available_gate_types'] = [];
        $state['gate_options'] = [];
        $state['gate_id'] = null;

        return ['action' => 'gather', 'state' => $state];
    }

    protected function applyMenuSelection(array $state, string $digit): array
    {
        $stage = $this->normalizeOptionalString($state['stage'] ?? '');

        if ($stage === self::IVR_STAGE_DEPARTMENT_SELECT) {
            $departmentOption = $this->resolveSelectedDepartmentOption((array)($state['department_options'] ?? []), $digit);
            if ($departmentOption === null) {
                return ['action' => 'invalid_selection', 'state' => $state];
            }

            return $this->advanceStateForDepartment($state, $departmentOption);
        }

        if ($stage === self::IVR_STAGE_GATE_TYPE_SELECT) {
            $gateOptions = $this->normalizeGateOptions((array)($state['gate_options'] ?? []));
            $gateType = $this->resolveGateTypeByDigit($digit, $gateOptions);
            if ($gateType === null) {
                return ['action' => 'invalid_selection', 'state' => $state];
            }

            $departmentId = $this->selectedDepartmentId($state);
            if ($departmentId === null) {
                return [
                    'action' => 'complete',
                    'reason' => 'missing_department',
                    'message' => 'The selected department is no longer available.',
                    'state' => $state,
                ];
            }

            $gate = $this->resolvePhoneCallGate($departmentId, $gateType);
            if ($gate === null || !$gate->exists()) {
                $state['selected_gate_type'] = $gateType;
                $state['gate_id'] = null;

                return [
                    'action' => 'complete',
                    'reason' => 'gate_not_found',
                    'message' => $this->buildMissingGateMessage($gateType),
                    'state' => $state,
                ];
            }

            $state['selected_gate_type'] = $gateType;
            $state['gate_id'] = (int)$gate->id;

            return ['action' => 'open_gate', 'state' => $state];
        }

        return ['action' => 'invalid_selection', 'state' => $state];
    }

    protected function advanceStateForDepartment(array $state, array $departmentOption): array
    {
        $departmentId = (int)($departmentOption['department_id'] ?? 0);
        if ($departmentId <= 0) {
            return [
                'action' => 'complete',
                'reason' => 'missing_department',
                'message' => 'The selected department is no longer available.',
                'state' => $state,
            ];
        }

        $state['selected_department_id'] = $departmentId;
        $state['selected_department_name'] = (string)($departmentOption['department_name'] ?? $this->getDepartmentNameById($departmentId));
        $state['department_options'] = (array)($state['department_options'] ?? []);
        $state['gate_id'] = null;
        $state['selected_gate_type'] = '';

        $availableGateTypes = $this->extractAvailableGateTypes($departmentOption);
        $state['available_gate_types'] = $availableGateTypes;
        $state['gate_options'] = $this->buildGateOptionMap($availableGateTypes);

        if ($availableGateTypes === []) {
            return [
                'action' => 'complete',
                'reason' => 'no_eligible_gates_for_department',
                'message' => 'The selected department has no phone-controlled gates.',
                'state' => $state,
            ];
        }

        $sharedGate = $this->resolveSharedPhoneCallGate($departmentId, $availableGateTypes);
        if ($sharedGate !== null && $sharedGate->exists()) {
            $state['gate_id'] = (int)$sharedGate->id;
            $state['gate_options'] = [];

            return ['action' => 'open_gate', 'state' => $state];
        }

        $state['stage'] = self::IVR_STAGE_GATE_TYPE_SELECT;

        return ['action' => 'gather', 'state' => $state];
    }

    protected function openSelectedGateAndComplete(string $callId, array $state, string $responseMode): array
    {
        $departmentId = $this->selectedDepartmentId($state);
        $gateType = $this->selectedGateType($state);
        $gateId = $this->selectedGateId($state);

        if ($departmentId === null) {
            $this->clearIvrState($callId);

            return $this->buildLifecycleCompletionResponse(
                $responseMode,
                $callId,
                $state,
                'completed',
                'no_action',
                'The selected gate is no longer available.',
                false,
                $departmentId,
                $gateType,
                $gateId,
                'gate_not_found',
            );
        }

        $gate = $gateId !== null ? $this->resolvePhoneCallGateById($gateId) : null;
        if (($gate === null || !$gate->exists()) && $gateType !== null) {
            $gate = $this->resolvePhoneCallGate($departmentId, $gateType);
        }

        if ($gate === null || !$gate->exists()) {
            $this->clearIvrState($callId);

            return $this->buildLifecycleCompletionResponse(
                $responseMode,
                $callId,
                $state,
                'completed',
                'no_action',
                $this->buildMissingGateMessage($gateType),
                false,
                $departmentId,
                $gateType,
                $gateId,
                'gate_not_found',
            );
        }

        try {
            $this->triggerGateOpen($gate);
            $this->clearIvrState($callId);

            return $this->buildLifecycleCompletionResponse(
                $responseMode,
                $callId,
                $state,
                'completed',
                'gate_opened',
                $this->buildGateOpenedMessage($gateType),
                true,
                $departmentId,
                $gateType,
                (int)$gate->id,
                'gate_opened',
            );
        } catch (\Throwable $e) {
            $this->clearIvrState($callId);

            return $this->buildLifecycleCompletionResponse(
                $responseMode,
                $callId,
                $state,
                'failed',
                'gate_open_failed',
                $this->buildGateOpenFailedMessage($gateType, $e->getMessage()),
                false,
                $departmentId,
                $gateType,
                (int)$gate->id,
                'gate_open_failed',
            );
        }
    }

    protected function completeActionResponse(
        string $callId,
        array $state,
        string $status,
        string $action,
        string $message,
        bool $gateOpened,
        ?int $departmentId,
        ?string $gateType,
        ?int $gateId,
        string $reason
    ): array {
        return [
            'requestId' => $this->requestIdForState($state),
            'result' => [
                'callId' => $callId,
                'status' => $status,
                'action' => $action,
                'message' => $message,
                'departmentId' => $departmentId,
                'gateType' => $gateType,
                'gateId' => $gateId,
                'gateOpened' => $gateOpened,
            ],
            'resumeData' => [
                'action' => $this->resumeActionForState($state),
                'completed' => true,
                'result' => $reason,
                'gateOpened' => $gateOpened,
            ],
            'completedAt' => $this->currentIsoTimestamp(),
            'statusCode' => 200,
            'statusText' => 'OK',
        ];
    }

    protected function buildLifecycleCompletionResponse(
        string $responseMode,
        string $callId,
        array $state,
        string $status,
        string $action,
        string $message,
        bool $gateOpened,
        ?int $departmentId,
        ?string $gateType,
        ?int $gateId,
        string $reason
    ): array {
        if ($responseMode === self::RESPONSE_MODE_COMMAND) {
            return $this->completeActionResponse(
                $callId,
                $state,
                $status,
                $action,
                $message,
                $gateOpened,
                $departmentId,
                $gateType,
                $gateId,
                $reason,
            );
        }

        return $this->buildFlowCompletionResponse(
            $callId,
            $state,
            $status,
            $action,
            $message,
            $gateOpened,
            $departmentId,
            $gateType,
            $gateId,
            $reason,
        );
    }

    protected function buildGatherAcceptedResponse(
        string $callId,
        array $state,
        string $prompt,
        bool $resumed
    ): array {
        $commandId = $this->generateUuidV4();
        $gatherOptions = $this->buildGatherCommandOptions($state, $prompt);
        $gatherPayload = [
            'duration' => 0,
            'keys' => '',
            'speech' => '',
            'speechConfidence' => 0,
        ];

        foreach ($gatherOptions as $key => $value) {
            $gatherPayload[$key] = $value;
        }

        $response = [
            'event' => [
                'callCommand' => [
                    'callId' => $callId,
                    'channelId' => $this->normalizeOptionalString($state['channel_id'] ?? ''),
                    'commandId' => $commandId,
                    'gather' => $gatherPayload,
                    'platformId' => 'voice-messagebird',
                    'platformReferenceId' => $callId,
                    'type' => 'gather',
                ],
            ],
            'requestId' => $this->requestIdForState($state),
            'result' => [
                'callId' => $callId,
                'command' => 'gather',
                'id' => $commandId,
                'status' => 'accepted',
            ],
            'resumeData' => [
                'action' => $this->resumeActionForState($state),
            ],
            'statusCode' => 202,
            'statusText' => 'Accepted',
            'suspendedAt' => $this->currentIsoTimestamp(),
        ];

        if ($resumed) {
            $response['resumedAt'] = $this->currentIsoTimestamp();
        }

        return $response;
    }

    protected function buildLifecycleGatherResponse(
        string $responseMode,
        string $callId,
        array $state,
        string $prompt,
        bool $resumed
    ): array {
        if ($responseMode === self::RESPONSE_MODE_COMMAND) {
            return $this->buildGatherAcceptedResponse($callId, $state, $prompt, $resumed);
        }

        return $this->buildFlowGatherResponse($callId, $state, $prompt, $resumed);
    }

    protected function buildFlowGatherResponse(
        string $callId,
        array $state,
        string $prompt,
        bool $resumed
    ): array {
        $gatherOptions = $this->buildGatherCommandOptions($state, $prompt);

        return [
            'requestId' => $this->requestIdForState($state),
            'callId' => $callId,
            'status' => 'gather',
            'completed' => false,
            'stage' => $this->normalizeOptionalString($state['stage'] ?? ''),
            'prompt' => $prompt,
            'gather' => [
                'input' => (string)($gatherOptions['input'] ?? 'dtmf'),
                'maxNumKeys' => (int)($gatherOptions['maxNumKeys'] ?? 1),
                'endKey' => $this->normalizeOptionalString($gatherOptions['endKey'] ?? self::DEFAULT_GATHER_END_KEY),
                'timeout' => (int)($gatherOptions['timeout'] ?? self::DEFAULT_GATHER_TIMEOUT_SECONDS),
                'retries' => (int)($gatherOptions['retries'] ?? self::DEFAULT_GATHER_RETRIES),
                'say' => [
                    'locale' => (string)((is_array($gatherOptions['say'] ?? null) ? ($gatherOptions['say']['locale'] ?? '') : '') ?: self::DEFAULT_GATHER_SAY_LOCALE),
                    'voice' => (string)((is_array($gatherOptions['say'] ?? null) ? ($gatherOptions['say']['voice'] ?? '') : '') ?: self::DEFAULT_GATHER_SAY_VOICE),
                    'text' => $prompt,
                ],
            ],
            // Bird's relay step is unreliable with nested JSON paths from HTTP responses,
            // so expose the active gather contract as flat aliases as well.
            'gatherInput' => (string)($gatherOptions['input'] ?? 'dtmf'),
            'gatherMaxNumKeys' => (int)($gatherOptions['maxNumKeys'] ?? 1),
            'gatherEndKey' => $this->normalizeOptionalString($gatherOptions['endKey'] ?? self::DEFAULT_GATHER_END_KEY),
            'gatherTimeout' => (int)($gatherOptions['timeout'] ?? self::DEFAULT_GATHER_TIMEOUT_SECONDS),
            'gatherRetries' => (int)($gatherOptions['retries'] ?? self::DEFAULT_GATHER_RETRIES),
            'gatherSayLocale' => (string)((is_array($gatherOptions['say'] ?? null) ? ($gatherOptions['say']['locale'] ?? '') : '') ?: self::DEFAULT_GATHER_SAY_LOCALE),
            'gatherSayVoice' => (string)((is_array($gatherOptions['say'] ?? null) ? ($gatherOptions['say']['voice'] ?? '') : '') ?: self::DEFAULT_GATHER_SAY_VOICE),
            'selection' => [
                'departmentId' => $this->selectedDepartmentId($state),
                'departmentName' => $this->normalizeOptionalString($state['selected_department_name'] ?? ''),
                'gateType' => $this->selectedGateType($state),
                'gateId' => $this->selectedGateId($state),
            ],
            'invalidSelectionCount' => max(0, (int)($state['invalid_selection_count'] ?? 0)),
            'resumed' => $resumed,
            'statusCode' => 200,
            'statusText' => 'OK',
        ];
    }

    protected function buildFlowCompletionResponse(
        string $callId,
        array $state,
        string $status,
        string $action,
        string $message,
        bool $gateOpened,
        ?int $departmentId,
        ?string $gateType,
        ?int $gateId,
        string $reason
    ): array {
        return [
            'requestId' => $this->requestIdForState($state),
            'callId' => $callId,
            'status' => $status,
            'action' => $action,
            'message' => $message,
            'completed' => true,
            'result' => $reason,
            'gateOpened' => $gateOpened,
            'departmentId' => $departmentId,
            'departmentName' => $this->normalizeOptionalString($state['selected_department_name'] ?? ''),
            'gateType' => $gateType,
            'gateId' => $gateId,
            'statusCode' => 200,
            'statusText' => 'OK',
            'completedAt' => $this->currentIsoTimestamp(),
        ];
    }
    protected function buildGatherCommandOptions(array $state, string $prompt): array
    {
        $options = (array)($state['gather_template'] ?? []);
        $options['input'] = 'dtmf';
        $options['maxNumKeys'] = $this->maxNumKeysForState($state);
        if ((int)$options['maxNumKeys'] > 1) {
            $options['endKey'] = '#';
        }

        $say = is_array($options['say'] ?? null) ? $options['say'] : [];
        if (!isset($say['locale']) || !is_scalar($say['locale']) || trim((string)$say['locale']) === '') {
            $say['locale'] = self::DEFAULT_GATHER_SAY_LOCALE;
        }
        if (!isset($say['voice']) || !is_scalar($say['voice']) || trim((string)$say['voice']) === '') {
            $say['voice'] = self::DEFAULT_GATHER_SAY_VOICE;
        }
        $say['text'] = $prompt;
        $options['say'] = $say;

        return $options;
    }

    protected function normalizeGatherTemplate(array $payload): array
    {
        $template = $payload;
        $template['input'] = 'dtmf';
        $template['maxNumKeys'] = 1;
        if (!isset($template['retries']) || !is_numeric($template['retries'])) {
            $template['retries'] = self::DEFAULT_GATHER_RETRIES;
        }
        if (!isset($template['timeout']) || !is_numeric($template['timeout'])) {
            $template['timeout'] = self::DEFAULT_GATHER_TIMEOUT_SECONDS;
        }
        if (!isset($template['endKey']) || !is_scalar($template['endKey']) || trim((string)$template['endKey']) === '') {
            $template['endKey'] = self::DEFAULT_GATHER_END_KEY;
        }

        $say = is_array($template['say'] ?? null) ? $template['say'] : [];
        if (!isset($say['locale']) || !is_scalar($say['locale']) || trim((string)$say['locale']) === '') {
            $say['locale'] = self::DEFAULT_GATHER_SAY_LOCALE;
        }
        if (!isset($say['voice']) || !is_scalar($say['voice']) || trim((string)$say['voice']) === '') {
            $say['voice'] = self::DEFAULT_GATHER_SAY_VOICE;
        }
        $say['text'] = '';
        $template['say'] = $say;

        return $template;
    }

    protected function extractInitialGatherTemplate(array $payload): array
    {
        if (isset($payload['payload']) && is_array($payload['payload'])) {
            return $payload['payload'];
        }

        $template = [];
        foreach (['endKey', 'input', 'maxNumKeys', 'retries', 'speechLocale', 'timeout'] as $field) {
            if (array_key_exists($field, $payload)) {
                $template[$field] = $payload[$field];
            }
        }

        if (isset($payload['say']) && is_array($payload['say'])) {
            $template['say'] = $payload['say'];
        }

        return $template;
    }

    protected function buildPromptTextForState(array $state, bool $withInvalidPrefix = false): string
    {
        $stage = $this->normalizeOptionalString($state['stage'] ?? '');

        if ($stage === self::IVR_STAGE_GATE_TYPE_SELECT) {
            $prompt = $this->buildGateTypePromptText(
                (string)($state['selected_department_name'] ?? ''),
                $this->normalizeGateOptions((array)($state['gate_options'] ?? [])),
            );
        } else {
            $prompt = $this->buildDepartmentPromptText((array)($state['department_options'] ?? []));
        }

        if ($withInvalidPrefix) {
            return 'Invalid selection. ' . $prompt;
        }

        return $prompt;
    }

    protected function loadEligibleDepartmentSummaries(): array
    {
        return (new department_gates_o())->getPhoneCallDepartmentSummaries();
    }

    protected function resolvePhoneCallGate(int $departmentId, string $gateType): ?department_gates_o
    {
        $gates = new department_gates_o();

        return $gateType === self::IVR_GATE_TYPE_EXIT
            ? $gates->getExitPhoneCallGate($departmentId)
            : $gates->getEntrancePhoneCallGate($departmentId);
    }

    protected function resolvePhoneCallGateById(int $gateId): ?department_gates_o
    {
        if ($gateId <= 0) {
            return null;
        }

        $gate = (new department_gates_o())->select($gateId);

        return $gate instanceof department_gates_o && $gate->exists() ? $gate : null;
    }

    protected function resolveSharedPhoneCallGate(int $departmentId, array $availableGateTypes): ?department_gates_o
    {
        if (
            !in_array(self::IVR_GATE_TYPE_ENTRANCE, $availableGateTypes, true)
            || !in_array(self::IVR_GATE_TYPE_EXIT, $availableGateTypes, true)
        ) {
            return null;
        }

        $entranceGate = $this->resolvePhoneCallGate($departmentId, self::IVR_GATE_TYPE_ENTRANCE);
        $exitGate = $this->resolvePhoneCallGate($departmentId, self::IVR_GATE_TYPE_EXIT);
        if (
            $entranceGate === null
            || !$entranceGate->exists()
            || $exitGate === null
            || !$exitGate->exists()
        ) {
            return null;
        }

        return (int)$entranceGate->id === (int)$exitGate->id ? $entranceGate : null;
    }

    protected function triggerGateOpen(department_gates_o $gate): void
    {
        $gate->openGate();
    }

    protected function resolveBirdClient(): bird
    {
        return new bird();
    }

    protected function acceptInboundCall(
        bird $client,
        string $workspaceId,
        string $channelId,
        string $callId
    ): void {
        if (!$this->shouldAcceptInboundCall($client)) {
            return;
        }

        try {
            $client->answerVoiceCall($workspaceId, $channelId, $callId, []);
        } catch (\Throwable) {
            // Flow Builder should answer inbound calls before invoking this webhook.
            // Treat backend acceptance as a best-effort fallback.
        }
    }

    protected function shouldAcceptInboundCall(bird $client): bool
    {
        return $this->isBirdModuleEnabled($client)
            && $this->readBirdConfigValue($client, 'api_key') !== ''
            && $this->readBirdConfigValue($client, 'server_url') !== '';
    }

    protected function isBirdModuleEnabled(bird $client): bool
    {
        try {
            if (!isset($client->config) || !is_object($client->config) || !property_exists($client->config, 'enabled')) {
                return false;
            }

            $enabledConfig = $client->config->enabled;
            if (!is_object($enabledConfig)) {
                return false;
            }

            if (method_exists($enabledConfig, 'isTrue')) {
                return $enabledConfig->isTrue();
            }

            if (method_exists($enabledConfig, 'getVariableValue')) {
                return filter_var($enabledConfig->getVariableValue(), FILTER_VALIDATE_BOOLEAN);
            }
        } catch (\Throwable) {
            return false;
        }

        return false;
    }

    protected function readBirdConfigValue(bird $client, string $property): string
    {
        try {
            if (
                !isset($client->config)
                || !is_object($client->config)
                || !property_exists($client->config, $property)
            ) {
                return '';
            }

            $configValue = $client->config->{$property};
            if (!is_object($configValue) || !method_exists($configValue, 'getVariableValue')) {
                return '';
            }

            return $this->normalizeOptionalString($configValue->getVariableValue());
        } catch (\Throwable) {
            return '';
        }
    }

    protected function readInboundWebhookPayload(): array
    {
        $rawBody = file_get_contents('php://input');
        if (is_string($rawBody) && trim($rawBody) !== '') {
            $decoded = json_decode($rawBody, true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }

        return is_array($_POST) ? $_POST : [];
    }

    protected function isSupportedInboundRequest(array $payload): bool
    {
        if ($this->isInitialGatherRequest($payload) || $this->isResumedGatherRequest($payload)) {
            return true;
        }

        return $this->extractCallIdFromPayload($payload) !== ''
            && $this->extractWorkspaceIdFromPayload($payload) !== ''
            && $this->extractChannelIdFromPayload($payload) !== '';
    }

    protected function isInitialGatherRequest(array $payload): bool
    {
        return isset($payload['payload'], $payload['request'], $payload['waitConditions'])
            && is_array($payload['payload'])
            && is_array($payload['request'])
            && is_array($payload['waitConditions']);
    }

    protected function isResumedGatherRequest(array $payload): bool
    {
        return isset($payload['event']) && is_array($payload['event']);
    }

    protected function shouldBootstrapIvrState(array $payload, array $state): bool
    {
        if ($this->isInitialGatherRequest($payload)) {
            return true;
        }

        if ($this->isResumedGatherRequest($payload)) {
            return false;
        }

        return !$this->hasActiveIvrState($state);
    }

    protected function hasActiveIvrState(array $state): bool
    {
        return $state !== [] && $this->normalizeOptionalString($state['stage'] ?? '') !== '';
    }

    protected function responseModeForPayload(array $payload, array $state = []): string
    {
        $stateMode = $this->responseModeForState($state);
        if ($stateMode !== '') {
            return $stateMode;
        }

        if ($this->isResumedGatherRequest($payload) || $this->isInitialGatherRequest($payload)) {
            return self::RESPONSE_MODE_COMMAND;
        }

        return self::RESPONSE_MODE_FLOW;
    }

    protected function responseModeForState(array $state): string
    {
        $mode = $this->normalizeOptionalString($state['response_mode'] ?? '');

        return in_array($mode, [self::RESPONSE_MODE_COMMAND, self::RESPONSE_MODE_FLOW], true)
            ? $mode
            : '';
    }

    protected function resolveInboundCallId(array $payload): string
    {
        $queryCallId = $this->normalizeOptionalString($this->fromQuery('callId'));
        if ($queryCallId !== '') {
            return $queryCallId;
        }

        if (isset($_POST['callId']) && is_scalar($_POST['callId'])) {
            $postCallId = $this->normalizeOptionalString((string)$_POST['callId']);
            if ($postCallId !== '') {
                return $postCallId;
            }
        }

        return $this->extractCallIdFromPayload($payload);
    }

    protected function resolveInboundWorkspaceId(bird $client, array $payload): string
    {
        $queryWorkspaceId = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
        if ($queryWorkspaceId !== '') {
            return $queryWorkspaceId;
        }

        if (isset($_POST['workspaceId']) && is_scalar($_POST['workspaceId'])) {
            $postWorkspaceId = $this->normalizeOptionalString((string)$_POST['workspaceId']);
            if ($postWorkspaceId !== '') {
                return $postWorkspaceId;
            }
        }

        $payloadWorkspaceId = $this->extractWorkspaceIdFromPayload($payload);
        if ($payloadWorkspaceId !== '') {
            return $payloadWorkspaceId;
        }

        return $this->getConfiguredWorkspaceId($client);
    }

    protected function resolveInboundChannelId(bird $client, array $payload): string
    {
        $queryChannelId = $this->normalizeOptionalString($this->fromQuery('channelId'));
        if ($queryChannelId !== '') {
            return $queryChannelId;
        }

        if (isset($_POST['channelId']) && is_scalar($_POST['channelId'])) {
            $postChannelId = $this->normalizeOptionalString((string)$_POST['channelId']);
            if ($postChannelId !== '') {
                return $postChannelId;
            }
        }

        $payloadChannelId = $this->extractChannelIdFromPayload($payload);
        if ($payloadChannelId !== '') {
            return $payloadChannelId;
        }

        return $this->getConfiguredChannelId($client);
    }

    protected function extractCallIdFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['request', 'callId'],
            ['body', 'callId'],
            ['event', 'callCommand', 'callId'],
            ['callId'],
            ['call_id'],
            ['call', 'id'],
            ['data', 'callId'],
            ['payload', 'callId'],
            ['event', 'callId'],
            ['voice', 'callId'],
        ]);
    }

    protected function extractWorkspaceIdFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['request', 'workspaceId'],
            ['body', 'workspaceId'],
            ['workspaceId'],
            ['workspace_id'],
            ['call', 'workspaceId'],
            ['data', 'workspaceId'],
            ['payload', 'workspaceId'],
        ]);
    }

    protected function extractChannelIdFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['request', 'channelId'],
            ['body', 'channelId'],
            ['event', 'callCommand', 'channelId'],
            ['channelId'],
            ['channel_id'],
            ['call', 'channelId'],
            ['data', 'channelId'],
            ['payload', 'channelId'],
        ]);
    }

    protected function extractRequestIdFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['requestId'],
            ['request', 'requestId'],
            ['event', 'requestId'],
        ]);
    }

    protected function extractResumeActionFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['resumeData', 'action'],
            ['waitConditions', 'events', 0, 'action'],
        ]);
    }

    protected function extractWaitTimeoutFromPayload(array $payload): string
    {
        return $this->extractPayloadStringByPaths($payload, [
            ['waitConditions', 'timeout'],
        ]);
    }

    protected function extractGatherKeysFromEvent(array $payload): ?string
    {
        $value = $this->extractPayloadStringByPaths($payload, [
            ['event', 'callCommand', 'gather', 'keys'],
            ['event', 'callCommand', 'gather', 'key'],
        ]);

        return $value !== '' ? $value : null;
    }

    protected function extractDtmfInput(array $payload): ?string
    {
        $value = $this->extractPayloadStringByPaths($payload, [
            ['dtmf'],
            ['digit'],
            ['digits'],
            ['keys'],
            ['key'],
            ['input'],
            ['input', 'dtmf'],
            ['input', 'digit'],
            ['input', 'digits'],
            ['input', 'keys'],
            ['input', 'key'],
            ['gather', 'dtmf'],
            ['gather', 'digit'],
            ['gather', 'digits'],
            ['gather', 'keys'],
            ['gather', 'key'],
            ['event', 'data', 'dtmf'],
            ['event', 'data', 'digit'],
            ['event', 'data', 'digits'],
            ['event', 'data', 'keys'],
            ['event', 'data', 'key'],
            ['result', 'dtmf'],
            ['result', 'digit'],
            ['result', 'digits'],
            ['result', 'keys'],
            ['result', 'key'],
            ['response', 'dtmf'],
            ['response', 'digit'],
            ['response', 'digits'],
            ['response', 'keys'],
            ['response', 'key'],
            ['variables', 'keys'],
            ['conditions', 0, 'value'],
        ]);

        return $value !== '' ? $value : null;
    }
    protected function normalizeMenuDigitInput(?string $input): ?string
    {
        if ($input === null) {
            return null;
        }

        $normalized = trim($input);
        while ($normalized !== '' && str_ends_with($normalized, '#')) {
            $normalized = substr($normalized, 0, -1);
        }

        $normalized = preg_replace('/[^0-9]/', '', $normalized);
        if (!is_string($normalized) || $normalized === '') {
            return null;
        }

        return preg_match('/^[1-9][0-9]*$/', $normalized) === 1 ? $normalized : null;
    }

    protected function extractPayloadStringByPaths(array $payload, array $candidatePaths): string
    {
        foreach ($candidatePaths as $path) {
            if (!is_array($path) || $path === []) {
                continue;
            }

            $value = $this->extractPayloadValueByPath($payload, $path);
            if (!is_scalar($value)) {
                continue;
            }

            $normalized = $this->normalizeOptionalString((string)$value);
            if ($normalized !== '') {
                return $normalized;
            }
        }

        return '';
    }

    protected function extractPayloadValueByPath(array $payload, array $path): mixed
    {
        $cursor = $payload;

        foreach ($path as $segment) {
            if (is_array($cursor) && array_key_exists($segment, $cursor)) {
                $cursor = $cursor[$segment];
                continue;
            }

            return null;
        }

        return $cursor;
    }

    protected function buildDepartmentOptionMap(array $departmentIds): array
    {
        $map = [];
        $index = 1;

        foreach ($departmentIds as $departmentEntry) {
            $departmentName = '';
            $hasEntranceGate = false;
            $hasExitGate = false;

            if (is_array($departmentEntry)) {
                $id = (int)($departmentEntry['department_id'] ?? 0);
                $departmentName = $this->normalizeOptionalString($departmentEntry['department_name'] ?? '');
                $hasEntranceGate = ($departmentEntry['has_entrance_gate'] ?? false) === true;
                $hasExitGate = ($departmentEntry['has_exit_gate'] ?? false) === true;
            } else {
                $id = (int)$departmentEntry;
            }

            if ($id <= 0) {
                continue;
            }

            $digit = (string)$index;
            $map[$digit] = [
                'department_id' => $id,
                'department_name' => $departmentName !== '' ? $departmentName : $this->getDepartmentNameById($id),
                'has_entrance_gate' => $hasEntranceGate,
                'has_exit_gate' => $hasExitGate,
            ];
            $index++;
        }

        return $map;
    }

    protected function buildDepartmentPromptText(array $optionMap): string
    {
        if ($optionMap === []) {
            return 'Choose department.';
        }

        $parts = ['Choose department.'];
        if ($this->requiresDepartmentTerminator($optionMap)) {
            $parts[] = 'Enter the option number followed by pound.';
        }

        foreach ($optionMap as $digit => $department) {
            $name = (string)($department['department_name'] ?? '');
            if ($name === '') {
                $name = 'department ' . (string)($department['department_id'] ?? '');
            }
            $parts[] = 'Press ' . $digit . ' for ' . $name . '.';
        }

        return implode(' ', $parts);
    }

    protected function buildGateTypePromptText(string $departmentName, array $gateOptions = []): string
    {
        $prefix = $departmentName !== '' ? 'You selected ' . $departmentName . '. ' : '';
        $gateOptions = $this->normalizeGateOptions($gateOptions);

        if ($gateOptions === []) {
            return $prefix . 'Press 1 for entrance. Press 2 for exit.';
        }

        $parts = [];
        foreach ($gateOptions as $digit => $gateType) {
            $parts[] = 'Press ' . $digit . ' for ' . $gateType . '.';
        }

        return $prefix . implode(' ', $parts);
    }

    protected function resolveDepartmentIdByDigit(array $optionMap, string $digits): ?int
    {
        $key = trim($digits);
        if ($key === '' || !isset($optionMap[$key])) {
            return null;
        }

        $id = (int)($optionMap[$key]['department_id'] ?? 0);

        return $id > 0 ? $id : null;
    }

    protected function resolveGateTypeByDigit(string $digits, array $gateOptions = []): ?string
    {
        $key = trim($digits);
        $gateOptions = $this->normalizeGateOptions($gateOptions);
        if ($gateOptions !== []) {
            return $gateOptions[$key] ?? null;
        }

        if ($key === '1') {
            return self::IVR_GATE_TYPE_ENTRANCE;
        }
        if ($key === '2') {
            return self::IVR_GATE_TYPE_EXIT;
        }

        return null;
    }

    protected function resolveSelectedDepartmentOption(array $optionMap, string $digit): ?array
    {
        $key = trim($digit);

        return isset($optionMap[$key]) && is_array($optionMap[$key]) ? $optionMap[$key] : null;
    }

    protected function extractAvailableGateTypes(array $departmentOption): array
    {
        $availableGateTypes = [];

        if (($departmentOption['has_entrance_gate'] ?? false) === true) {
            $availableGateTypes[] = self::IVR_GATE_TYPE_ENTRANCE;
        }
        if (($departmentOption['has_exit_gate'] ?? false) === true) {
            $availableGateTypes[] = self::IVR_GATE_TYPE_EXIT;
        }

        return $availableGateTypes;
    }

    protected function buildGateOptionMap(array $availableGateTypes): array
    {
        $gateOptions = [];
        $digit = 1;

        foreach ($availableGateTypes as $gateType) {
            $normalizedGateType = $this->normalizeOptionalString((string)$gateType);
            if ($normalizedGateType === '') {
                continue;
            }

            $gateOptions[(string)$digit] = $normalizedGateType;
            $digit++;
        }

        return $gateOptions;
    }

    protected function normalizeGateOptions(array $gateOptions): array
    {
        $normalized = [];

        foreach ($gateOptions as $digit => $gateType) {
            $normalizedDigit = $this->normalizeMenuDigitInput((string)$digit);
            $normalizedGateType = $this->normalizeOptionalString((string)$gateType);
            if ($normalizedDigit === null || $normalizedGateType === '') {
                continue;
            }

            $normalized[$normalizedDigit] = $normalizedGateType;
        }

        return $normalized;
    }

    protected function maxNumKeysForState(array $state): int
    {
        $stage = $this->normalizeOptionalString($state['stage'] ?? '');
        if ($stage !== self::IVR_STAGE_DEPARTMENT_SELECT) {
            return 1;
        }

        $departmentOptions = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
        $count = count($departmentOptions);

        return max(1, strlen((string)$count));
    }

    protected function requiresDepartmentTerminator(array $optionMap): bool
    {
        return count($optionMap) > 9;
    }

    protected function getDepartmentNameById(int $departmentId): string
    {
        $row = (new departments_o())->getDepartmentById($departmentId);

        return trim((string)($row['name'] ?? ('Department ' . $departmentId)));
    }

    protected function saveIvrState(string $callId, array $state): void
    {
        if ($callId === '') {
            return;
        }

        try {
            $encoded = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (!is_string($encoded) || $encoded === '') {
                return;
            }

            $this->writeIvrStateRaw($this->ivrStateKey($callId), $encoded, self::IVR_STATE_TTL_SECONDS);
        } catch (\Throwable) {
        }
    }

    protected function readIvrState(string $callId): ?array
    {
        if ($callId === '') {
            return null;
        }

        try {
            $raw = $this->readIvrStateRaw($this->ivrStateKey($callId));
            if (!is_string($raw) || trim($raw) === '') {
                return null;
            }

            $decoded = json_decode($raw, true);

            return is_array($decoded) ? $decoded : null;
        } catch (\Throwable) {
            return null;
        }
    }

    protected function clearIvrState(string $callId): void
    {
        if ($callId === '') {
            return;
        }

        try {
            $this->deleteIvrStateRaw($this->ivrStateKey($callId));
        } catch (\Throwable) {
        }
    }

    protected function normalizeState(array $state): array
    {
        if ($state === []) {
            return [];
        }

        $state['response_mode'] = $this->responseModeForState($state) ?: self::RESPONSE_MODE_FLOW;
        $state['department_options'] = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
        $state['available_gate_types'] = array_values(array_filter(
            is_array($state['available_gate_types'] ?? null) ? $state['available_gate_types'] : [],
            static fn(mixed $value): bool => is_string($value) && $value !== ''
        ));
        $state['gate_options'] = $this->normalizeGateOptions(
            is_array($state['gate_options'] ?? null) ? $state['gate_options'] : []
        );
        if ($state['gate_options'] === [] && $state['available_gate_types'] !== []) {
            $state['gate_options'] = $this->buildGateOptionMap($state['available_gate_types']);
        }
        $state['gather_template'] = is_array($state['gather_template'] ?? null) ? $state['gather_template'] : [];

        return $state;
    }

    protected function ivrStateKey(string $callId): string
    {
        return self::IVR_STATE_PREFIX . $callId;
    }

    protected function ivrLockKey(string $callId): string
    {
        return self::IVR_LOCK_PREFIX . $callId;
    }

    protected function acquireIvrLock(string $callId): bool
    {
        if ($callId === '') {
            return false;
        }

        try {
            return $this->acquireIvrLockRaw($this->ivrLockKey($callId), self::IVR_LOCK_TTL_SECONDS);
        } catch (\Throwable) {
            return true;
        }
    }

    protected function releaseIvrLock(string $callId): void
    {
        if ($callId === '') {
            return;
        }

        try {
            $this->releaseIvrLockRaw($this->ivrLockKey($callId));
        } catch (\Throwable) {
        }
    }

    protected function acquireIvrLockRaw(string $key, int $ttlSeconds): bool
    {
        $redis = $this->resolveRedisClient();
        if ($redis === null) {
            return true;
        }

        if ($redis->exists($key)) {
            return false;
        }

        $redis->set($key, $this->generateUuidV4());
        $redis->expire($key, $ttlSeconds);

        return true;
    }

    protected function releaseIvrLockRaw(string $key): void
    {
        $redis = $this->resolveRedisClient();
        if ($redis === null) {
            return;
        }

        $redis->delete($key);
    }

    protected function writeIvrStateRaw(string $key, string $value, int $ttlSeconds): void
    {
        $redis = $this->resolveRedisClient();
        if ($redis === null) {
            return;
        }

        $redis->set($key, $value);
        $redis->expire($key, $ttlSeconds);
    }

    protected function readIvrStateRaw(string $key): ?string
    {
        $redis = $this->resolveRedisClient();
        if ($redis === null) {
            return null;
        }

        return $redis->get($key);
    }

    protected function deleteIvrStateRaw(string $key): void
    {
        $redis = $this->resolveRedisClient();
        if ($redis === null) {
            return;
        }

        $redis->delete($key);
    }

    protected function resolveRedisClient(): ?redis
    {
        try {
            return (new redis())->connect();
        } catch (\Throwable) {
            return null;
        }
    }

    protected function requestIdForState(array $state): string
    {
        $requestId = $this->normalizeOptionalString($state['request_id'] ?? '');

        return $requestId !== '' ? $requestId : $this->generateUuidV4();
    }

    protected function resumeActionForState(array $state): string
    {
        $action = $this->normalizeOptionalString($state['resume_action'] ?? '');

        return $action !== '' ? $action : 'continue';
    }

    protected function selectedDepartmentId(array $state): ?int
    {
        $departmentId = (int)($state['selected_department_id'] ?? 0);

        return $departmentId > 0 ? $departmentId : null;
    }

    protected function selectedGateType(array $state): ?string
    {
        $gateType = $this->normalizeOptionalString($state['selected_gate_type'] ?? '');

        return $gateType !== '' ? $gateType : null;
    }

    protected function selectedGateId(array $state): ?int
    {
        $gateId = (int)($state['gate_id'] ?? 0);

        return $gateId > 0 ? $gateId : null;
    }

    protected function buildGateOpenedMessage(?string $gateType): string
    {
        return $gateType !== null ? 'Opening the ' . $gateType . ' gate now.' : 'Opening the gate now.';
    }

    protected function buildGateOpenFailedMessage(?string $gateType, string $errorMessage): string
    {
        $prefix = $gateType !== null ? 'Failed to open the ' . $gateType . ' gate.' : 'Failed to open the gate.';

        return $errorMessage !== '' ? $prefix . ' ' . $errorMessage : $prefix;
    }

    protected function buildMissingGateMessage(?string $gateType): string
    {
        if ($gateType === null) {
            return 'The selected gate is no longer available.';
        }

        return 'No phone-controlled ' . $gateType . ' gate is configured for the selected department.';
    }

    protected function buildTransportErrorResponse(array $payload, int $statusCode, string $message): array
    {
        return [
            'requestId' => $this->extractRequestIdFromPayload($payload) ?: $this->generateUuidV4(),
            'statusCode' => $statusCode,
            'statusText' => $this->statusTextForCode($statusCode),
            'error' => [
                'message' => $message,
            ],
        ];
    }

    protected function statusTextForCode(int $statusCode): string
    {
        return match ($statusCode) {
            202 => 'Accepted',
            400 => 'Bad Request',
            500 => 'Internal Server Error',
            default => 'OK',
        };
    }

    protected function currentIsoTimestamp(): string
    {
        return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.u\Z');
    }

    protected function generateUuidV4(): string
    {
        $bytes = random_bytes(16);
        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
        $hex = bin2hex($bytes);

        return sprintf(
            '%s-%s-%s-%s-%s',
            substr($hex, 0, 8),
            substr($hex, 8, 4),
            substr($hex, 12, 4),
            substr($hex, 16, 4),
            substr($hex, 20, 12),
        );
    }
}
