<?php
namespace EasyChat\Model;








class AppDevice extends Base {
    public const INSTALLATION_ID_PATTERN = '/^[A-Za-z0-9_-]{43}$/D';

    public static function tableSql($prefix, $with_foreign_key = true) {
        $foreign_key = $with_foreign_key
            ? ",
            CONSTRAINT `fk_app_device_manager`
                FOREIGN KEY (`manager_id`) REFERENCES `{$prefix}managers` (`id`) ON DELETE CASCADE"
            : "";

        return "CREATE TABLE IF NOT EXISTS `{$prefix}app_devices` (
            `device_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
            `public_id` CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
            `installation_hash` CHAR(64) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
            `manager_id` INT(11) NOT NULL,
            `display_name` VARCHAR(120) NOT NULL DEFAULT '',
            `device_label` VARCHAR(120) NOT NULL DEFAULT 'Android',
            `manufacturer` VARCHAR(80) NOT NULL DEFAULT '',
            `device_model` VARCHAR(80) NOT NULL DEFAULT '',
            `android_sdk` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
            `app_version_code` INT UNSIGNED NOT NULL DEFAULT 0,
            `app_version_name` VARCHAR(32) NOT NULL DEFAULT '',
            `identity_status` VARCHAR(16) NOT NULL DEFAULT 'verified',
            `status` VARCHAR(16) NOT NULL DEFAULT 'active',
            `first_seen_at` DATETIME NOT NULL,
            `last_seen_at` DATETIME NOT NULL,
            `last_paired_at` DATETIME NOT NULL,
            `revoked_at` DATETIME DEFAULT NULL,
            `revoke_reason` VARCHAR(64) NOT NULL DEFAULT '',
            `date_modified` DATETIME NOT NULL,
            UNIQUE KEY `uniq_app_device_public` (`public_id`),
            UNIQUE KEY `uniq_app_device_installation` (`installation_hash`),
            KEY `idx_app_device_manager_status` (`manager_id`, `status`, `last_seen_at`){$foreign_key}
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
    }

    public static function eventTableSql($prefix) {
        return "CREATE TABLE IF NOT EXISTS `{$prefix}app_device_events` (
            `event_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
            `device_id` INT UNSIGNED NOT NULL,
            `manager_id` INT(11) NOT NULL,
            `event_type` VARCHAR(40) NOT NULL,
            `reason_code` VARCHAR(64) NOT NULL DEFAULT '',
            `context_json` TEXT DEFAULT NULL,
            `created_at` DATETIME NOT NULL,
            KEY `idx_app_device_event_device` (`device_id`, `event_id`),
            KEY `idx_app_device_event_manager` (`manager_id`, `event_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
    }

    public function isValidInstallationId($installation_id) {
        return preg_match(self::INSTALLATION_ID_PATTERN, trim((string)$installation_id)) === 1;
    }

    public function installationHash($installation_id) {
        $installation_id = trim((string)$installation_id);
        if (!$this->isValidInstallationId($installation_id)) {
            return '';
        }
        return hash_hmac('sha256', $installation_id, $this->identitySecret());
    }

    public function getByInstallationId($installation_id) {
        $hash = $this->installationHash($installation_id);
        if ($hash === '') return [];
        $query = $this->db->query(
            "SELECT * FROM `{$this->prefix}app_devices`
             WHERE `installation_hash` = '" . $this->db->escape($hash) . "'
             LIMIT 1"
        );
        return $query->row ?: [];
    }

    public function getById($device_id) {
        $query = $this->db->query(
            "SELECT * FROM `{$this->prefix}app_devices`
             WHERE `device_id` = " . (int)$device_id . " LIMIT 1"
        );
        return $query->row ?: [];
    }

    public function getByPublicId($public_id) {
        $public_id = strtolower(trim((string)$public_id));
        if (!preg_match('/^[a-f0-9]{32}$/D', $public_id)) return [];
        $query = $this->db->query(
            "SELECT * FROM `{$this->prefix}app_devices`
             WHERE `public_id` = '" . $this->db->escape($public_id) . "'
             LIMIT 1"
        );
        return $query->row ?: [];
    }

    



    public function upsertVerified($installation_id, $manager_id, array $client, $allow_transfer = false) {
        $hash = $this->installationHash($installation_id);
        if ($hash === '') {
            throw new \InvalidArgumentException('invalid_installation_id');
        }
        $manager_id = (int)$manager_id;
        $existing = $this->db->query(
            "SELECT * FROM `{$this->prefix}app_devices`
             WHERE `installation_hash` = '" . $this->db->escape($hash) . "'
             LIMIT 1 FOR UPDATE"
        );
        $metadata = $this->normalizeMetadata($client);
        if ($existing->num_rows) {
            $device = $existing->row;
            if ((int)$device['manager_id'] !== $manager_id && !$allow_transfer) {
                throw new \RuntimeException('device_transfer_confirmation_required');
            }
            $this->db->query(
                "UPDATE `{$this->prefix}app_devices` SET
                    `manager_id` = {$manager_id},
                    `device_label` = '" . $this->db->escape($metadata['device_label']) . "',
                    `manufacturer` = '" . $this->db->escape($metadata['manufacturer']) . "',
                    `device_model` = '" . $this->db->escape($metadata['device_model']) . "',
                    `android_sdk` = " . (int)$metadata['android_sdk'] . ",
                    `app_version_code` = " . (int)$metadata['app_version_code'] . ",
                    `app_version_name` = '" . $this->db->escape($metadata['app_version_name']) . "',
                    `identity_status` = 'verified',
                    `status` = 'active',
                    `last_seen_at` = NOW(),
                    `last_paired_at` = NOW(),
                    `revoked_at` = NULL,
                    `revoke_reason` = '',
                    `date_modified` = NOW()
                 WHERE `device_id` = " . (int)$device['device_id']
            );
            return $this->getById((int)$device['device_id']);
        }

        $public_id = bin2hex(random_bytes(16));
        $this->db->query(
            "INSERT INTO `{$this->prefix}app_devices` SET
                `public_id` = '" . $this->db->escape($public_id) . "',
                `installation_hash` = '" . $this->db->escape($hash) . "',
                `manager_id` = {$manager_id},
                `display_name` = '',
                `device_label` = '" . $this->db->escape($metadata['device_label']) . "',
                `manufacturer` = '" . $this->db->escape($metadata['manufacturer']) . "',
                `device_model` = '" . $this->db->escape($metadata['device_model']) . "',
                `android_sdk` = " . (int)$metadata['android_sdk'] . ",
                `app_version_code` = " . (int)$metadata['app_version_code'] . ",
                `app_version_name` = '" . $this->db->escape($metadata['app_version_name']) . "',
                `identity_status` = 'verified',
                `status` = 'active',
                `first_seen_at` = NOW(),
                `last_seen_at` = NOW(),
                `last_paired_at` = NOW(),
                `date_modified` = NOW()
             ON DUPLICATE KEY UPDATE `device_id` = `device_id`"
        );
        $created = $this->getByInstallationId($installation_id);
        if (!$created) {
             
             
            throw new \RuntimeException('app_device_identity_conflict');
        }
        if ((int)$created['manager_id'] !== $manager_id) {
            if (!$allow_transfer) {
                throw new \RuntimeException('device_transfer_confirmation_required');
            }
             
             
            return $this->upsertVerified($installation_id, $manager_id, $client, true);
        }
        return $created;
    }

    public function touchFromSession($device_id, array $client = []) {
        if (!$client) {
            $this->db->query(
                "UPDATE `{$this->prefix}app_devices`
                 SET `last_seen_at` = NOW(), `date_modified` = NOW()
                 WHERE `device_id` = " . (int)$device_id . "
                   AND `status` = 'active'
                   AND `last_seen_at` < DATE_SUB(NOW(), INTERVAL 2 MINUTE)"
            );
            return;
        }
        $metadata = $this->normalizeMetadata($client);
        $this->db->query(
            "UPDATE `{$this->prefix}app_devices` SET
                `device_label` = '" . $this->db->escape($metadata['device_label']) . "',
                `manufacturer` = IF('" . $this->db->escape($metadata['manufacturer']) . "' = '', `manufacturer`, '" . $this->db->escape($metadata['manufacturer']) . "'),
                `device_model` = IF('" . $this->db->escape($metadata['device_model']) . "' = '', `device_model`, '" . $this->db->escape($metadata['device_model']) . "'),
                `android_sdk` = IF(" . (int)$metadata['android_sdk'] . " = 0, `android_sdk`, " . (int)$metadata['android_sdk'] . "),
                `app_version_code` = IF(" . (int)$metadata['app_version_code'] . " = 0, `app_version_code`, " . (int)$metadata['app_version_code'] . "),
                `app_version_name` = IF('" . $this->db->escape($metadata['app_version_name']) . "' = '', `app_version_name`, '" . $this->db->escape($metadata['app_version_name']) . "'),
                `last_seen_at` = NOW(),
                `date_modified` = NOW()
             WHERE `device_id` = " . (int)$device_id . " AND `status` = 'active'"
        );
    }

    public function listForManager($manager_id, $include_history = true) {
        $history_sql = $include_history ? '' : " AND d.`status` = 'active'";
        $query = $this->db->query(
            "SELECT d.*, sd.`device_id` AS `sms_device_id`, sd.`status` AS `sms_status`,
                    sd.`admin_requested`, sd.`operator_consent`, sd.`android_permission`,
                    sd.`inbound_status`, sd.`inbound_admin_requested`,
                    sd.`inbound_operator_consent`, sd.`inbound_android_permission`,
                    sd.`reliability_status`, sd.`background_restricted`,
                    sd.`battery_unrestricted`, sd.`notifications_allowed`, sd.`fcm_synced`,
                    sd.`last_seen_at` AS `sms_last_seen_at`
             FROM `{$this->prefix}app_devices` d
             LEFT JOIN `{$this->prefix}sms_devices` sd ON sd.`app_device_id` = d.`device_id`
             WHERE d.`manager_id` = " . (int)$manager_id . $history_sql . "
             ORDER BY (d.`status` = 'active') DESC, d.`last_seen_at` DESC, d.`device_id` DESC"
        );
        return $query->rows ?: [];
    }

    public function countActiveForManager($manager_id) {
        $query = $this->db->query(
            "SELECT COUNT(*) AS total FROM `{$this->prefix}app_devices`
             WHERE `manager_id` = " . (int)$manager_id . " AND `status` = 'active'"
        );
        return $query->num_rows ? (int)$query->row['total'] : 0;
    }

    public function rename($device_id, $manager_id, $display_name) {
        $display_name = $this->cleanText($display_name, 120);
        $this->db->query(
            "UPDATE `{$this->prefix}app_devices`
             SET `display_name` = '" . $this->db->escape($display_name) . "', `date_modified` = NOW()
             WHERE `device_id` = " . (int)$device_id . "
               AND `manager_id` = " . (int)$manager_id
        );
        return $this->db->countAffected() === 1;
    }

    public function revoke($device_id, $reason) {
        $reason = $this->cleanCode($reason, 64, 'admin_revoked');
        $this->db->query(
            "UPDATE `{$this->prefix}app_devices`
             SET `status` = 'revoked', `revoked_at` = NOW(),
                 `revoke_reason` = '" . $this->db->escape($reason) . "',
                 `date_modified` = NOW()
             WHERE `device_id` = " . (int)$device_id . " AND `status` = 'active'"
        );
    }

    public function recordEvent($device_id, $manager_id, $event_type, $reason_code = '', array $context = []) {
        $event_type = $this->cleanCode($event_type, 40, 'unknown');
        $reason_code = $this->cleanCode($reason_code, 64, '');
        $safe_context = [];
        foreach (['from_manager_id', 'to_manager_id', 'session_id', 'app_version_code'] as $key) {
            if (isset($context[$key])) $safe_context[$key] = (int)$context[$key];
        }
        $context_json = $safe_context
            ? "'" . $this->db->escape(json_encode($safe_context, JSON_UNESCAPED_SLASHES)) . "'"
            : 'NULL';
        $this->db->query(
            "INSERT INTO `{$this->prefix}app_device_events` SET
                `device_id` = " . (int)$device_id . ",
                `manager_id` = " . (int)$manager_id . ",
                `event_type` = '" . $this->db->escape($event_type) . "',
                `reason_code` = '" . $this->db->escape($reason_code) . "',
                `context_json` = {$context_json},
                `created_at` = NOW()"
        );
    }

    public function historyForDevice($device_id, $limit = 30) {
        $limit = max(1, min(100, (int)$limit));
        $query = $this->db->query(
            "SELECT `event_type`, `reason_code`, `context_json`, `created_at`
             FROM `{$this->prefix}app_device_events`
             WHERE `device_id` = " . (int)$device_id . "
             ORDER BY `event_id` DESC LIMIT {$limit}"
        );
        return $query->rows ?: [];
    }

    



    public function scopeCredential(array $device) {
        $public_id = (string)($device['public_id'] ?? '');
        $manager_id = (int)($device['manager_id'] ?? 0);
        if (!preg_match('/^[a-f0-9]{32}$/D', $public_id) || $manager_id <= 0) return '';
        return hash_hmac(
            'sha256',
            "easychat:device-sms-scope:v1\0" . $public_id . "\0" . $manager_id,
            $this->identitySecret()
        );
    }

    public function scopeCredentialById($device_id) {
        $device = $this->getById((int)$device_id);
        return $device ? $this->scopeCredential($device) : '';
    }

    private function identitySecret() {
        $settings = new Setting($this->registry);
        $secret = trim((string)$settings->get('app_device_identity_secret', ''));
        if (!preg_match('/^[a-f0-9]{64}$/D', $secret)) {
            $candidate = bin2hex(random_bytes(32));
             
             
            $this->db->query(
                "UPDATE `{$this->prefix}settings`
                 SET `value` = '" . $this->db->escape($candidate) . "'
                 WHERE `key` = 'app_device_identity_secret'
                   AND `value` = '" . $this->db->escape($secret) . "'"
            );
            $this->db->query(
                "INSERT INTO `{$this->prefix}settings` (`key`, `value`)
                 VALUES ('app_device_identity_secret', '" . $this->db->escape($candidate) . "')
                 ON DUPLICATE KEY UPDATE `value` = `value`"
            );
            $secret = trim((string)$settings->get('app_device_identity_secret', ''));
        }
        if (!preg_match('/^[a-f0-9]{64}$/D', $secret)) {
            throw new \RuntimeException('app_device_identity_secret_unavailable');
        }
        return $secret;
    }

    private function normalizeMetadata(array $client) {
        return [
            'device_label' => $this->cleanText($client['device_label'] ?? 'Android', 120) ?: 'Android',
            'manufacturer' => $this->cleanText($client['manufacturer'] ?? '', 80),
            'device_model' => $this->cleanText($client['model'] ?? $client['device_model'] ?? '', 80),
            'android_sdk' => max(0, min(999, (int)($client['android_sdk'] ?? 0))),
            'app_version_code' => max(0, min(2147483647, (int)($client['app_version_code'] ?? 0))),
            'app_version_name' => $this->cleanText($client['app_version_name'] ?? $client['app_version'] ?? '', 32),
        ];
    }

    private function cleanText($value, $limit) {
        $value = trim((string)$value);
        $value = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $value) ?: '';
        return mb_substr($value, 0, (int)$limit, 'UTF-8');
    }

    private function cleanCode($value, $limit, $fallback) {
        $value = strtolower(trim((string)$value));
        $value = preg_replace('/[^a-z0-9_.-]+/', '_', $value) ?: '';
        return mb_substr($value !== '' ? $value : $fallback, 0, (int)$limit, 'UTF-8');
    }
}
