<?php

namespace objects;

use classes\department_gate_config;
use classes\edge_gateway_manager;
use classes\bird;
use classes\db;
use classes\object_property;
use classes\slack;
use Exception;
use traits\db_object_t;

class department_gates_o extends db
{
    use db_object_t;
    public object_property $department; // The department id the gate relates to
    // The reason why there's two separate boolean properties for entrance and exit is to allow for departments where the same gate can be both an entrance and an exit (e.g. a turnstile that people both enter and exit through, where the gate criteria is based on a phone call triggered by passing through the turnstile, and the same criteria applies for both entering and exiting)
    public object_property $is_entrance; // Whether the gate is an entrance gate
    public object_property $is_exit; // Whether the gate is an exit gate
    public object_property $name; // The name of the gate
    public object_property $config; // A JSON with the gate config, including the type of gate and any relevant parameters (e.g. for a phone call triggered gate, the config would include the phone number to call and the call duration threshold)
    public object_property $created_at;
    public object_property $updated_at;
    public object_property $deleted_at;


    public function structure(): void
    {
        $this->setTable('department_gates');
    }

    /**
     * Add a gate
     * @param departments_o $department The department for the gate
     * @param bool $is_entrance Whether the gate is an entrance gate (true) or an exit gate (false)
     * @param bool $is_exit Whether the gate is an exit gate (true) or an entrance gate (false)
     * @param string $name The name of the gate
     * @param department_gate_config $config A JSON with the gate config, including the type of gate and any relevant parameters (e.g. for a phone call triggered gate, the config would include the phone number to call and the call duration threshold)
     * @return department_gates_o
     * @throws Exception If the object was not created successfully
     */
    public function add(departments_o $department, bool $is_entrance, bool $is_exit, string $name, department_gate_config $config): department_gates_o
    {
        global /** @var db $db */ $db;
        // Sanitize the input
        $department->requireSelected();
        $name = trim($db->escape_string($name));
        if (empty($name)) {
            throw new Exception('Gate name cannot be empty');
        }
        if (!$is_entrance && !$is_exit) {
            throw new Exception('Gate must be either entrance, exit or both');
        }
        $config->validate();
        // Add the object
        $tmp_id = self::add_object([
            'department' => (int)$department->id,
            'is_entrance' => (bool)$is_entrance,
            'is_exit' => (bool)$is_exit,
            'name' => $name,
            'config' => (array)$config->toArray(),
        ]);
        $this->id = $tmp_id;
        self::getObjectProperties();
        self::objectChanged();
        return $this;
    }



    public function getObjectProperties(): void
    {
        $this->department = new object_property($this->table, $this->id, 'department', 'int', false);
        $this->is_entrance = new object_property($this->table, $this->id, 'is_entrance', 'bool', false);
        $this->is_exit = new object_property($this->table, $this->id, 'is_exit', 'bool', false);
        $this->name = new object_property($this->table, $this->id, 'name', 'string', false);
        $this->config = new object_property($this->table, $this->id, 'config', 'json', false);
        $this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
        $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
        $this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
    }

    public function objectChanged(): void
    {
        //TODO: Add cache invalidation
    }

    /**
     * Get the gate as an array, including progress details if $include_progress is true
     * @return array The gate as an array
     * @throws Exception If the object is not valid or selected.
     */
    public function asArray(): array
    {
        $this->requireSelected();
        return [
            'id' => (int)$this->id,
            'department' => (int)$this->department->value(),
            'is_entrance' => (bool)$this->is_entrance->value(),
            'is_exit' => (bool)$this->is_exit->value(),
            'name' => (string)$this->name->value(),
            'config' => (array)$this->config->value(),
            'created_at' => (string)$this->created_at->value(),
            'updated_at' => (string)$this->updated_at->value(),
        ];
    }

