<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AndroidPushNotifier;
use EasyChat\Library\Notification\TrialLockGuard;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\SmsDevice;
use EasyChat\Model\SmsJob;
use EasyChat\Model\SmsJobEvent;

class SmsOutboundService {
    private $registry;
    private $logger;

    public function __construct($registry) {
        $this->registry = $registry;
        $this->logger = new MessengerLogger($registry, SmsProvider::PROVIDER);
    }

    public function enqueueSms(array $payload): array {
        if ((new TrialLockGuard($this->registry))->isSmsBlocked(false)) {
            return [
                'success' => false,
                'http_code' => 402,
                'error' => 'trial_limit_exceeded',
                'code' => 'trial_limit_exceeded',
            ];
        }

        $phone = SmsPhoneNumber::normalize((string)($payload['phone'] ?? ''));
        if ($phone === '') {
            return ['success' => false, 'error' => 'sms_phone_invalid'];
        }
        $text = (string)($payload['text'] ?? '');
        $segments = SmsTextMetrics::segments($text);
        if ($segments > SmsTextMetrics::MAX_SEGMENTS) {
            return [
                'success' => false,
                'error' => 'sms_segment_limit_exceeded',
                'sms_segments' => $segments,
                'max_sms_segments' => SmsTextMetrics::MAX_SEGMENTS,
            ];
        }
        if ((int)($payload['message_id'] ?? 0) <= 0 || (int)($payload['delivery_id'] ?? 0) <= 0) {
             
             
             
            return ['success' => false, 'error' => 'sms_job_identity_required'];
        }
        $expires_at = trim((string)($payload['expires_at'] ?? ''));
        if ($expires_at !== '') {
            $expires_date = \DateTimeImmutable::createFromFormat(
                '!Y-m-d H:i:s',
                $expires_at,
                new \DateTimeZone('UTC')
            );
            if (!$expires_date instanceof \DateTimeImmutable || $expires_date->format('Y-m-d H:i:s') !== $expires_at) {
                return ['success' => false, 'terminal' => true, 'error' => 'sms_expiry_invalid'];
            }
            if ($expires_date->getTimestamp() <= time()) {
                return ['success' => false, 'terminal' => true, 'error' => 'deadline_expired'];
            }
        }

        $job_model = new SmsJob($this->registry);
        $preferred_manager_id = (int)($payload['manager_id'] ?? 0);
        $device_model = new SmsDevice($this->registry);
        $device = $this->selectReadyDevice($job_model, $preferred_manager_id);
        if (!$device) {
            return ['success' => false, 'error' => 'sms_device_missing'];
        }

        $device_rate_lock_name = (string)($device['_capacity_lock_name'] ?? '');
        $hold_rate_lock = !empty($payload['hold_rate_lock']) && !empty($payload['create_only']);
        $created = $job_model->createRateLimited([
            'device_id' => (int)$device['device_id'],
            'app_device_id' => (int)($device['app_device_id'] ?? 0),
            'app_session_id' => (int)$device['app_session_id'],
            'manager_id' => (int)($payload['manager_id'] ?? 0),
            'chat_id' => (int)($payload['chat_id'] ?? 0),
            'message_id' => (int)($payload['message_id'] ?? 0),
            'delivery_id' => (int)($payload['delivery_id'] ?? 0),
            'phone_e164' => $phone,
            'message_text' => $text,
            'expires_at' => $expires_at,
        ], 3, $hold_rate_lock);
        if (empty($created['success'])) {
            if ($device_rate_lock_name !== '') $job_model->releaseRateLock($device_rate_lock_name);
            return ['success' => false, 'error' => (string)($created['error'] ?? 'sms_job_create_failed')];
        }
        $job_id = (int)$created['job_id'];
        $reused_job = !empty($created['reused']);
        $rate_lock_name = (string)($created['rate_lock_name'] ?? '');
        $keep_device_lock = $hold_rate_lock && !$reused_job;
        if (!$keep_device_lock && $device_rate_lock_name !== '') {
            $job_model->releaseRateLock($device_rate_lock_name);
            $device_rate_lock_name = '';
        }
        try {
            $device_model->markUsed((int)$device['device_id']);

            $event_model = new SmsJobEvent($this->registry);
            $event_model->recordServer($job_id, 'job_created', '', [], 'server:job_created');
        } catch (\Throwable $e) {
             
             
            if ($rate_lock_name !== '') $job_model->releaseRateLock($rate_lock_name);
            if ($device_rate_lock_name !== '') $job_model->releaseRateLock($device_rate_lock_name);
            throw $e;
        }

        if (!empty($payload['create_only'])) {
            $result = [
                'success' => true,
                'http_code' => 202,
                'external_message_id' => 'sms-job-' . $job_id,
                'status' => 'queued',
                'job_id' => $job_id,
                'device_id' => (int)$device['device_id'],
                'push_deferred' => 1,
            ];
            if ($rate_lock_name !== '') $result['rate_lock_name'] = $rate_lock_name;
            if ($device_rate_lock_name !== '') $result['device_rate_lock_name'] = $device_rate_lock_name;
            if ($reused_job) $result['reused'] = 1;
            return $result;
        }

         
         
        $push = $this->notifyExistingJob($job_id);
        $final_job = $job_model->getById($job_id);
        $push_sent = (int)($push['push_sent'] ?? 0);
        $this->logger->info('sms_job_created', [
            'job_id' => $job_id,
            'chat_id' => (int)($payload['chat_id'] ?? 0),
            'message_id' => (int)($payload['message_id'] ?? 0),
            'device_id' => (int)($final_job['device_id'] ?? $device['device_id']),
            'push_sent' => $push_sent,
        ]);

        if ((string)($final_job['status'] ?? '') === 'expired'
            || (!empty($push['terminal']) && empty($push['success']))) {
             
             
             
            return [
                'success' => false,
                'terminal' => true,
                'retryable' => false,
                'status' => (string)($final_job['status'] ?? $push['status'] ?? 'expired'),
                'error' => (string)($push['error'] ?? 'deadline_expired'),
                'job_id' => $job_id,
                'device_id' => (int)($final_job['device_id'] ?? $device['device_id']),
                'push_sent' => $push_sent,
            ];
        }

        $result = [
            'success' => true,
            'http_code' => 202,
            'external_message_id' => 'sms-job-' . $job_id,
            'status' => 'queued',
            'job_id' => $job_id,
            'device_id' => (int)($final_job['device_id'] ?? $device['device_id']),
            'push_sent' => $push_sent,
        ];
        if ($rate_lock_name !== '') $result['rate_lock_name'] = $rate_lock_name;
        if ($device_rate_lock_name !== '') $result['device_rate_lock_name'] = $device_rate_lock_name;
        if ($reused_job) $result['reused'] = 1;
        return $result;
    }

    




