<?php

namespace classes;


use bird\bird_c;
use Exception;
use objects\logs_o;

class bird
{
    public const TEST_OUTBOUND_NUMBER_RAW = '+45 42 33 11 28';
    public const TEST_OUTBOUND_NUMBER_E164 = '+4542331128';
    private const ALLOWED_HANGUP_CAUSES = ['rejected', 'busy', 'completed'];

    /**
     * Configuration of the Bird module
     * @var bird_c|object
     */
    public $config;

    public function __construct()
    {
        $this->config = new bird_c();
    }

    /**
     * Ensure module is enabled
     * @throws Exception
     */
    function requireModuleEnabled(): void
    {
        if (!$this->config->enabled->isTrue()) {
            throw new Exception('The bird module is not enabled');
        }
    }

    /**
     * Ensure API key is present
     * @throws Exception
     */
    function requireValidApiKey(): void
    {
        $k = $this->config->api_key->getVariableValue();
        if ($k === null || $k === '') {
            throw new Exception('Invalid API key defined in the config (bird_api_key_c)');
        }
    }

    /**
     * Ensure base URL is defined
     * @throws Exception
     */
    function requireValidServerURL(): void
    {
        $u = $this->config->server_url->getVariableValue();
        if ($u === null || $u === '') {
            throw new Exception('Invalid server URL defined in the config (bird_server_url_c)');
        }
    }

    /**
     * Send a request to Bird API
     * @param string $endpoint e.g. "/v1/devices"
     * @param array $data request body
     * @param string $method HTTP method (currently only POST supported)
     * @return object|array|null
     * @throws Exception
     */
    function sendRequest(string $endpoint, array $data = [], string $method = 'POST'): object|array|null
    {
        $this->requireModuleEnabled();
        $this->requireValidApiKey();
        $this->requireValidServerURL();
        return match (strtoupper($method)) {
            'POST' => $this->sendPostRequest($endpoint, $data),
            'GET'  => $this->sendGetRequest($endpoint, $data),
            'PATCH' => $this->sendPatchRequest($endpoint, $data),
            'DELETE' => $this->sendDeleteRequest($endpoint, $data),
            default => throw new Exception('Invalid request method'),
        };
    }

    /**
     * Send POST request
     * @param string $endpoint
     * @param array $data
     * @return array|object|null
     * @throws Exception
     */
    function sendPostRequest(string $endpoint, array $data): array|object|null
    {
        $this->requireModuleEnabled();
        $this->requireValidApiKey();
        $this->requireValidServerURL();

        $url = rtrim($this->config->server_url->getVariableValue(), '/') . $endpoint;
        $headers = [
            'Content-Type: application/json',
            $this->buildAuthorizationHeader(),
        ];
        $body = $this->encodeJsonBody($data);

        $this->logBirdAction(
            'BIRD_HTTP_REQUEST',
            'method=POST endpoint=' . $endpoint . ' payload_keys=' . implode(',', array_keys($data))
        );
        $result = $this->doHttpRequest('POST', $url, $headers, $body);
        $status = (int)($result['status_code'] ?? 0);
        $response = $result['body'] ?? '';
        $this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=POST endpoint=' . $endpoint . ' status=' . $status);
        if ($status >= 400) {
            $this->logBirdAction('BIRD_HTTP_ERROR', 'method=POST endpoint=' . $endpoint . ' status=' . $status, 0);
            throw new Exception($this->buildHttpErrorMessage($status, $response));
        }
        if ($response === '' || $response === false || $response === null) {
            return null;
        }
        $decoded = json_decode($response, true);
        return $decoded ?? $response;
    }