    /**
     * @throws Exception
     * @return department_gates_o[]
     */
    public function getDepartmentGates(int $department_id): array
    {
        $gates = [];
        $department_gates = self::getFieldsWhere(
            [
                'department' => $department_id,
                'deleted_at' => null,
            ],
            [
                'id',
            ],
        );

        foreach ($department_gates as $department_gate) {
            $gates[] = (new department_gates_o())->select((int)$department_gate['id']);
        }

        return $gates;
    }

    public function getEntranceGate(int $department_id): ?department_gates_o
    {
        $entrance_gate_data = self::getFieldsWhere(
            [
                'department' => $department_id,
                'is_entrance' => true,
                'deleted_at' => null,
            ],
            [
                'id',
            ],
        );

        if (empty($entrance_gate_data)) {
            return null;
        }

        return (new department_gates_o())->select((int)$entrance_gate_data[0]['id']);
    }

    public function getExitGate(int $department_id): ?department_gates_o
    {
        $exit_gate_data = self::getFieldsWhere(
            [
                'department' => $department_id,
                'is_exit' => true,
                'deleted_at' => null,
            ],
            [
                'id',
            ],
        );

        if (empty($exit_gate_data)) {
            return null;
        }

        return (new department_gates_o())->select((int)$exit_gate_data[0]['id']);
    }

    public static function normalizePhoneCandidate(mixed $candidate): ?array
    {
        if (is_array($candidate)) {
            $phone = $candidate['phone_number'] ?? null;
            if ($phone !== null) {
                $country = self::extractCountryCodeFromPhoneNumber((string)$phone);
                return self::normalizePhone((string)$phone, $country);
            }
            return null;
        }

        if (is_string($candidate) && trim($candidate) !== '') {
            return self::normalizePhone($candidate);
        }

        return null;
    }