    public function notifyExistingJob(int $job_id): array {
        $model = new SmsJob($this->registry);
        $was_reassigned = false;
        $job = $model->getById($job_id);
        if (!$job) {
            return ['success' => false, 'terminal' => true, 'error' => 'sms_job_missing'];
        }
        if ((string)$job['status'] !== 'pending') {
            return $this->resultForCurrentJobState($job);
        }
        $expires_at = trim((string)($job['expires_at'] ?? ''));
        $expires_ts = $expires_at !== ''
            ? \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $expires_at, new \DateTimeZone('UTC'))
            : false;
        if ($expires_ts instanceof \DateTimeImmutable && $expires_ts->getTimestamp() <= time()) {
             
             
             
            $current = $model->markExpired($job_id);
            if (!$current) $current = $model->getById($job_id);
            return $this->resultForCurrentJobState($current);
        }
        $events = new SmsJobEvent($this->registry);
        if (!$this->jobDeviceIsReady($job)) {
            $previous_device_id = (int)$job['device_id'];
            $fallback = $this->selectReadyDevice($model, (int)($job['manager_id'] ?? 0), $previous_device_id);
            if (!$fallback) {
                return ['success' => false, 'terminal' => false, 'retry_after' => 60, 'error' => 'sms_device_missing'];
            }
            $moved = $this->reassignPendingToDevice($model, $job_id, (int)$job['app_session_id'], $fallback);
            if (!$moved) {
                $current = $model->getById($job_id);
                return ['success' => true, 'terminal' => (string)($current['status'] ?? '') !== 'pending', 'status' => (string)($current['status'] ?? '')];
            }
            (new SmsDevice($this->registry))->markUsed((int)$fallback['device_id']);
            $events->recordServer($job_id, 'device_reassigned', 'device_not_ready', [
                'previous_device_id' => $previous_device_id,
                'device_id' => (int)$fallback['device_id'],
            ], 'server:device_reassigned:' . $previous_device_id . ':' . (int)$fallback['device_id']);
            $job = $moved;
            $was_reassigned = true;
        }
        $attempt = (int)($job['push_attempt_count'] ?? 0);
        if ($attempt >= 8) {
            return ['success' => false, 'terminal' => false, 'retry_after' => 900, 'error' => 'push_retry_limit'];
        }
        $next_push_at = trim((string)($job['next_push_at'] ?? ''));
        $next_date = $next_push_at !== ''
            ? \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $next_push_at, new \DateTimeZone('UTC'))
            : false;
        $next_ts = $next_date instanceof \DateTimeImmutable ? $next_date->getTimestamp() : 0;
        if ($next_ts > time()) {
            return ['success' => true, 'terminal' => false, 'retry_after' => max(15, $next_ts - time()), 'deferred' => 1];
        }

        if ($this->shouldFailoverForMissingAck($job)) {
            $previous_device_id = (int)$job['device_id'];
            $fallback = $this->selectReadyDevice($model, (int)($job['manager_id'] ?? 0), $previous_device_id);
            if ($fallback) {
                 
                 
                 
                $moved = $this->reassignPendingToDevice($model, $job_id, (int)$job['app_session_id'], $fallback);
                if ($moved) {
                    (new SmsDevice($this->registry))->markUsed((int)$fallback['device_id']);
                    $events->recordServer($job_id, 'device_reassigned', 'push_ack_timeout', [
                        'previous_device_id' => $previous_device_id,
                        'device_id' => (int)$fallback['device_id'],
                    ], 'server:device_reassigned:' . $previous_device_id . ':' . (int)$fallback['device_id']);
                    $job = $moved;
                    $attempt = 0;
                    $was_reassigned = true;
                }
            }
        }

        $delay = min(600, 30 * (1 << min(4, $attempt)));
        $job = $model->registerPushAttempt($job_id, $delay);
        $attempt = (int)($job['push_attempt_count'] ?? ($attempt + 1));
        $notifier = new AndroidPushNotifier($this->registry);
        $push = $notifier->notifySmsJob((int)$job['app_session_id'], $job_id);
        $push_sent = (int)($push['sent'] ?? 0);
        $device_id = (int)$job['device_id'];
        if ($push_sent > 0) {
            $events->recordServer($job_id, 'push_accepted', '', ['push_sent' => $push_sent], 'server:push_result:' . $device_id . ':' . $attempt);
        } else {
            $code = (string)($push['error'] ?? $push['skipped'] ?? 'push_not_accepted');
            $events->recordServer($job_id, 'push_failed', $code, ['push_sent' => 0], 'server:push_result:' . $device_id . ':' . $attempt);
            if ($this->isPermanentTargetFailure($code)) {
                $fallback = $this->selectReadyDevice($model, (int)($job['manager_id'] ?? 0), $device_id);
                if ($fallback) {
                     
                     
                    $moved = $this->reassignPendingToDevice($model, $job_id, (int)$job['app_session_id'], $fallback);
                    if ($moved) {
                        (new SmsDevice($this->registry))->markUsed((int)$fallback['device_id']);
                        $events->recordServer($job_id, 'device_reassigned', $code, [
                            'previous_device_id' => $device_id,
                            'device_id' => (int)$fallback['device_id'],
                        ], 'server:device_reassigned:' . $device_id . ':' . (int)$fallback['device_id']);
                        $job = $model->registerPushAttempt($job_id, $delay);
                        $attempt = (int)($job['push_attempt_count'] ?? 1);
                        $device_id = (int)$job['device_id'];
                        $push = $notifier->notifySmsJob((int)$job['app_session_id'], $job_id);
                        $push_sent = (int)($push['sent'] ?? 0);
                        $fallback_code = (string)($push['error'] ?? $push['skipped'] ?? 'push_not_accepted');
                        $events->recordServer(
                            $job_id,
                            $push_sent > 0 ? 'push_accepted' : 'push_failed',
                            $push_sent > 0 ? '' : $fallback_code,
                            ['push_sent' => $push_sent],
                            'server:push_result:' . $device_id . ':' . $attempt
                        );
                        $push['reassigned'] = 1;
                    }
                }
            }
        }
        $this->logger->info('sms_job_push_attempt', [
            'job_id' => $job_id,
            'device_id' => (int)$job['device_id'],
            'attempt' => $attempt,
            'push_sent' => $push_sent,
            'reassigned' => $was_reassigned || !empty($push['reassigned']) ? 1 : 0,
        ]);
        return [
            'success' => $push_sent > 0,
            'terminal' => false,
            'retry_after' => $delay,
            'push_sent' => $push_sent,
            'attempt' => $attempt,
            'device_id' => $device_id,
            'reassigned' => $was_reassigned || !empty($push['reassigned']) ? 1 : 0,
            'error' => $push_sent > 0 ? '' : (string)($push['error'] ?? $push['skipped'] ?? 'push_not_accepted'),
        ];
    }

     
    private function resultForCurrentJobState(array $job): array {
        $status = (string)($job['status'] ?? '');
        if ($status === '') {
            return ['success' => false, 'terminal' => true, 'error' => 'sms_job_missing'];
        }
        if ($status === 'expired') {
            return [
                'success' => false,
                'terminal' => true,
                'status' => 'expired',
                'error' => 'deadline_expired',
            ];
        }
        if ($status === 'pending') {
             
             
            return [
                'success' => false,
                'terminal' => false,
                'status' => 'pending',
                'retry_after' => 15,
                'error' => 'sms_job_state_changed',
            ];
        }
        return ['success' => true, 'terminal' => true, 'status' => $status];
    }

    private function selectReadyDevice(SmsJob $job_model, int $preferred_manager_id, int $exclude_device_id = 0): array {
        $ready = (new SmsDevice($this->registry))->listReady();
        foreach ($ready as $position => &$candidate) $candidate['_stable_position'] = $position;
        unset($candidate);
        usort($ready, static function (array $left, array $right) use ($preferred_manager_id): int {
            $left_preferred = $preferred_manager_id > 0 && (int)($left['manager_id'] ?? 0) === $preferred_manager_id ? 0 : 1;
            $right_preferred = $preferred_manager_id > 0 && (int)($right['manager_id'] ?? 0) === $preferred_manager_id ? 0 : 1;
            return $left_preferred === $right_preferred
                ? (int)$left['_stable_position'] <=> (int)$right['_stable_position']
                : $left_preferred <=> $right_preferred;
        });
        foreach ($ready as $candidate) {
            $device_id = (int)($candidate['device_id'] ?? 0);
            if ($device_id <= 0 || $device_id === $exclude_device_id) continue;
            $lock_name = $job_model->acquireDeviceCapacityLock($device_id, 2);
            if ($lock_name === '') continue;
            try {
                if ($job_model->countRecentByDevice($device_id, 3600) < 10) {
                    $candidate['_capacity_lock_name'] = $lock_name;
                    return $candidate;
                }
            } catch (\Throwable $e) {
                $job_model->releaseRateLock($lock_name);
                throw $e;
            }
            $job_model->releaseRateLock($lock_name);
        }
        return [];
    }

    private function reassignPendingToDevice(
        SmsJob $job_model,
        int $job_id,
        int $current_app_session_id,
        array $device
    ): array {
        $lock_name = (string)($device['_capacity_lock_name'] ?? '');
        try {
            return $job_model->reassignPendingDevice($job_id, $current_app_session_id, $device);
        } finally {
            if ($lock_name !== '') $job_model->releaseRateLock($lock_name);
        }
    }

    private function isPermanentTargetFailure(string $code): bool {
         
         
         
        return in_array($code, ['no_target_token', 'push_token_not_registered', 'not_found'], true);
    }

    private function jobDeviceIsReady(array $job): bool {
        foreach ((new SmsDevice($this->registry))->listReady() as $device) {
            if ((int)($device['device_id'] ?? 0) === (int)($job['device_id'] ?? 0)
                && (int)($device['app_session_id'] ?? 0) === (int)($job['app_session_id'] ?? 0)) return true;
        }
        return false;
    }

    private function shouldFailoverForMissingAck(array $job): bool {
        if ((string)($job['status'] ?? '') !== 'pending'
            || (int)($job['push_attempt_count'] ?? 0) < 2
            || in_array((string)($job['last_stage'] ?? ''), ['push_received','job_reserved'], true)) return false;
        $last_push_at = trim((string)($job['last_push_at'] ?? ''));
        $last_push = $last_push_at !== ''
            ? \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $last_push_at, new \DateTimeZone('UTC'))
            : false;
        return $last_push instanceof \DateTimeImmutable && $last_push->getTimestamp() <= time() - 60;
    }

    public function ensureChannel(): array {
        $model = new MessengerModel($this->registry);
        $channel = $model->ensureChannel(SmsProvider::PROVIDER, 'SMS');
        if (($channel['status'] ?? '') !== 'active') {
            $model->updateChannel((int)$channel['channel_id'], [
                'status' => 'active',
                'credentials_json' => json_encode(['local' => true], JSON_UNESCAPED_SLASHES),
                'settings_json' => json_encode(['round_robin' => true], JSON_UNESCAPED_SLASHES),
            ]);
            $channel = $model->getChannelByProvider(SmsProvider::PROVIDER);
        }
        return $channel ?: [];
    }

}
