<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AndroidPushNotifier;
use EasyChat\Library\AppClock;
use EasyChat\Library\CustomerWebPushNotifier;
use EasyChat\Library\MessageTextRenderer;
use EasyChat\Library\ResponseTimeMetrics;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Model\Message;
use EasyChat\Model\ScheduledMessage;
use EasyChat\Model\Session;







class ScheduledMessageService {
    public const MIN_LEAD_SECONDS = 120;
    public const MAX_HORIZON_DAYS = 365;
    public const CLAIM_LEASE_SECONDS = 300;
    public const MAX_TEXT_LENGTH = 10000;

    private $registry;
    private $model;
    private $clock;

    public function __construct($registry) {
        $this->registry = $registry;
        $this->model = new ScheduledMessage($registry);
        $this->clock = new AppClock($registry);
    }

    public function createForChat(array $input): array {
        $chat_id = max(0, (int)($input['chat_id'] ?? 0));
        $manager_id = max(0, (int)($input['manager_id'] ?? 0));
        $manager_name = trim((string)($input['manager_name'] ?? '')) ?: 'Оператор';
        $client_request_id = trim((string)($input['client_request_id'] ?? ''));
        $text = trim((string)($input['text'] ?? ''));
        if (!DirectMessageIdempotency::validClientRequestId($client_request_id)) {
            return $this->fail('client_request_id_invalid', 'Некорректный идентификатор операции.', 422);
        }
        if ($chat_id <= 0 || $manager_id <= 0) {
            return $this->fail('chat_not_found', 'Диалог не найден.', 404);
        }
        if ($text === '') {
            return $this->fail('empty_message', 'Введите текст сообщения.', 422);
        }
        if (mb_strlen($text, 'UTF-8') > self::MAX_TEXT_LENGTH) {
            return $this->fail('message_too_long', 'Сообщение слишком длинное.', 422);
        }

        $scheduled = $this->normalizeScheduledAt((string)($input['scheduled_at'] ?? ''));
        if (empty($scheduled['success'])) {
            return $scheduled;
        }

        $chat = (new Session($this->registry))->getSessionById($chat_id);
        if (!$chat) {
            return $this->fail('chat_not_found', 'Диалог не найден.', 404);
        }
        $reply = (new ReplyCapability($this->registry))->forChatId($chat_id);
        if (empty($reply['can_reply'])) {
            return $this->fail(
                'reply_not_allowed',
                (string)($reply['message'] ?? 'В этот диалог нельзя ответить из EasyChat.'),
                409,
                ['reply_capability' => $reply]
            );
        }

        $payload = is_array($input['idempotency_payload'] ?? null)
            ? $input['idempotency_payload']
            : [
                'chat_id' => $chat_id,
                'manager_id' => $manager_id,
                'text' => $text,
                'scheduled_at' => (string)$scheduled['scheduled_at'],
            ];
        $payload_hash = DirectMessageIdempotency::payloadHash($payload);
        $existing = $this->model->findByClientRequest($manager_id, $client_request_id);
        if ($existing) {
            if (!hash_equals((string)$existing['payload_hash'], $payload_hash)) {
                return $this->fail('client_request_id_reused', 'Эта операция уже относится к другому сообщению.', 409);
            }
            return ['success' => true, 'replayed' => true, 'scheduled_message' => $this->payload($existing)];
        }

        try {
            $this->model->begin();
            $existing = $this->model->findByClientRequest($manager_id, $client_request_id, true);
            if ($existing) {
                $this->model->commit();
                if (!hash_equals((string)$existing['payload_hash'], $payload_hash)) {
                    return $this->fail('client_request_id_reused', 'Эта операция уже относится к другому сообщению.', 409);
                }
                return ['success' => true, 'replayed' => true, 'scheduled_message' => $this->payload($existing)];
            }
            $id = $this->model->create([
                'public_id' => 'sch_' . bin2hex(random_bytes(16)),
                'client_request_id' => $client_request_id,
                'payload_hash' => $payload_hash,
                'chat_id' => $chat_id,
                'source_provider' => strtolower((string)($chat['source_provider'] ?? 'widget')),
                'message_text' => $text,
                'scheduled_at' => (string)$scheduled['scheduled_at'],
                'timezone' => $this->clock->timezoneName(),
                'manager_id' => $manager_id,
                'manager_name' => $manager_name,
            ]);
            $this->model->recordEvent($id, 'created', $manager_id, '', 'scheduled', '', (string)$scheduled['scheduled_at'], $client_request_id, [
                'surface' => substr((string)($input['surface'] ?? 'web'), 0, 24),
            ]);
            $this->model->commit();
        } catch (\Throwable $e) {
            try {
                $this->model->rollback();
            } catch (\Throwable $ignored) {
            }
            return $this->fail('schedule_save_failed', 'Не удалось надёжно сохранить расписание.', 500);
        }

        $row = $this->model->findById($id);
        $this->notifyScheduleChanged($chat);
        return ['success' => true, 'scheduled_message' => $this->payload($row)];
    }

