<?php
namespace EasyChat\Model;





class SmsJobEvent extends Base {
    private const MAX_APP_EVENTS_PER_JOB = 100;

    private const APP_STAGES = [
        'push_received',
        'domain_mismatch',
        'job_fetch_failed',
        'permission_missing',
        'device_unavailable',
        'job_not_available',
        'license_blocked',
        'license_check_failed',
        'sms_manager_started',
        'modem_accepted',
        'modem_failed',
        'send_outcome_unknown',
        'deadline_expired',
        'completion_report_failed',
        'carrier_delivered',
        'carrier_delivery_failed',
    ];

    




    private const PRE_CLAIM_SESSION_STAGES = [
        'push_received',
        'domain_mismatch',
        'job_fetch_failed',
        'permission_missing',
        'device_unavailable',
        'job_not_available',
        'license_blocked',
        'license_check_failed',
    ];

    private const CARRIER_STAGES = [
        'carrier_delivered',
        'carrier_delivery_failed',
    ];

    private const SERVER_STAGES = [
        'job_created',
        'push_accepted',
        'push_failed',
        'completion_reported',
        'job_reserved',
        'device_reassigned',
    ];

    private const CONTEXT_KEYS = [
        'app_version',
        'android_sdk',
        'manufacturer',
        'model',
        'push_domain',
        'current_domain',
        'send_permission',
        'receive_permission',
        'send_available',
        'default_subscription_id',
        'subscription_id',
        'active_subscription_count',
        'attempt',
        'part_index',
        'part_count',
        'android_result_code',
        'worker_run_attempt_count',
        'elapsed_ms',
        'push_sent',
        'previous_device_id',
        'device_id',
    ];

     
     
    private const STAGE_ORDER = [
        'job_created' => 10,
        'push_accepted' => 20,
        'push_failed' => 21,
        'push_received' => 30,
        'domain_mismatch' => 31,
        'job_fetch_failed' => 35,
        'job_not_available' => 36,
        'job_reserved' => 40,
         
         
        'device_reassigned' => 19,
        'permission_missing' => 50,
        'device_unavailable' => 50,
        'license_blocked' => 50,
        'license_check_failed' => 51,
        'sms_manager_started' => 60,
        'modem_accepted' => 70,
        'modem_failed' => 71,
        'send_outcome_unknown' => 72,
        'deadline_expired' => 73,
        'completion_report_failed' => 80,
        'completion_reported' => 90,
        'carrier_delivered' => 100,
        'carrier_delivery_failed' => 101,
    ];

    public function recordServer(int $job_id, string $stage, string $code = '', array $context = [], string $event_id = ''): array {
        if (!in_array($stage, self::SERVER_STAGES, true)) {
            return [];
        }
        $job = $this->jobById($job_id);
        if (!$job) {
            return [];
        }
        return $this->record($job, $stage, $code, $context, $event_id, 0);
    }

    public function recordFromApp(int $app_session_id, int $job_id, string $stage, string $code, array $context, string $event_id, int $client_time_ms): array {
        if (!in_array($stage, self::APP_STAGES, true)) {
            return [];
        }
        $job = $this->jobForSession($app_session_id, $job_id);
        if (!$this->sessionStageAuthorized($job, $stage)) {
            return [];
        }
        return $this->recordAuthorizedAppEvent($job, $stage, $code, $context, $event_id, $client_time_ms);
    }

     
    public function recordFromReceipt(int $job_id, string $receipt_token, string $stage, string $code, array $context, string $event_id, int $client_time_ms): array {
        if (!in_array($stage, self::APP_STAGES, true)) return [];
        $job = (new SmsJob($this->registry))->getForReceipt($job_id, $receipt_token);
        if (!$this->hasSuccessfulClaim($job)) return [];
        return $this->recordAuthorizedAppEvent($job, $stage, $code, $context, $event_id, $client_time_ms);
    }

    