    /**
     * Low-level HTTP transport (curl). Tests can override this to stub network.
     * @param string $method
     * @param string $url
     * @param array $headers
     * @param string $body
     * @return array{status_code:int, body:string|false}
     * @throws Exception
     */
    protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 40);
        $upper = strtoupper($method);
        if ($upper === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        } else {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $upper);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if (curl_errno($ch)) {
            $err = curl_error($ch);
            curl_close($ch);
            $this->logBirdAction('BIRD_HTTP_CURL_ERROR', 'method=' . $upper . ' url=' . $url . ' error=' . $err, 0);
            throw new Exception('cURL error: ' . $err);
        }
        curl_close($ch);
        return [
            'status_code' => (int)$code,
            'body' => $resp,
        ];
    }

    /**
     * Send GET request
     * @param string $endpoint
     * @param array $query
     * @return array|object|null
     * @throws Exception
     */
    function sendGetRequest(string $endpoint, array $query = []): array|object|null
    {
        $this->requireModuleEnabled();
        $this->requireValidApiKey();
        $this->requireValidServerURL();

        $base = rtrim($this->config->server_url->getVariableValue(), '/');
        $url = $base . $endpoint;
        if (!empty($query)) {
            $qs = http_build_query($query);
            $url .= (str_contains($url, '?') ? '&' : '?') . $qs;
        }
        $headers = [
            'Content-Type: application/json',
            $this->buildAuthorizationHeader(),
        ];
        $this->logBirdAction(
            'BIRD_HTTP_REQUEST',
            'method=GET endpoint=' . $endpoint . ' query_keys=' . implode(',', array_keys($query))
        );
        $result = $this->doHttpRequest('GET', $url, $headers);
        $status = (int)($result['status_code'] ?? 0);
        $response = $result['body'] ?? '';
        $this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=GET endpoint=' . $endpoint . ' status=' . $status);
        if ($status >= 400) {
            $this->logBirdAction('BIRD_HTTP_ERROR', 'method=GET endpoint=' . $endpoint . ' status=' . $status, 0);
            throw new Exception($this->buildHttpErrorMessage($status, $response));
        }
        if ($response === '' || $response === false || $response === null) {
            return null;
        }
        $decoded = json_decode($response, true);
        return $decoded ?? $response;
    }

    function sendPatchRequest(string $endpoint, array $data = []): array|object|null
    {
        $this->requireModuleEnabled();
        $this->requireValidApiKey();
        $this->requireValidServerURL();

        $url = rtrim($this->config->server_url->getVariableValue(), '/') . $endpoint;
        $headers = [
            'Content-Type: application/json',
            $this->buildAuthorizationHeader(),
        ];
        $body = $this->encodeJsonBody($data);

        $this->logBirdAction(
            'BIRD_HTTP_REQUEST',
            'method=PATCH endpoint=' . $endpoint . ' payload_keys=' . implode(',', array_keys($data))
        );
        $result = $this->doHttpRequest('PATCH', $url, $headers, $body);
        $status = (int)($result['status_code'] ?? 0);
        $response = $result['body'] ?? '';
        $this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=PATCH endpoint=' . $endpoint . ' status=' . $status);
        if ($status >= 400) {
            $this->logBirdAction('BIRD_HTTP_ERROR', 'method=PATCH endpoint=' . $endpoint . ' status=' . $status, 0);
            throw new Exception($this->buildHttpErrorMessage($status, $response));
        }
        if ($response === '' || $response === false || $response === null) {
            return null;
        }
        $decoded = json_decode($response, true);
        return $decoded ?? $response;
    }

    function sendDeleteRequest(string $endpoint, array $query = []): array|object|null
    {
        $this->requireModuleEnabled();
        $this->requireValidApiKey();
        $this->requireValidServerURL();

        $base = rtrim($this->config->server_url->getVariableValue(), '/');
        $url = $base . $endpoint;
        if (!empty($query)) {
            $qs = http_build_query($query);
            $url .= (str_contains($url, '?') ? '&' : '?') . $qs;
        }
        $headers = [
            'Content-Type: application/json',
            $this->buildAuthorizationHeader(),
        ];
        $this->logBirdAction(
            'BIRD_HTTP_REQUEST',
            'method=DELETE endpoint=' . $endpoint . ' query_keys=' . implode(',', array_keys($query))
        );
        $result = $this->doHttpRequest('DELETE', $url, $headers);
        $status = (int)($result['status_code'] ?? 0);
        $response = $result['body'] ?? '';
        $this->logBirdAction('BIRD_HTTP_RESPONSE', 'method=DELETE endpoint=' . $endpoint . ' status=' . $status);
        if ($status >= 400) {
            $this->logBirdAction('BIRD_HTTP_ERROR', 'method=DELETE endpoint=' . $endpoint . ' status=' . $status, 0);
            throw new Exception($this->buildHttpErrorMessage($status, $response));
        }
        if ($response === '' || $response === false || $response === null) {
            return null;
        }
        $decoded = json_decode($response, true);
        return $decoded ?? $response;
    }

    public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null
    {
        $base = $this->voiceBase($workspaceId, $channelId);
        $this->logBirdAction('BIRD_VOICE_CALL_CREATE', 'workspace=' . $workspaceId . ' channel=' . $channelId);
        return $this->sendPostRequest($base, $payload);
    }

    public function listVoiceCalls(string $workspaceId, string $channelId, array $query = []): array|object|null
    {
        $base = $this->voiceBase($workspaceId, $channelId);
        $this->logBirdAction('BIRD_VOICE_CALL_LIST', 'workspace=' . $workspaceId . ' channel=' . $channelId);
        return $this->sendGetRequest($base, $query);
    }

    public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null
    {
        $base = $this->voiceBase($workspaceId, $channelId);
        $this->logBirdAction('BIRD_VOICE_CALL_GET', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
        return $this->sendGetRequest($base . '/' . rawurlencode($callId));
    }

    public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
    {
        $base = $this->voiceBase($workspaceId, $channelId);
        $this->logBirdAction('BIRD_VOICE_CALL_HANGUP', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
        return $this->sendPostRequest($base . '/' . rawurlencode($callId) . '/hangup', $payload);
    }

    public function sayMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
    {
        $this->logBirdAction('BIRD_VOICE_CALL_SAY', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
        /**
         * Example payload:
         * https://api.bird.com/workspaces/{workspaceId}/channels/{channelId}/calls/{callId}/say
         * {
         * "text": "text",
         * "locale": "en-US",
         * "voice": "text",
         * "loop": 1,
         * "timeout": 1
         * }
         */
        if (!isset($payload['text']) || !is_string($payload['text']) || trim($payload['text']) === '') {
            throw new Exception('The "text" field is required in the payload and must be a non-empty string');
        }
        $tmp = [
            // Default values
            'locale' => 'da-DK',
            'voice' => 'text',
            'loop' => 1,
            'timeout' => 1,
            ...$payload,
        ];
        return $this->sendPostRequest($this->sayBase($workspaceId, $channelId, $callId), $tmp);
    }

    public function gatherMessage(string $workspaceId, string $channelId, string $callId, array $payload): array|object|null
    {
        $this->logBirdAction('BIRD_VOICE_CALL_GATHER', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' call=' . $callId);
        /**
         * Example payload:
         * https://api.bird.com/workspaces/{workspaceId}/channels/{channelId}/calls/{callId}/gather
         * {
         * "maxNumKeys": 1,
         * "endKey": "0",
         * "timeout": 1,
         * "retries": 1,
         * "input": "dtmf",
         * "speechLocale": "en-US",
         * "playback": {
         * "media": [ "text" ],
         * "loop": 1,
         * "timeout": 1,
         * "pauseMilliseconds": 1
         * },
         * "say": {
         * "text": "text",
         * "locale": "en-US",
         * "voice": "text",
         * "loop": 1,
         * "timeout": 1
         * }
         * }
         */
        /**
         * Example response:
         * {
         * "id": "123e4567-e89b-12d3-a456-426614174000",
         * "callId": "123e4567-e89b-12d3-a456-426614174000",
         * "callFlowId": "123e4567-e89b-12d3-a456-426614174000",
         * "status": "text",
         * "command": "text",
         * "conditions": [
         * {
         * "variable": "keys",
         * "operator": "eq",
         * "value": "text"
         * }
         * ]
         * }
         */
        $tmp = [
            // Default values
            'input' => 'dtmf',
            ...$payload,
        ];
        return $this->sendPostRequest($this->gatherBase($workspaceId, $channelId, $callId), $tmp);
    }

    public function listNumbers(string $workspaceId, array $query = []): array|object|null
    {
        $this->logBirdAction('BIRD_NUMBERS_LIST', 'workspace=' . $workspaceId . ' query_keys=' . implode(',', array_keys($query)));
        return $this->sendGetRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers', $query);
    }

    public function getNumber(string $workspaceId, string $numberId): array|object|null
    {
        $this->logBirdAction('BIRD_NUMBER_GET', 'workspace=' . $workspaceId . ' number=' . $numberId);
        return $this->sendGetRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers/' . rawurlencode($numberId));
    }

    public function deleteNumber(string $workspaceId, string $numberId): array|object|null
    {
        $this->logBirdAction('BIRD_NUMBER_DELETE', 'workspace=' . $workspaceId . ' number=' . $numberId);
        return $this->sendDeleteRequest('/workspaces/' . rawurlencode($workspaceId) . '/numbers/' . rawurlencode($numberId));
    }

    public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array
    {
        return $this->executeCallAndHangupWhenAccepted($workspaceId, $channelId, $options, 'BIRD_TEST_OUTBOUND_CALL');
    }

    /**
     * Internal method to place a call, wait for it to be accepted/ongoing, and then hang up.
     * Used by both test outbound calls and actual gate calls.
     */
    private function executeCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options, string $logPrefix): array
    {
        $pollIntervalSeconds = max(1, (int)($options['pollIntervalSeconds'] ?? 2));
        $maxPollSeconds = max(5, (int)($options['maxPollSeconds'] ?? 30));
        $maxAttempts = (int)max(1, floor($maxPollSeconds / $pollIntervalSeconds));

        $payload = $options;
        // Keep polling/hangup controls out of create-call payload to avoid Bird validation errors.
        unset($payload['pollIntervalSeconds'], $payload['maxPollSeconds'], $payload['hangupCause']);
        $targetNumber = (string)($payload['to'] ?? self::TEST_OUTBOUND_NUMBER_E164);
        if ($targetNumber === '') {
            $targetNumber = self::TEST_OUTBOUND_NUMBER_E164;
        }
        $payload['to'] = $targetNumber;

        // Ensure Bird terminates an unanswered call after we've stopped polling for it.
        if (!isset($payload['timeout'])) {
            $payload['timeout'] = $maxPollSeconds;
        }

        $this->logBirdAction(
            $logPrefix . '_START',
            'workspace=' . $workspaceId . ' channel=' . $channelId . ' to=' . $targetNumber
        );

        $createResponse = $this->createVoiceCall($workspaceId, $channelId, $payload);
        $callId = $this->extractId($createResponse);
        if ($callId === null) {
            $this->logBirdAction($logPrefix . '_NO_ID', 'Call accepted but no call ID returned', 0);
            return [
                'to' => $targetNumber,
                'to_e164' => $targetNumber,
                'created_call' => $createResponse,
                'hangup_sent' => false,
                'message' => 'Call was created, but no call ID was returned, so hangup could not be sent.',
            ];
        }

        $lastCall = null;
        $acceptedStates = ['accepted', 'ongoing'];
        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
            $current = $this->getVoiceCall($workspaceId, $channelId, $callId);
            $lastCall = $current;
            $status = $this->extractStatus($current);

            $this->logBirdAction(
                $logPrefix . '_POLL',
                'call=' . $callId . ' attempt=' . $attempt . '/' . $maxAttempts . ' status=' . ($status ?? 'unknown')
            );

            if ($status !== null && in_array(strtolower($status), $acceptedStates, true)) {
                $hangupPayload = [];
                if (isset($options['hangupCause']) && is_string($options['hangupCause']) && $options['hangupCause'] !== '') {
                    $normalizedCause = strtolower(trim($options['hangupCause']));
                    if (in_array($normalizedCause, self::ALLOWED_HANGUP_CAUSES, true)) {
                        $hangupPayload['cause'] = $normalizedCause;
                    }
                }
                $hangupResponse = $this->hangupVoiceCall($workspaceId, $channelId, $callId, $hangupPayload);
                $this->logBirdAction($logPrefix . '_HANGUP_SENT', 'call=' . $callId . ' status=' . $status);
                return [
                    'to' => $targetNumber,
                    'to_e164' => $targetNumber,
                    'call_id' => $callId,
                    'final_status' => $status,
                    'hangup_sent' => true,
                    'created_call' => $createResponse,
                    'last_call_snapshot' => $lastCall,
                    'hangup_response' => $hangupResponse,
                ];
            }

            if ($attempt < $maxAttempts) {
                sleep($pollIntervalSeconds);
            }
        }

        $lastStatus = $this->extractStatus($lastCall);
        $this->logBirdAction(
            $logPrefix . '_TIMEOUT',
            'call=' . $callId . ' last_status=' . ($lastStatus ?? 'unknown'),
            0
        );
        return [
            'to' => $targetNumber,
            'to_e164' => $targetNumber,
            'call_id' => $callId,
            'final_status' => $lastStatus,
            'hangup_sent' => false,
            'timed_out_waiting_for_accepted' => true,
            'created_call' => $createResponse,
            'last_call_snapshot' => $lastCall,
        ];
    }

    private function voiceBase(string $workspaceId, string $channelId): string
    {
        return '/workspaces/' . rawurlencode($workspaceId) . '/channels/' . rawurlencode($channelId) . '/calls';
    }

    private function sayBase(string $workspaceId, string $channelId, string $callId): string
    {
        // https://api.bird.com/workspaces/{workspaceId}/channels/{channelId}/calls/{callId}/say
        return '/workspaces/' . rawurlencode($workspaceId) . '/channels/' . rawurlencode($channelId) . '/calls/' . rawurlencode($callId) . '/say';
    }

    private function gatherBase(string $workspaceId, string $channelId, string $callId): string
    {
        // https://api.bird.com/workspaces/{workspaceId}/channels/{channelId}/calls/{callId}/gather
        return '/workspaces/' . rawurlencode($workspaceId) . '/channels/' . rawurlencode($channelId) . '/calls/' . rawurlencode($callId) . '/gather';
    }

    protected function extractId(array|object|string|null $response): ?string
    {
        if (is_object($response) && isset($response->id) && is_string($response->id) && $response->id !== '') {
            return $response->id;
        }
        if (is_array($response) && isset($response['id']) && is_string($response['id']) && $response['id'] !== '') {
            return $response['id'];
        }
        return null;
    }

    protected function extractStatus(array|object|string|null $response): ?string
    {
        if (is_object($response) && isset($response->status) && is_string($response->status) && $response->status !== '') {
            return $response->status;
        }
        if (is_array($response) && isset($response['status']) && is_string($response['status']) && $response['status'] !== '') {
            return $response['status'];
        }
        return null;
    }

    private function buildHttpErrorMessage(int $status, string|false|null $response): string
    {
        $base = 'Bird API request failed with status ' . $status;
        if (!is_string($response) || trim($response) === '') {
            return $base;
        }

        $decoded = json_decode($response, true);
        if (is_array($decoded)) {
            $details = [];
            foreach (['message', 'error', 'description'] as $key) {
                if (isset($decoded[$key]) && is_string($decoded[$key]) && trim($decoded[$key]) !== '') {
                    $details[] = trim($decoded[$key]);
                }
            }
            if (isset($decoded['errors'])) {
                if (is_string($decoded['errors']) && trim($decoded['errors']) !== '') {
                    $details[] = trim($decoded['errors']);
                } elseif (is_array($decoded['errors'])) {
                    foreach ($decoded['errors'] as $error) {
                        if (is_string($error) && trim($error) !== '') {
                            $details[] = trim($error);
                        } elseif (is_array($error) && isset($error['message']) && is_string($error['message']) && trim($error['message']) !== '') {
                            $details[] = trim($error['message']);
                        }
                    }
                }
            }
            $details = array_values(array_unique(array_filter($details)));
            if (!empty($details)) {
                return $base . ': ' . implode(' | ', $details);
            }
        }

        $snippet = preg_replace('/\s+/', ' ', trim($response));
        if (!is_string($snippet) || $snippet === '') {
            return $base;
        }
        if (strlen($snippet) > 220) {
            $snippet = substr($snippet, 0, 220) . '...';
        }
        return $base . ': ' . $snippet;
    }

    private function buildAuthorizationHeader(): string
    {
        $apiKey = trim((string)$this->config->api_key->getVariableValue());
        if ($apiKey === '') {
            return 'Authorization: AccessKey';
        }
        if (preg_match('/^(Bearer|AccessKey)\s+/i', $apiKey) === 1) {
            return 'Authorization: ' . $apiKey;
        }
        return 'Authorization: AccessKey ' . $apiKey;
    }

    private function encodeJsonBody(array $data): string
    {
        // Bird control endpoints expect an object payload; use {} for empty body.
        if ($data === []) {
            return '{}';
        }
        return json_encode($data);
    }

    private function logBirdAction(string $action, string $message, int $type = 1): void
    {
        try {
            $userId = 0;
            if (isset($_SERVER['HTTP_AUTHORIZATION']) || isset($_SERVER['Authorization'])) {
                try {
                    $auth = new authentication();
                    $user = $auth->get_user();
                    if ($user && isset($user->id)) {
                        $userId = (int)$user->id;
                    }
                } catch (\Throwable) {
                    $userId = 0;
                }
            }
            (new logs_o())->add('bird', 'global', $type, $userId, $action, $message);
        } catch (\Throwable) {
            // Logging must never break Bird request flow.
        }
    }

    public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout)
    {
        $ws = $this->config->workplaceId->getVariableValue();
        $ch = $this->config->channelId->getVariableValue();

        $options = [
            'to' => '+' . $countryCode . $phone,
            'maxPollSeconds' => (int)$timeout,
        ];

        $result = $this->executeCallAndHangupWhenAccepted($ws, $ch, $options, 'BIRD_GATE_CALL');

        if (!($result['hangup_sent'] ?? false)) {
            $msg = $result['message'] ?? 'Failed to call gate and hangup when accepted';
            if ($result['timed_out_waiting_for_accepted'] ?? false) {
                $msg = 'Timed out waiting for gate to accept call';
            }
            throw new Exception("Failed to call gate (+{$countryCode}{$phone}): " . $msg);
        }
    }
}