    public function validateDraft(string $text, string $scheduled_at): array {
        $text = trim($text);
        if ($text === '') {
            return $this->fail('empty_message', 'Введите текст сообщения.', 422);
        }
        if (mb_strlen($text, 'UTF-8') > self::MAX_TEXT_LENGTH) {
            return $this->fail('message_too_long', 'Сообщение слишком длинное.', 422);
        }
        return $this->normalizeScheduledAt($scheduled_at);
    }

    public function update(array $input): array {
        $row = $this->model->findByPublicId(trim((string)($input['public_id'] ?? '')));
        if (!$row) {
            return $this->fail('scheduled_message_not_found', 'Отложенное сообщение не найдено.', 404);
        }
        $revision = max(0, (int)($input['revision'] ?? 0));
        $manager_id = max(0, (int)($input['manager_id'] ?? 0));
        $manager_name = trim((string)($input['manager_name'] ?? '')) ?: 'Оператор';
        $text = trim((string)($input['text'] ?? ''));
        if ($text === '' || mb_strlen($text, 'UTF-8') > self::MAX_TEXT_LENGTH) {
            return $this->fail('message_invalid', 'Проверьте текст сообщения.', 422);
        }
        $scheduled = $this->normalizeScheduledAt((string)($input['scheduled_at'] ?? ''));
        if (empty($scheduled['success'])) {
            return $scheduled;
        }
        $reply = (new ReplyCapability($this->registry))->forChatId((int)$row['chat_id']);
        if (empty($reply['can_reply'])) {
            return $this->fail('reply_not_allowed', (string)($reply['message'] ?? 'Ответ недоступен.'), 409, ['reply_capability' => $reply]);
        }
        if (!$this->model->updateScheduled((int)$row['scheduled_message_id'], $revision, [
            'message_text' => $text,
            'scheduled_at' => (string)$scheduled['scheduled_at'],
            'timezone' => $this->clock->timezoneName(),
            'manager_id' => $manager_id,
            'manager_name' => $manager_name,
        ])) {
            return $this->conflict($row['public_id']);
        }
        $fresh = $this->model->findById((int)$row['scheduled_message_id']);
        $this->model->recordEvent(
            (int)$row['scheduled_message_id'],
            'updated',
            $manager_id,
            (string)$row['status'],
            'scheduled',
            (string)$row['scheduled_at'],
            (string)$fresh['scheduled_at'],
            (string)($input['client_request_id'] ?? '')
        );
        $this->notifyScheduleChangedByChatId((int)$row['chat_id']);
        return ['success' => true, 'scheduled_message' => $this->payload($fresh)];
    }

    public function cancel(array $input): array {
        $row = $this->model->findByPublicId(trim((string)($input['public_id'] ?? '')));
        if (!$row) {
            return $this->fail('scheduled_message_not_found', 'Отложенное сообщение не найдено.', 404);
        }
        if ((string)$row['status'] === 'cancelled') {
            return ['success' => true, 'replayed' => true, 'public_id' => (string)$row['public_id']];
        }
        $manager_id = max(0, (int)($input['manager_id'] ?? 0));
        $manager_name = trim((string)($input['manager_name'] ?? '')) ?: 'Оператор';
        if (!$this->model->cancel(
            (int)$row['scheduled_message_id'],
            max(0, (int)($input['revision'] ?? 0)),
            $manager_id,
            $manager_name
        )) {
            return $this->conflict((string)$row['public_id']);
        }
        $this->model->recordEvent(
            (int)$row['scheduled_message_id'],
            'cancelled',
            $manager_id,
            (string)$row['status'],
            'cancelled',
            (string)$row['scheduled_at'],
            '',
            (string)($input['client_request_id'] ?? '')
        );
        $this->notifyScheduleChangedByChatId((int)$row['chat_id']);
        return ['success' => true, 'public_id' => (string)$row['public_id']];
    }