    private function sessionStageAuthorized(array $job, string $stage): bool {
        if (!$job) return false;
        if (in_array($stage, self::PRE_CLAIM_SESSION_STAGES, true)) return true;
        if (!$this->hasSuccessfulClaim($job)) return false;

        if (in_array($stage, self::CARRIER_STAGES, true)) {
             
             
             
             
            $app_version_code = (int)($job['session_app_version_code'] ?? 0);
            return $app_version_code > 0
                && $app_version_code < SmsJob::RESERVATION_KEY_REQUIRED_VERSION_CODE;
        }
        return true;
    }

    private function hasSuccessfulClaim(array $job): bool {
        if (!$job || (int)($job['attempt_count'] ?? 0) < 1) return false;
        $reserved_at = trim((string)($job['reserved_at'] ?? ''));
        if ($reserved_at === '' || $reserved_at === '0000-00-00 00:00:00') return false;
        return (string)($job['status'] ?? 'pending') !== 'pending';
    }

    private function recordAuthorizedAppEvent(array $job, string $stage, string $code, array $context, string $event_id, int $client_time_ms): array {
        if (!$job) return [];
        $carrier_evidence = $this->carrierEvidence($stage, $context, (int)($job['attempt_count'] ?? 0));
        if (is_array($carrier_evidence) && !$carrier_evidence) return [];
        $job_id = (int)$job['job_id'];
        $normalized_event_id = $this->normalizeEventId($event_id, $stage);
        $lock_name = $this->acquireAppEventLock($job_id);
        if ($lock_name === '') {
             
             
             
            return [
                'job_id' => $job_id,
                'stage' => $stage,
                'accepted' => 0,
                'retryable' => 1,
                'error' => 'sms_job_event_busy',
            ];
        }

        try {
            $existing_where = "`client_event_id` = '" . $this->db->escape($normalized_event_id) . "'";
            if (is_array($carrier_evidence)) {
                 
                 
                $existing_where .= " OR (`carrier_attempt` = " . (int)$carrier_evidence['attempt']
                    . " AND `carrier_part_index` = " . (int)$carrier_evidence['part_index'] . ")";
            }
            $existing_query = $this->db->query(
                "SELECT * FROM `{$this->prefix}sms_job_events`
                 WHERE `job_id` = {$job_id}
                   AND ({$existing_where})
                 LIMIT 1"
            );
            if ($existing_query->num_rows) {
                $stored = $existing_query->row;
                $stored_context = json_decode((string)($stored['context_json'] ?? ''), true);
                $this->updateJobState(
                    $job,
                    (string)$stored['stage'],
                    (string)$stored['code'],
                    is_array($stored_context) ? $stored_context : []
                );
                return ['job_id' => $job_id, 'stage' => (string)$stored['stage'], 'accepted' => 0, 'reconciled' => 1];
            }

             
             
            $app_stages = array_map(function ($app_stage) {
                return "'" . $this->db->escape($app_stage) . "'";
            }, self::APP_STAGES);
            $count_query = $this->db->query(
                "SELECT COUNT(*) AS total FROM `{$this->prefix}sms_job_events`
                 WHERE `job_id` = {$job_id}
                   AND `stage` IN (" . implode(',', $app_stages) . ")"
            );
            if ((int)($count_query->row['total'] ?? 0) >= self::MAX_APP_EVENTS_PER_JOB) {
                 
                 
                return ['job_id' => $job_id, 'stage' => $stage, 'accepted' => 0, 'limited' => 1];
            }
            return $this->record($job, $stage, $code, $context, $normalized_event_id, $client_time_ms);
        } finally {
            $this->releaseAppEventLock($lock_name);
        }
    }

    




    private function acquireAppEventLock(int $job_id): string {
        try {
            $database_query = $this->db->query('SELECT DATABASE() AS `database_name`');
            $database_name = (string)($database_query->row['database_name'] ?? '');
            if ($database_name === '') return '';
            $scope = $database_name . "\0" . $this->prefix . "\0" . $job_id;
            $lock_name = 'easychat_sms_evt_' . substr(hash('sha256', $scope), 0, 32);
            $lock_query = $this->db->query(
                "SELECT GET_LOCK('" . $this->db->escape($lock_name) . "', 3) AS `locked`"
            );
            return (int)($lock_query->row['locked'] ?? 0) === 1 ? $lock_name : '';
        } catch (\Throwable $ignored) {
            return '';
        }
    }

