<?php

namespace routes;

use classes\bird;
use classes\redis;
use classes\slack;
use objects\department_gates_o;
use objects\departments_o;
use objects\subusers_o;
use objects\users_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 = 600;
    private const IVR_STATE_PREFIX = 'bird_ivr_state:';
    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';

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

            //self::requirePermission('modules_bird_voice_call_webhooks_trigger');
            $client = new bird();

            $ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
            $ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
            if ($ws === '') {
                $ws = $this->getConfiguredWorkspaceId($client);
            }
            if ($ch === '') {
                $ch = $this->getConfiguredChannelId($client);
            }
            if ($ws === '' || $ch === '') {
                $response->error('Missing required parameters: workspaceId, channelId', 400);
            }

            $payload = $this->getParametersAsArray();
            $callId = $this->normalizeOptionalString($this->fromRequest('call_id') ?? $this->fromQuery('call_id'));
            $digits = $this->normalizeOptionalString($payload['digits'] ?? $this->fromRequest('digits') ?? $this->fromQuery('digits'));
            if ($callId === '') {
                $response->error('Missing required parameter: call_id', 400);
            }

            $caller = $this->extractCallerPhone($payload);
            if ($caller === null) {
                $this->say($client, $ws, $ch, $callId, 'Vi kunne ikke identificere dit telefonnummer. Kontakt venligst support.', true);
                return;
            }

            [$countryCode, $localPhone] = $caller;

            $slack = new Slack();
            $slack->send_message('Webhook received for incoming voice call (ID: ' . $callId . '): ' . json_encode($payload), 'Bird Voice Call Webhooks');

            if (!$this->isRegisteredCaller($countryCode, $localPhone)) {
                $this->say($client, $ws, $ch, $callId, 'Du er ikke sat op til automatisk portaabning med landekode ' . $countryCode . ' og telefonnummer ' . $localPhone . '. Kontakt venligst support for at blive sat op.', true);
                return;
            }

            $departments = $this->getCallerDepartments($countryCode, $localPhone);
            if (empty($departments)) {
                $this->clearIvrState($callId);
                $this->say($client, $ws, $ch, $callId, 'Vi kunne ikke finde nogen afdelinger tilknyttet din profil. Kontakt venligst support.', true);
                return;
            }

            if ($digits !== '' && $callId !== '') {
                $this->handleGatherResponse($client, $ws, $ch, $callId, $digits, $countryCode, $localPhone, $departments);
                return;
            }

            $this->promptForDepartmentSelection($client, $ws, $ch, $callId, $departments, $countryCode, $localPhone);

        }, [
            'modules_bird_voice_call_webhooks_trigger' => 'Trigger a voice call via Bird webhooks',
        ]);
    }

    protected function handleGatherResponse(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        string $digits,
        int $countryCode,
        int $phone,
        array $departments
    ): void {
        $state = $this->readIvrState($callId);
        if ($state === null || !$this->isStateForCaller($state, $countryCode, $phone)) {
            $this->promptForDepartmentSelection($client, $ws, $ch, $callId, $departments, $countryCode, $phone);
            return;
        }

        $stage = (string)($state['stage'] ?? '');
        if ($stage === self::IVR_STAGE_DEPARTMENT_SELECT) {
            $this->handleDepartmentSelectionDigit($client, $ws, $ch, $callId, $digits, $state, $countryCode, $phone);
            return;
        }
        if ($stage === self::IVR_STAGE_GATE_TYPE_SELECT) {
            $this->handleGateTypeSelectionDigit($client, $ws, $ch, $callId, $digits, $state);
            return;
        }

        $this->clearIvrState($callId);
        $this->promptForDepartmentSelection($client, $ws, $ch, $callId, $departments, $countryCode, $phone);
    }

    protected function handleDepartmentSelectionDigit(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        string $digits,
        array $state,
        int $countryCode,
        int $phone
    ): void {
        $optionMap = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
        $selected = $this->resolveDepartmentIdByDigit($optionMap, $digits);
        if ($selected === null) {
            $this->promptForDepartmentRetry($client, $ws, $ch, $callId, $state);
            return;
        }

        $state['stage'] = self::IVR_STAGE_GATE_TYPE_SELECT;
        $state['selected_department'] = $selected;
        $state['country_code'] = $countryCode;
        $state['phone'] = $phone;
        $state['selected_department_name'] = $this->getDepartmentNameById($selected);
        $this->saveIvrState($callId, $state);

        $this->promptForGateTypeSelection($client, $ws, $ch, $callId, $selected, (string)$state['selected_department_name']);
    }

    protected function handleGateTypeSelectionDigit(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        string $digits,
        array $state
    ): void {
        $departmentId = (int)($state['selected_department'] ?? 0);
        if ($departmentId <= 0) {
            $this->clearIvrState($callId);
            $this->say($client, $ws, $ch, $callId, 'Sessionen er udloeebet. Ring venligst op igen.', true);
            return;
        }

        $gateType = $this->resolveGateTypeByDigit($digits);
        if ($gateType === null) {
            $departmentName = (string)($state['selected_department_name'] ?? $this->getDepartmentNameById($departmentId));
            $this->promptForGateTypeRetry($client, $ws, $ch, $callId, $departmentId, $departmentName);
            return;
        }

        $gate = $gateType === self::IVR_GATE_TYPE_ENTRANCE
            ? (new department_gates_o())->getEntranceGate($departmentId)
            : (new department_gates_o())->getExitGate($departmentId);

        $this->clearIvrState($callId);

        if ($gate === null || !$gate->exists()) {
            $directionLabel = $gateType === self::IVR_GATE_TYPE_ENTRANCE ? 'indgang' : 'udgang';
            $this->say($client, $ws, $ch, $callId, 'Vi kunne ikke finde en ' . $directionLabel . 'sport for den valgte afdeling.', true);
            return;
        }

        if ($this->callGateToOpen($gate)) {
            $this->say($client, $ws, $ch, $callId, 'Aabner port: ' . $gate->name->value(), true);
            return;
        }

        $this->say($client, $ws, $ch, $callId, 'Kunne ikke aktivere relaeet for ' . $gate->name->value() . '. Kontakt venligst support.', true);
    }

    protected function getCallerDepartments(int $countryCode, int $phone): array
    {
        // Return all visible departments.
        $tmp = (new departments_o())->getFieldsWhere([
            'visible' => 1,
        ], ['id']);
        return array_map(function ($row) {
            return (int)$row['id'];
        }, $tmp);
    }

    protected function promptForDepartmentSelection(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        array $departments,
        int $countryCode,
        int $phone
    ): void {
        $optionMap = $this->buildDepartmentOptionMap($departments);
        if (empty($optionMap)) {
            $this->say($client, $ws, $ch, $callId, 'Der er ingen valgbare afdelinger lige nu. Kontakt venligst support.', true);
            return;
        }

        $state = [
            'stage' => self::IVR_STAGE_DEPARTMENT_SELECT,
            'department_options' => $optionMap,
            'selected_department' => null,
            'country_code' => $countryCode,
            'phone' => $phone,
        ];
        $this->saveIvrState($callId, $state);

        $text = $this->buildDepartmentPromptText($optionMap);
        $this->gather($client, $ws, $ch, $callId, $text);
    }

    protected function promptForDepartmentRetry(bird $client, string $ws, string $ch, string $callId, array $state): void
    {
        $optionMap = is_array($state['department_options'] ?? null) ? $state['department_options'] : [];
        if (empty($optionMap)) {
            $this->clearIvrState($callId);
            $this->say($client, $ws, $ch, $callId, 'Sessionen er udloeebet. Ring venligst op igen.', true);
            return;
        }
        $text = 'Ugyldigt valg. ' . $this->buildDepartmentPromptText($optionMap);
        $this->gather($client, $ws, $ch, $callId, $text);
    }

    protected function promptForGateTypeSelection(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        int $departmentId,
        string $departmentName
    ): void {
        $text = $this->buildGateTypePromptText($departmentName);
        $this->gather($client, $ws, $ch, $callId, $text);
    }

    protected function promptForGateTypeRetry(
        bird $client,
        string $ws,
        string $ch,
        string $callId,
        int $departmentId,
        string $departmentName
    ): void {
        $text = 'Ugyldigt valg. ' . $this->buildGateTypePromptText($departmentName);
        $this->gather($client, $ws, $ch, $callId, $text);
    }

    protected function buildDepartmentOptionMap(array $departmentIds): array
    {
        $map = [];
        $index = 1;
        foreach ($departmentIds as $departmentId) {
            if ($index > 9) {
                break;
            }
            $id = (int)$departmentId;
            if ($id <= 0) {
                continue;
            }
            $digit = (string)$index;
            $map[$digit] = [
                'department_id' => $id,
                'department_name' => $this->getDepartmentNameById($id),
            ];
            $index++;
        }
        return $map;
    }

    protected function buildDepartmentPromptText(array $optionMap): string
    {
        $parts = [];
        foreach ($optionMap as $digit => $department) {
            $name = (string)($department['department_name'] ?? '');
            if ($name === '') {
                $name = 'afdeling ' . (string)($department['department_id'] ?? '');
            }
            $parts[] = 'Tast ' . $digit . ' for ' . $name;
        }
        return 'Vaelg afdeling. ' . implode('. ', $parts) . '. Afslut med firkantstasten.';
    }

    protected function buildGateTypePromptText(string $departmentName): string
    {
        $prefix = $departmentName !== '' ? 'Du valgte ' . $departmentName . '. ' : '';
        return $prefix . 'Tast 1 for indgang. Tast 2 for udgang. Afslut med firkantstasten.';
    }

    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): ?string
    {
        $key = trim($digits);
        if ($key === '1') {
            return self::IVR_GATE_TYPE_ENTRANCE;
        }
        if ($key === '2') {
            return self::IVR_GATE_TYPE_EXIT;
        }
        return null;
    }

    protected function getDepartmentNameById(int $departmentId): string
    {
        $row = (new departments_o())->getDepartmentById($departmentId);
        return trim((string)($row['name'] ?? 'Afdeling ' . $departmentId));
    }

    protected function gather(bird $client, string $ws, string $ch, string $callId, string $text): void
    {
        if ($callId === '') {
            throw new \Exception('Call ID is missing, cannot gather input');
        }

        $client->gatherMessage($ws, $ch, $callId, [
            'input' => 'dtmf',
            'maxNumKeys' => 1,
            'retries' => 1,
            'timeout' => 8,
            'endKey' => '#',
            'say' => [
                'text' => $text,
            ],
        ]);
        $this->respondJsonAndExit();
    }

    protected function saveIvrState(string $callId, array $state): void
    {
        if ($callId === '') {
            return;
        }
        try {
            $encoded = json_encode($state);
            if (!is_string($encoded) || $encoded === '') {
                return;
            }
            $this->writeIvrStateRaw($this->ivrStateKey($callId), $encoded, self::IVR_STATE_TTL_SECONDS);
        } catch (\Throwable) {
            // State caching should not break webhook flow.
        }
    }

    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) {
            // Best effort only.
        }
    }

    protected function isStateForCaller(array $state, int $countryCode, int $phone): bool
    {
        return (int)($state['country_code'] ?? 0) === $countryCode
            && (int)($state['phone'] ?? 0) === $phone;
    }

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

    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;
        }
    }

    public function callGateToOpen(department_gates_o $gate): bool
    {
        $config = (array)$gate->config->value();

        if (!isset($config['type']) || $config['type'] !== 'PHONE_CALL') {
            return false;
        }
        if (!isset($config['phone_number'])) {
            return false;
        }
        $phoneConfig = $config['phone_number'];
        $normalized = $this->normalizePhoneCandidate($phoneConfig);
        if ($normalized === null) {
            return false;
        }
        [$countryCode, $phone] = $normalized;

        // Use the configured threshold or default to 10 seconds.
        $timeout = (int)($config['call_duration_threshold'] ?? 10);

        // Use bird voice call to call the phone number, and then hang up after it is accepted to trigger the gate.
        $client = new bird();
        try {
            $client->callGateAndHangupWhenAccepted(
                (int)$countryCode,
                (int)$phone,
                $timeout,
            );
            return true;
        } catch (\Throwable $e) {
            // If the call fails, log the error and return false
            $slack = new Slack();
            $slack->send_message('Failed to call gate for phone ' . $countryCode . $phone . ': ' . $e->getMessage(), 'Bird Voice Call Webhooks');
            return false;
        }
    }

    protected function extractCallerPhone(array $payload): ?array
    {
        $candidates = [
            $payload['phone_number'] ?? null,
            $payload['source'] ?? null,
            $payload['from'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            $normalized = $this->normalizePhoneCandidate($candidate);
            if ($normalized !== null) {
                return $normalized;
            }
        }

        return null;
    }

    protected function normalizePhoneCandidate(mixed $candidate): ?array
    {
        if (is_array($candidate)) {
            // Determine the country code from the phone number if possible, otherwise default to 45 (Denmark)
            $phone = $candidate['phone_number'] ?? null; // E.g. +4512345678
            // If the phone number starts with a + followed by the country code and then the local phone number, we can extract the country code and local phone number

            if ($phone !== null) {
                $country = $this->extractCountryCodeFromPhoneNumber($phone);
                return $this->normalizePhone((string)$phone, $country);
            }
            return null;
        }

        if (is_string($candidate) && trim($candidate) !== '') {
            return $this->normalizePhone($candidate);
        }

        return null;
    }

    protected function normalizePhone(string $raw, ?int $defaultCountryCode = 45): ?array
    {
        $trimmed = trim($raw);
        if ($trimmed === '') {
            return null;
        }
        $digits = preg_replace('/\D+/', '', $trimmed);
        if (!is_string($digits) || $digits === '') {
            return null;
        }

        $country = $defaultCountryCode;
        $phone = $digits;

        if (str_starts_with($trimmed, '+')) {
            $extractedCountry = $this->extractCountryCodeFromPhoneNumber($trimmed);
            if ($extractedCountry !== null) {
                $country = $extractedCountry;
                $phone = substr($digits, strlen((string)$extractedCountry));
            } elseif (strlen($digits) > 8) {
                $country = (int)substr($digits, 0, 2);
                $phone = substr($digits, 2);
            }
        } elseif ($country !== null && str_starts_with($digits, (string)$country) && strlen($digits) > 8) {
            $phone = substr($digits, strlen((string)$country));
        }

        if ($country === null || $country <= 0 || $phone === '') {
            return null;
        }

        return [$country, (int)$phone];
    }

    protected function isRegisteredCaller(int $countryCode, int $phone): bool
    {
        $userRows = (new users_o())->getFieldsWhere([
            'phone_country_code' => $countryCode,
            'phone' => $phone,
        ], ['id']);
        if (!empty($userRows)) {
            return true;
        }

        return (new subusers_o())->getSubuserByPhone($countryCode, $phone) !== null;
    }

    protected function say(bird $client, string $ws, string $ch, string $callId, string $text, bool $hangup = false): void
    {
        if ($callId !== '') {
            try {
                $payload = ['text' => $text];
                if ($hangup) {
                    $payload['hangup'] = true;
                }
                $client->sayMessage($ws, $ch, $callId, $payload);
                $this->respondJsonAndExit();
            } catch (\Throwable $e) {
                // If REST API fails, fallback to standard response
                throw new \Exception('Failed to say message and hang up: ' . $e->getMessage(), 0, $e);
            }
        } else {
            throw new \Exception('Call ID is missing, cannot say message');
        }
    }

    protected function respondJsonAndExit(): void
    {
        header('Content-Type: application/json');
        echo json_encode([]);
        exit;
    }

    protected function extractCountryCodeFromPhoneNumber(string $phone): ?int
    {
        if (str_starts_with($phone, '+45')) {
            return 45;
        }
        if (str_starts_with($phone, '+46')) {
            return 46;
        }
        if (str_starts_with($phone, '+47')) {
            return 47;
        }
        if (str_starts_with($phone, '+358')) {
            return 358;
        }
        if (str_starts_with($phone, '+49')) {
            return 49;
        }
        if (str_starts_with($phone, '+44')) {
            return 44;
        }
        if (str_starts_with($phone, '+1')) {
            return 1;
        }
        return null;
    }
}