    public function sendNow(array $input): array {
        $row = $this->model->findByPublicId(trim((string)($input['public_id'] ?? '')));
        if (!$row) {
            return $this->fail('scheduled_message_not_found', 'Отложенное сообщение не найдено.', 404);
        }
        $manager_id = max(0, (int)($input['manager_id'] ?? 0));
        $manager_name = trim((string)($input['manager_name'] ?? '')) ?: 'Оператор';
        $now = $this->clock->nowSql();
        if (!$this->model->updateScheduled((int)$row['scheduled_message_id'], max(0, (int)($input['revision'] ?? 0)), [
            'message_text' => (string)$row['message_text'],
            'scheduled_at' => $now,
            'timezone' => $this->clock->timezoneName(),
            'manager_id' => $manager_id,
            'manager_name' => $manager_name,
        ])) {
            return $this->conflict((string)$row['public_id']);
        }
        $this->model->recordEvent(
            (int)$row['scheduled_message_id'],
            'send_now',
            $manager_id,
            (string)$row['status'],
            'scheduled',
            (string)$row['scheduled_at'],
            $now,
            (string)($input['client_request_id'] ?? '')
        );
        $this->enqueueId((int)$row['scheduled_message_id']);
        $this->notifyScheduleChangedByChatId((int)$row['chat_id']);
        return ['success' => true, 'scheduled_message' => $this->payload($this->model->findById((int)$row['scheduled_message_id']))];
    }

    public function payloadForChat(int $chat_id): array {
        $items = [];
        foreach ($this->model->activeForChat($chat_id) as $row) {
            $items[] = $this->payload($row);
        }
        return [
            'scheduled_messages' => $items,
            'scheduled_count' => count($items),
            'scheduled_sync_at' => $this->clock->nowIsoUtc(),
            'scheduled_timezone' => $this->clock->timezoneName(),
        ];
    }

    public function enqueueDue(int $limit = 100): array {
        $recovered = $this->model->recoverStaleClaims(self::CLAIM_LEASE_SECONDS);
        $queued = 0;
        foreach ($this->model->dueIds($limit) as $id) {
            if ($this->enqueueId($id) > 0) {
                $queued++;
            }
        }
        return ['ran' => true, 'queued' => $queued, 'recovered' => $recovered];
    }