    private function releaseAppEventLock(string $lock_name): void {
        if (!preg_match('/^easychat_sms_evt_[a-f0-9]{32}$/D', $lock_name)) return;
        try {
            $this->db->query("SELECT RELEASE_LOCK('" . $this->db->escape($lock_name) . "')");
        } catch (\Throwable $ignored) {
        }
    }

    public function recentForDevice(int $device_id, int $job_limit = 8): array {
        $job_limit = max(1, min(20, $job_limit));
        $jobs_query = $this->db->query(
            "SELECT `job_id`, `chat_id`, `message_id`, `status`, `attempt_count`,
                    `last_stage`, `last_stage_code`, `last_stage_at`, `delivery_status`,
                    `parts_total`, `parts_delivered`, `parts_delivery_failed`, `reserved_at`,
                    `completed_at`, `delivered_at`, `date_added`, `date_modified`
             FROM `{$this->prefix}sms_jobs`
             WHERE `device_id` = " . (int)$device_id . "
             ORDER BY `job_id` DESC
             LIMIT " . $job_limit
        );
        $jobs = $jobs_query->rows ?: [];
        if (!$jobs) {
            return [];
        }

        $ids = array_map(static function ($job) { return (int)$job['job_id']; }, $jobs);
        $events_query = $this->db->query(
            "SELECT `event_id`, `job_id`, `stage`, `level`, `code`, `context_json`, `client_time_ms`, `date_added`
             FROM `{$this->prefix}sms_job_events`
             WHERE `job_id` IN (" . implode(',', $ids) . ")
             ORDER BY `event_id` ASC"
        );
        $events_by_job = [];
        foreach ($events_query->rows ?: [] as $event) {
            $event['context'] = json_decode((string)($event['context_json'] ?? ''), true) ?: [];
            unset($event['context_json']);
            $events_by_job[(int)$event['job_id']][] = $event;
        }
        foreach ($jobs as &$job) {
            $job['events'] = $events_by_job[(int)$job['job_id']] ?? [];
        }
        unset($job);
        return $jobs;
    }

    private function record(array $job, string $stage, string $code, array $context, string $event_id, int $client_time_ms): array {
        $job_id = (int)$job['job_id'];
        $device_id = (int)$job['device_id'];
        $event_id = $this->normalizeEventId($event_id, $stage);
        $code = $this->normalizeCode($code);
        $context = $this->sanitizeContext($context);
        $carrier_evidence = $this->carrierEvidence($stage, $context, (int)($job['attempt_count'] ?? 0));
        if (is_array($carrier_evidence) && !$carrier_evidence) return [];
        if (is_array($carrier_evidence)) {
             
             
            $context['attempt'] = (int)$carrier_evidence['attempt'];
        }
        $level = $this->levelForStage($stage);
        $context_json = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($context_json)) {
            $context_json = '{}';
        }

