<?php

declare(strict_types=1);

namespace Openguru\OpenCartFramework\TeleCartPulse;

use Carbon\Carbon;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
use Ramsey\Uuid\Uuid;
use RuntimeException;

class TeleCartEvent
{
    private const TABLE_NAME = 'telecart_events';

    private ConnectionInterface $database;
    private Builder $builder;

    public function __construct(ConnectionInterface $database, Builder $builder)
    {
        $this->database = $database;
        $this->builder = $builder;
    }

    /**
     * Найти запись по ID
     *
     * @param int $id ID записи
     * @return array|null Данные события или null если не найдено
     */
    public function findById(int $id): ?array
    {
        return $this->builder
            ->newQuery()
            ->select(['*'])
            ->from(self::TABLE_NAME)
            ->where('id', '=', $id)
            ->firstOrNull();
    }

    /**
     * Создать новое событие
     *
     * @param string $event Название события
     * @param array $payload Payload события (webapp и eventData)
     * @param string|null $idempotencyKey Ключ идемпотентности (если не передан - генерируется UUID)
     * @return int ID созданной записи
     * @throws RuntimeException Если не удалось создать запись
     */
    public function create(string $event, array $payload, ?string $idempotencyKey = null): int
    {
        // Если ключ не передан - генерируем UUID
        if ($idempotencyKey === null) {
            $idempotencyKey = $this->generateIdempotencyKey();
        }

        // Проверяем, существует ли уже событие с таким ключом идемпотентности
        $existing = $this->findByIdempotencyKey($idempotencyKey);
        if ($existing !== null) {
            // Возвращаем ID существующего события
            return (int) $existing['id'];
        }

        $eventTime = Carbon::now('UTC');
        $now = Carbon::now('UTC');
        $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $data = [
            'event' => $event,
            'payload' => $payloadJson,
            'idempotency_key' => $idempotencyKey,
            'event_time' => $eventTime->format('Y-m-d H:i:s'),
            'status' => 'pending',
            'attempts_count' => 0,
            'error_reason' => null,
            'created_at' => $now->format('Y-m-d H:i:s'),
            'updated_at' => $now->format('Y-m-d H:i:s'),
        ];

        $success = $this->database->insert(self::TABLE_NAME, $data);

        if (! $success) {
            $error = $this->database->getLastError();

            // Обработка race condition: если произошла ошибка уникальности,
            // значит событие было создано другим запросом, пытаемся найти его
            if ($error && isset($error[1]) && strpos($error[1], 'Duplicate entry') !== false) {
                $existing = $this->findByIdempotencyKey($idempotencyKey);
                if ($existing !== null) {
                    return (int) $existing['id'];
                }
            }

            $errorMessage = $error ? $error[1] : 'Unknown error';
            throw new RuntimeException("Failed to insert telecart event. Error: {$errorMessage}");
        }

        return $this->database->lastInsertId();
    }

    /**
     * Генерирует idempotency key как UUID
     *
     * @return string Idempotency key (UUID v4)
     */
    private function generateIdempotencyKey(): string
    {
        return Uuid::uuid4()->toString();
    }

    /**
     * Найти запись по idempotency key
     *
     * @param string $idempotencyKey Idempotency key
     * @return array|null Данные события или null если не найдено
     */
    public function findByIdempotencyKey(string $idempotencyKey): ?array
    {
        return $this->builder
            ->newQuery()
            ->select(['*'])
            ->from(self::TABLE_NAME)
            ->where('idempotency_key', '=', $idempotencyKey)
            ->firstOrNull();
    }

    /**
     * Обновить статус события
     *
     * @param int $id ID события
     * @param string $status Новый статус
     * @param string|null $errorReason Причина ошибки (если статус failed)
     * @return bool true если обновление успешно
     */
    public function updateStatus(int $id, string $status, ?string $errorReason = null): bool
    {
        $data = [
            'status' => $status,
        ];

        if ($errorReason !== null) {
            $data['error_reason'] = $errorReason;
        }

        return $this->updateById($id, $data);
    }

    /**
     * Увеличить счетчик попыток отправки
     *
     * @param int $id ID события
     * @return bool true если обновление успешно
     */
    public function incrementAttempts(int $id): bool
    {
        $now = Carbon::now('UTC')->format('Y-m-d H:i:s');
        $table = self::TABLE_NAME;
        $sql = "UPDATE `$table` SET `attempts_count` = `attempts_count` + 1, updated_at = ? WHERE id = ?";

        return $this->database->statement($sql, [$now, $id]);
    }

    /**
     * Обновить запись по ID
     *
     * @param int $id ID события
     * @param array $data Данные для обновления
     * @return bool true если обновление успешно
     */
    public function updateById(int $id, array $data): bool
    {
        // Автоматически обновляем updated_at, если он не указан явно
        if (!isset($data['updated_at'])) {
            $data['updated_at'] = Carbon::now('UTC')->format('Y-m-d H:i:s');
        }

        return $this->builder->newQuery()
            ->where('id', '=', $id)
            ->update(self::TABLE_NAME, $data);
    }

    /**
     * Найти события со статусом pending
     *
     * @param int $limit Лимит записей
     * @return array Массив событий
     */
    public function findPending(int $limit = 100): array
    {
        return $this->builder
            ->newQuery()
            ->select(['*'])
            ->from(self::TABLE_NAME)
            ->where('status', '=', 'pending')
            ->orderBy('event_time', 'ASC')
            ->limit($limit)
            ->get();
    }

    /**
     * Получить статистику событий по статусам
     *
     * @return array Статистика: pending, sent, failed
     */
    public function getStats(): array
    {
        $stats = $this->builder
            ->newQuery()
            ->select([
                'status',
                new RawExpression('COUNT(*) as count'),
            ])
            ->from(self::TABLE_NAME)
            ->groupBy(['status'])
            ->get();

        $result = [
            'pending' => 0,
            'sent' => 0,
            'failed' => 0,
        ];

        foreach ($stats as $stat) {
            $status = $stat['status'];
            if (isset($result[$status])) {
                $result[$status] = (int) $stat['count'];
            }
        }

        return $result;
    }
}