    public function release(int $scheduled_message_id): array {
        $claim_token = bin2hex(random_bytes(32));
        $claim_hash = hash('sha256', $claim_token);
        $row = [];
        try {
            $this->model->begin();
            $row = $this->model->claim($scheduled_message_id, $claim_hash);
            if (!$row) {
                $this->model->commit();
                return ['success' => true, 'skipped' => 'not_due_or_owned'];
            }
            $reply = (new ReplyCapability($this->registry))->forChatId((int)$row['chat_id']);
            if (empty($reply['can_reply'])) {
                $message = $this->providerClosedMessage((string)$row['source_provider'], $reply);
                $this->model->markFailed((int)$row['scheduled_message_id'], $claim_hash, 'reply_not_allowed', $message);
                $this->model->recordEvent((int)$row['scheduled_message_id'], 'failed', 0, 'releasing', 'failed', (string)$row['scheduled_at'], '', '', [
                    'code' => 'reply_not_allowed',
                ]);
                $this->model->commit();
                $this->notifyScheduleChangedByChatId((int)$row['chat_id']);
                return ['success' => false, 'terminal' => true, 'error' => 'reply_not_allowed'];
            }

            $message_model = new Message($this->registry);
            $message_id = (int)$message_model->addMessage([
                'chat_id' => (int)$row['chat_id'],
                'author' => 2,
                'manager_id' => (int)$row['created_by_manager_id'],
                'manager_name' => (string)$row['created_by_manager_name'],
                'text' => urlencode((string)$row['message_text']),
                'image' => '',
                'status_user' => 0,
                'status_admin' => 1,
                'ip' => '',
                'source_provider' => (string)$row['source_provider'],
                'skip_metrics' => true,
                'touch_chat' => false,
            ]);
            if ((string)$row['source_provider'] === SmsProvider::PROVIDER) {
                (new DirectSmsDispatchOutbox($this->registry))->enqueue($message_id);
            }
            if (!$this->model->markReleased((int)$row['scheduled_message_id'], $claim_hash, $message_id)) {
                throw new \RuntimeException('scheduled_release_state_conflict');
            }
            $this->model->recordEvent((int)$row['scheduled_message_id'], 'released', 0, 'releasing', 'released', (string)$row['scheduled_at'], '', '', [
                'message_id' => $message_id,
            ]);
            $this->model->commit();
        } catch (\Throwable $e) {
            try {
                $this->model->rollback();
            } catch (\Throwable $ignored) {
            }
            if ($row) {
                try {
                    $this->model->markFailed((int)$row['scheduled_message_id'], $claim_hash, 'release_failed', 'Не удалось подготовить отправку. Повторите попытку.');
                } catch (\Throwable $ignored) {
                }
            }
            return ['success' => false, 'retryable' => true, 'error' => 'release_failed'];
        }

        $chat_id = (int)$row['chat_id'];
        (new Session($this->registry))->updateModifiedDate($chat_id);
        try {
            (new ResponseTimeMetrics($this->registry))->recordMessage($message_id);
        } catch (\Throwable $ignored) {
        }
        try {
            $chat = (new Session($this->registry))->getSessionById($chat_id);
            (new CustomerWebPushNotifier($this->registry))->notifyReply($chat_id, 'operator');
            (new AndroidPushNotifier($this->registry))->notifySyncChat(
                $chat_id,
                (string)($chat['source_provider'] ?? 'widget'),
                (string)($chat['customer_name'] ?? ''),
                strtoupper((string)($chat['source_provider'] ?? 'widget'))
            );
        } catch (\Throwable $ignored) {
        }

        $dispatch = (new OutboundDispatchGuard($this->registry))->sendMessageId($message_id);
        if ((string)$row['source_provider'] === SmsProvider::PROVIDER) {
            (new DirectSmsDispatchOutbox($this->registry))->settleAfterImmediateAttempt($message_id, $dispatch);
        }
        return ['success' => true, 'message_id' => $message_id, 'outbound_delivery' => $dispatch];
    }

    public function existingCreateResult(int $manager_id, string $client_request_id, array $payload): array {
        if (!DirectMessageIdempotency::validClientRequestId($client_request_id)) {
            return ['state' => 'invalid'];
        }
        $row = $this->model->findByClientRequest($manager_id, $client_request_id);
        if (!$row) {
            return ['state' => 'missing'];
        }
        $hash = DirectMessageIdempotency::payloadHash($payload);
        if (!hash_equals((string)$row['payload_hash'], $hash)) {
            return ['state' => 'conflict'];
        }
        return ['state' => 'completed', 'result' => ['success' => true, 'replayed' => true, 'chat_id' => (int)$row['chat_id'], 'scheduled_message' => $this->payload($row)]];
    }

    private function enqueueId(int $id): int {
        return (new QueueManager($this->registry))->pushUnique(
            'scheduled_message_release',
            ['scheduled_message_id' => $id],
            'scheduled_message:' . $id,
            0,
            15
        );
    }

    private function normalizeScheduledAt(string $value): array {
        $value = trim($value);
        if (!preg_match('/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?$/D', $value)) {
            return $this->fail('scheduled_at_invalid', 'Выберите корректные дату и время.', 422);
        }
        $normalized = str_replace('T', ' ', $value);
        if (strlen($normalized) === 16) {
            $normalized .= ':00';
        }
        try {
            $date = \DateTimeImmutable::createFromFormat(
                '!Y-m-d H:i:s',
                $normalized,
                new \DateTimeZone($this->clock->timezoneName())
            );
            $parse_errors = \DateTimeImmutable::getLastErrors();
            $valid = $date instanceof \DateTimeImmutable
                && ($parse_errors === false || (
                    (int)($parse_errors['warning_count'] ?? 0) === 0
                    && (int)($parse_errors['error_count'] ?? 0) === 0
                ))
                && $date->format('Y-m-d H:i:s') === $normalized;
            $sql = $valid ? $date->format('Y-m-d H:i:s') : '';
        } catch (\Throwable $e) {
            $sql = '';
        }
        $timestamp = $sql !== '' ? $this->clock->sqlToTimestamp($sql) : false;
        if ($timestamp === false) {
            return $this->fail('scheduled_at_invalid', 'Выберите корректные дату и время.', 422);
        }
        $now = time();
        if ($timestamp < $now + self::MIN_LEAD_SECONDS) {
            return $this->fail('scheduled_at_too_soon', 'Выберите время хотя бы через 2 минуты.', 422);
        }
        if ($timestamp > $now + self::MAX_HORIZON_DAYS * 86400) {
            return $this->fail('scheduled_at_too_far', 'Можно запланировать отправку не более чем на год вперёд.', 422);
        }
        return ['success' => true, 'scheduled_at' => $sql, 'scheduled_at_iso' => $this->clock->sqlToIsoUtc($sql)];
    }