        $carrier_columns = '';
        if (is_array($carrier_evidence)) {
            $carrier_columns = "`carrier_attempt` = " . (int)$carrier_evidence['attempt']
                . ",\n                 `carrier_part_index` = " . (int)$carrier_evidence['part_index'] . ",\n                 ";
        }
        $this->db->query(
            "INSERT IGNORE INTO `{$this->prefix}sms_job_events`
             SET `job_id` = {$job_id},
                 `device_id` = {$device_id},
                 `client_event_id` = '" . $this->db->escape($event_id) . "',
                 {$carrier_columns}`stage` = '" . $this->db->escape($stage) . "',
                 `level` = '" . $this->db->escape($level) . "',
                 `code` = '" . $this->db->escape($code) . "',
                 `context_json` = '" . $this->db->escape($context_json) . "',
                 `client_time_ms` = " . max(0, $client_time_ms) . ",
                 `date_added` = NOW()"
        );
        $inserted = (int)$this->db->countAffected() > 0;
        if ($inserted) {
            $this->updateJobState($job, $stage, $code, $context);
            $event_db_id = (int)$this->db->getLastId();
            if ($event_db_id > 0 && $event_db_id % 100 === 0) {
                $this->db->query("DELETE FROM `{$this->prefix}sms_job_events` WHERE `date_added` < DATE_SUB(NOW(), INTERVAL 30 DAY) LIMIT 500");
            }
        } else {
             
             
             
            $conflict_where = "`client_event_id` = '" . $this->db->escape($event_id) . "'";
            if (is_array($carrier_evidence)) {
                $conflict_where .= " OR (`carrier_attempt` = " . (int)$carrier_evidence['attempt']
                    . " AND `carrier_part_index` = " . (int)$carrier_evidence['part_index'] . ")";
            }
            $stored_query = $this->db->query(
                "SELECT `stage`, `code`, `context_json`
                 FROM `{$this->prefix}sms_job_events`
                 WHERE `job_id` = {$job_id}
                   AND ({$conflict_where})
                 LIMIT 1"
            );
            if ($stored_query && $stored_query->num_rows) {
                $stored = $stored_query->row;
                $stored_context = json_decode((string)($stored['context_json'] ?? ''), true);
                $this->updateJobState($job, (string)$stored['stage'], (string)$stored['code'], is_array($stored_context) ? $stored_context : []);
                return [
                    'job_id' => $job_id,
                    'stage' => (string)$stored['stage'],
                    'accepted' => 0,
                    'reconciled' => 1,
                ];
            }
        }