    public static 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 = self::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];
    }

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

    protected function matchesPhoneCallGateConfig(array $config): bool
    {
        return strtoupper(trim((string)($config['type'] ?? ''))) === 'PHONE_CALL';
    }

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

    protected function resolveSlackClient(): slack
    {
        return new slack();
    }

    protected function resolveEdgeGatewayManager(): edge_gateway_manager
    {
        return new edge_gateway_manager();
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function getPhoneCallDepartmentSummaries(): array
    {
        $summaries = [];
        $gateRows = self::getFieldsWhere(
            [
                'deleted_at' => null,
            ],
            [
                'id',
            ],
        );

        foreach ($gateRows as $gateRow) {
            $gate = (new department_gates_o())->select((int)$gateRow['id']);
            if (!$gate->exists()) {
                continue;
            }

            $config = (array)$gate->config->value();
            if (!$this->matchesPhoneCallGateConfig($config)) {
                continue;
            }

            $departmentId = (int)$gate->department->value();
            if ($departmentId <= 0) {
                continue;
            }

            if (!isset($summaries[$departmentId])) {
                $departmentRow = (new departments_o())->getDepartmentById($departmentId);
                $summaries[$departmentId] = [
                    'department_id' => $departmentId,
                    'department_name' => trim((string)($departmentRow['name'] ?? ('Afdeling ' . $departmentId))),
                    'order_priority' => (int)($departmentRow['order_priority'] ?? PHP_INT_MAX),
                    'has_entrance_gate' => false,
                    'has_exit_gate' => false,
                ];
            }

            if ((bool)$gate->is_entrance->value()) {
                $summaries[$departmentId]['has_entrance_gate'] = true;
            }
            if ((bool)$gate->is_exit->value()) {
                $summaries[$departmentId]['has_exit_gate'] = true;
            }
        }

        $summaries = array_values(array_filter($summaries, static function (array $summary): bool {
            return ($summary['has_entrance_gate'] ?? false) === true
                || ($summary['has_exit_gate'] ?? false) === true;
        }));

        usort($summaries, static function (array $left, array $right): int {
            $leftPriority = (int)($left['order_priority'] ?? PHP_INT_MAX);
            $rightPriority = (int)($right['order_priority'] ?? PHP_INT_MAX);
            if ($leftPriority !== $rightPriority) {
                return $leftPriority <=> $rightPriority;
            }

            return (int)($left['department_id'] ?? 0) <=> (int)($right['department_id'] ?? 0);
        });

        return $summaries;
    }

    public function getEntrancePhoneCallGate(int $department_id): ?department_gates_o
    {
        $gates = $this->getDepartmentGates($department_id);
        foreach ($gates as $gate) {
            if (!$gate->exists() || !(bool)$gate->is_entrance->value()) {
                continue;
            }

            if ($this->matchesPhoneCallGateConfig((array)$gate->config->value())) {
                return $gate;
            }
        }

        return null;
    }

    public function getExitPhoneCallGate(int $department_id): ?department_gates_o
    {
        $gates = $this->getDepartmentGates($department_id);
        foreach ($gates as $gate) {
            if (!$gate->exists() || !(bool)$gate->is_exit->value()) {
                continue;
            }

            if ($this->matchesPhoneCallGateConfig((array)$gate->config->value())) {
                return $gate;
            }
        }

        return null;
    }

    public function openGate(): void
    {
        $this->requireSelected();
        $config = (array)$this->config->value();

        if ($this->matchesPhoneCallGateConfig($config)) {
            $this->openPhoneCallGate($config);
            return;
        }

        if (strtoupper(trim((string)($config['type'] ?? ''))) === 'RELAY') {
            $this->openRelayBackedGate($config);
            return;
        }

        throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? ''));
    }

    /**
     * @param array<string,mixed> $config
     * @throws Exception
     */
    protected function openPhoneCallGate(array $config): void
    {
        if (!isset($config['phone_number'])) {
            throw new Exception('Phone number is required for PHONE_CALL gate type');
        }

        $normalized = self::normalizePhoneCandidate($config['phone_number']);
        if ($normalized === null) {
            throw new Exception('Invalid phone number for PHONE_CALL gate type');
        }
        [$countryCode, $phone] = $normalized;
        $ringTimeout = isset($config['call_duration_threshold']) ? (int)$config['call_duration_threshold'] : 30;
        $client = $this->resolveBirdClient();
        try {
            $client->callGatePreferringFlashCall($countryCode, $phone, $ringTimeout);
        } catch (\Throwable $e) {
            $this->resolveSlackClient()->send_message(
                'Failed to call gate for phone ' . $countryCode . ' ' . $phone . ': ' . $e->getMessage(),
                'Bird Voice Call Webhooks'
            );
            throw new Exception('Failed to open gate relay via phone call', 0, $e);
        }
    }

    /**
     * @param array<string,mixed> $config
     * @throws Exception
     */
    protected function openRelayBackedGate(array $config): void
    {
        $relayId = trim((string)($config['relay_id'] ?? ''));
        if ($relayId === '') {
            throw new Exception('relay_id is required for RELAY gate type');
        }

        $departmentId = (int)$this->department->value();
        if ($departmentId <= 0) {
            throw new Exception('Gate department is invalid');
        }

        $pulseSeconds = isset($config['pulse_seconds']) ? max(0, (int)$config['pulse_seconds']) : 1;
        $manager = $this->resolveEdgeGatewayManager();
        $manager->dispatchRelaySwitch($departmentId, $relayId, true, [
            'module' => 'department_gates',
            'reason' => 'Open relay-backed department gate',
            'gate_id' => (int)$this->id,
            'gate_name' => (string)$this->name->value(),
            'relay_id' => $relayId,
            'pulse_seconds' => $pulseSeconds,
        ]);

        if ($pulseSeconds > 0) {
            usleep($pulseSeconds * 1000000);
            $manager->dispatchRelaySwitch($departmentId, $relayId, false, [
                'module' => 'department_gates',
                'reason' => 'Close relay-backed department gate after pulse',
                'gate_id' => (int)$this->id,
                'gate_name' => (string)$this->name->value(),
                'relay_id' => $relayId,
                'pulse_seconds' => $pulseSeconds,
            ]);
        }
    }
}