    private function payload(array $row): array {
        $text = (string)($row['message_text'] ?? '');
        return [
            'public_id' => (string)$row['public_id'],
            'chat_id' => (int)$row['chat_id'],
            'source_provider' => (string)$row['source_provider'],
            'text' => $text,
            'text_html' => (new MessageTextRenderer())->toHtml($text),
            'status' => (string)$row['status'],
            'scheduled_at' => (string)$row['scheduled_at'],
            'scheduled_at_iso' => $this->clock->sqlToIsoUtc((string)$row['scheduled_at']),
            'scheduled_at_display' => $this->displayDate((string)$row['scheduled_at']),
            'timezone' => (string)$row['timezone'],
            'created_by_manager_id' => (int)$row['created_by_manager_id'],
            'created_by_manager_name' => (string)$row['created_by_manager_name'],
            'updated_by_manager_id' => (int)$row['updated_by_manager_id'],
            'updated_by_manager_name' => (string)$row['updated_by_manager_name'],
            'revision' => (int)$row['revision'],
            'message_id' => (int)($row['message_id'] ?? 0),
            'attempt_count' => (int)$row['attempt_count'],
            'last_error_code' => (string)$row['last_error_code'],
            'last_error' => (string)$row['last_error'],
            'can_manage' => true,
        ];
    }

    private function displayDate(string $sql): string {
        $parts = date_parse_from_format('Y-m-d H:i:s', $sql);
        if (!is_array($parts) || !empty($parts['errors'])) {
            return '';
        }
        return sprintf('%02d.%02d.%04d %02d:%02d', $parts['day'], $parts['month'], $parts['year'], $parts['hour'], $parts['minute']);
    }

    private function notifyScheduleChangedByChatId(int $chat_id): void {
        $chat = (new Session($this->registry))->getSessionById($chat_id);
        if ($chat) {
            $this->notifyScheduleChanged($chat);
        }
    }

    private function notifyScheduleChanged(array $chat): void {
        try {
            (new AndroidPushNotifier($this->registry))->notifySyncChat(
                (int)$chat['chat_id'],
                (string)($chat['source_provider'] ?? 'widget'),
                (string)($chat['customer_name'] ?? ''),
                strtoupper((string)($chat['source_provider'] ?? 'widget'))
            );
        } catch (\Throwable $ignored) {
        }
    }

    private function providerClosedMessage(string $provider, array $reply): string {
        if ($provider === 'ozon') {
            return 'Ozon закрыл диалог или больше не разрешает ответ. Измените время либо отмените сообщение.';
        }
        if ($provider === 'wildberries') {
            return 'Wildberries больше не разрешает ответ в этом диалоге. Проверьте чат и повторите отправку.';
        }
        if ($provider === 'yandex_market') {
            return 'Яндекс Маркет закрыл диалог или больше не разрешает ответ. Измените время либо отмените сообщение.';
        }
        return (string)($reply['message'] ?? 'Канал больше не разрешает отправку.');
    }

    private function conflict(string $public_id): array {
        $fresh = $this->model->findByPublicId($public_id);
        return $this->fail('scheduled_message_conflict', 'Сообщение уже изменил другой оператор. Данные обновлены.', 409, [
            'current' => $fresh ? $this->payload($fresh) : null,
        ]);
    }

    private function fail(string $code, string $message, int $http_code = 422, array $extra = []): array {
        return array_merge([
            'success' => false,
            'code' => $code,
            'error' => $message,
            'http_code' => $http_code,
        ], $extra);
    }
}