        return [
            'job_id' => $job_id,
            'stage' => $stage,
            'accepted' => $inserted ? 1 : 0,
        ];
    }

    private function updateJobState(array $job, string $stage, string $code, array $context): void {
        $job_id = (int)$job['job_id'];
        $stage_rank = (int)(self::STAGE_ORDER[$stage] ?? 0);
        $rank_cases = [];
        foreach (self::STAGE_ORDER as $ordered_stage => $rank) {
            $rank_cases[] = "WHEN '" . $this->db->escape($ordered_stage) . "' THEN " . (int)$rank;
        }
        $current_rank_sql = '(CASE `last_stage` ' . implode(' ', $rank_cases) . ' ELSE 0 END)';
        $stage_can_advance_sql = "{$current_rank_sql} < {$stage_rank}";
        if ($stage === 'carrier_delivery_failed') {
             
             
            $stage_can_advance_sql .= " AND `delivery_status` <> 'delivered'";
        }
         
         
        $parts = [
            "`last_stage_code` = IF({$stage_can_advance_sql}, '" . $this->db->escape($code) . "', `last_stage_code`)",
            "`last_stage_at` = IF({$stage_can_advance_sql}, NOW(), `last_stage_at`)",
            "`date_modified` = NOW()",
        ];
        $part_count = max(0, min(100, (int)($context['part_count'] ?? 0)));
        if ($part_count > 0) {
            $parts[] = in_array($stage, ['carrier_delivered', 'carrier_delivery_failed'], true)
                ? "`parts_total` = IF(`delivery_status` IN ('delivered','failed'), `parts_total`, GREATEST(`parts_total`, {$part_count}))"
                : "`parts_total` = GREATEST(`parts_total`, {$part_count})";
        }
        if ($stage === 'modem_accepted') {
            $parts[] = "`delivery_status` = IF(`delivery_status` IN ('delivered','failed'), `delivery_status`, 'pending')";
        } elseif ($stage === 'send_outcome_unknown') {
            $parts[] = "`delivery_status` = IF(`delivery_status` IN ('delivered','failed','pending'), `delivery_status`, 'unknown')";
        } elseif ($stage === 'carrier_delivered') {
            $carrier_evidence = $this->carrierEvidence($stage, $context, (int)($job['attempt_count'] ?? 0));
            if (!is_array($carrier_evidence) || !$carrier_evidence) return;
            $carrier_attempt = (int)$carrier_evidence['attempt'];
            $delivered_count = "(SELECT COUNT(*) FROM `{$this->prefix}sms_job_events` e WHERE e.`job_id` = {$job_id} AND e.`carrier_attempt` = {$carrier_attempt} AND e.`stage` = 'carrier_delivered')";
            $failed_count = "(SELECT COUNT(*) FROM `{$this->prefix}sms_job_events` e WHERE e.`job_id` = {$job_id} AND e.`carrier_attempt` = {$carrier_attempt} AND e.`stage` = 'carrier_delivery_failed')";
            $delivery_confirmed = "{$failed_count} = 0 AND {$delivered_count} >= GREATEST(`parts_total`, {$part_count}, 1)";
             
             
            $stage_can_advance_sql = "({$delivery_confirmed}) OR ({$current_rank_sql} < {$stage_rank})";
            $parts[0] = "`last_stage_code` = IF({$stage_can_advance_sql}, '" . $this->db->escape($code) . "', `last_stage_code`)";
            $parts[1] = "`last_stage_at` = IF({$stage_can_advance_sql}, NOW(), `last_stage_at`)";
            $preserve_terminal_parts = "`delivery_status` IN ('delivered','failed') AND NOT ({$delivery_confirmed})";
            $parts[] = "`parts_delivered` = IF({$preserve_terminal_parts}, `parts_delivered`, {$delivered_count})";
            $parts[] = "`parts_delivery_failed` = IF({$preserve_terminal_parts}, `parts_delivery_failed`, {$failed_count})";
            $parts[] = "`delivery_status` = IF(`delivery_status` = 'delivered', 'delivered', IF({$delivery_confirmed}, 'delivered', IF(`delivery_status` = 'failed' OR {$failed_count} > 0, 'failed', 'pending')))";
            $parts[] = "`delivered_at` = IF({$delivery_confirmed}, COALESCE(`delivered_at`, NOW()), `delivered_at`)";
        } elseif ($stage === 'carrier_delivery_failed') {
            $carrier_evidence = $this->carrierEvidence($stage, $context, (int)($job['attempt_count'] ?? 0));
            if (!is_array($carrier_evidence) || !$carrier_evidence) return;
            $carrier_attempt = (int)$carrier_evidence['attempt'];
            $delivered_count = "(SELECT COUNT(*) FROM `{$this->prefix}sms_job_events` e WHERE e.`job_id` = {$job_id} AND e.`carrier_attempt` = {$carrier_attempt} AND e.`stage` = 'carrier_delivered')";
            $failed_count = "(SELECT COUNT(*) FROM `{$this->prefix}sms_job_events` e WHERE e.`job_id` = {$job_id} AND e.`carrier_attempt` = {$carrier_attempt} AND e.`stage` = 'carrier_delivery_failed')";
            $parts[] = "`parts_delivered` = IF(`delivery_status` IN ('delivered','failed'), `parts_delivered`, {$delivered_count})";
            $parts[] = "`parts_delivery_failed` = IF(`delivery_status` IN ('delivered','failed'), `parts_delivery_failed`, {$failed_count})";
            $parts[] = "`delivery_status` = IF(`delivery_status` = 'delivered', 'delivered', 'failed')";
        }
        $parts[] = "`last_stage` = IF({$stage_can_advance_sql}, '" . $this->db->escape($stage) . "', `last_stage`)";
         
         
        $this->db->query(
            "UPDATE `{$this->prefix}sms_jobs` SET " . implode(', ', $parts)
            . " WHERE `job_id` = {$job_id}"
            . " AND `device_id` = " . (int)($job['device_id'] ?? 0)
            . " AND `app_session_id` = " . (int)($job['app_session_id'] ?? 0)
        );

        $message_id = (int)($job['message_id'] ?? 0);
        if ($message_id > 0) {
            $this->db->query("UPDATE `{$this->prefix}messages` SET `date_modified` = NOW() WHERE `message_id` = {$message_id}");
        }
    }

    private function jobById(int $job_id): array {
        $query = $this->db->query("SELECT * FROM `{$this->prefix}sms_jobs` WHERE `job_id` = " . (int)$job_id . " LIMIT 1");
        return $query->row ?: [];
    }

    private function jobForSession(int $app_session_id, int $job_id): array {
        $query = $this->db->query(
            "SELECT j.*, s.`app_version_code` AS `session_app_version_code`
             FROM `{$this->prefix}sms_jobs` j
             LEFT JOIN `{$this->prefix}app_session` s ON s.`session_id` = j.`app_session_id`
             WHERE j.`job_id` = " . (int)$job_id . "
               AND j.`app_session_id` = " . (int)$app_session_id . "
             LIMIT 1"
        );
        return $query->row ?: [];
    }

    private function normalizeEventId(string $value, string $stage): string {
        $value = preg_replace('/[^a-zA-Z0-9._:-]/', '', trim($value));
        if ($value === '') {
            $value = 'server:' . $stage . ':' . bin2hex(random_bytes(8));
        }
        return substr($value, 0, 96);
    }

    private function normalizeCode(string $value): string {
        $value = strtolower(trim($value));
        $value = preg_replace('/[^a-z0-9._-]/', '_', $value);
        return substr((string)$value, 0, 64);
    }

    



    private function carrierEvidence(string $stage, array $context, int $legacy_attempt = 0): ?array {
        if (!in_array($stage, ['carrier_delivered', 'carrier_delivery_failed'], true)) return null;
        foreach (['part_index', 'part_count'] as $key) {
            if (!array_key_exists($key, $context) || filter_var($context[$key], FILTER_VALIDATE_INT) === false) return [];
        }
        if (array_key_exists('attempt', $context)) {
            if (filter_var($context['attempt'], FILTER_VALIDATE_INT) === false) return [];
            $attempt = (int)$context['attempt'];
        } else {
             
             
            $attempt = max(1, min(100, $legacy_attempt));
        }
        $part_index = (int)$context['part_index'];
        $part_count = (int)$context['part_count'];
        if ($attempt < 1 || $attempt > 100 || $part_count < 1 || $part_count > 100 || $part_index < 0 || $part_index >= $part_count) return [];
        return ['attempt' => $attempt, 'part_index' => $part_index, 'part_count' => $part_count];
    }

    private function sanitizeContext(array $context): array {
        $safe = [];
        foreach (self::CONTEXT_KEYS as $key) {
            if (!array_key_exists($key, $context) || !is_scalar($context[$key])) {
                continue;
            }
            if (in_array($key, ['default_subscription_id', 'subscription_id'], true)) {
                 
                 
                 
                $safe[$key] = max(-1, min(2147483647, (int)$context[$key]));
                continue;
            }
            if ($key === 'attempt') {
                $safe[$key] = max(0, min(100, (int)$context[$key]));
                continue;
            }
            if (is_bool($context[$key]) || is_int($context[$key]) || is_float($context[$key])) {
                $safe[$key] = $context[$key];
                continue;
            }
            $value = preg_replace('/[\x00-\x1F\x7F]/u', '', trim((string)$context[$key]));
            $safe[$key] = mb_substr((string)$value, 0, 120, 'UTF-8');
        }
        return $safe;
    }

    private function levelForStage(string $stage): string {
        if (in_array($stage, ['send_outcome_unknown', 'deadline_expired'], true)) {
            return 'warning';
        }
        if (in_array($stage, ['push_failed', 'domain_mismatch', 'permission_missing', 'device_unavailable', 'job_fetch_failed', 'license_blocked', 'license_check_failed', 'modem_failed', 'completion_report_failed', 'carrier_delivery_failed'], true)) {
            return 'error';
        }
        if (in_array($stage, ['push_accepted', 'modem_accepted', 'completion_reported', 'carrier_delivered'], true)) {
            return 'success';
        }
        return 'info';
    }
}
