<?php

namespace App\Services;

use App\Models\SiteSyncOutbox;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class MansionSiteSyncService
{
    private const OPERATION = 'edit_mansion';

    private $companyApiService;

    public function __construct(CompanyApiService $companyApiService)
    {
        $this->companyApiService = $companyApiService;
    }

    public function sendOrQueue(int $companyId, int $buildingId, array $payload): SiteSyncResult
    {
        $deliveryToken = Str::random(40);
        $this->registerDelivery($companyId, $buildingId, $payload, $deliveryToken);

        return $this->deliver($companyId, $buildingId, $payload, $deliveryToken);
    }

    public function sendOrQueueIfIdle(int $companyId, int $buildingId, array $payload): SiteSyncResult
    {
        $deliveryToken = Str::random(40);
        if (!$this->registerDeliveryIfIdle($companyId, $buildingId, $payload, $deliveryToken)) {
            return SiteSyncResult::failure(null, 'Site synchronization is already processing.');
        }

        return $this->deliver($companyId, $buildingId, $payload, $deliveryToken);
    }

    private function deliver(
        int $companyId,
        int $buildingId,
        array $payload,
        string $deliveryToken
    ): SiteSyncResult {
        $result = $this->companyApiService->editMansion($buildingId, $payload);

        if ($result->isSuccessful()) {
            $this->markSucceeded($companyId, $buildingId, $deliveryToken);

            return $result;
        }

        $this->queueFailure($companyId, $buildingId, $deliveryToken, $result);

        return $result;
    }

    private function registerDeliveryIfIdle(
        int $companyId,
        int $buildingId,
        array $payload,
        string $deliveryToken
    ): bool {
        return DB::transaction(function () use ($companyId, $buildingId, $payload, $deliveryToken) {
            $outbox = SiteSyncOutbox::where([
                'company_id' => $companyId,
                'building_id' => $buildingId,
                'operation' => self::OPERATION,
            ])->lockForUpdate()->first();

            if (!is_null($outbox) && $outbox->status === SiteSyncOutbox::STATUS_PROCESSING) {
                return false;
            }
            if (is_null($outbox)) {
                $outbox = new SiteSyncOutbox([
                    'company_id' => $companyId,
                    'building_id' => $buildingId,
                    'operation' => self::OPERATION,
                ]);
            }
            $outbox->fill([
                'delivery_token' => $deliveryToken,
                'payload' => $payload,
                'status' => SiteSyncOutbox::STATUS_PROCESSING,
                'retry_count' => 0,
                'last_error' => null,
                'next_retry_at' => null,
                'processing_started_at' => now(),
                'synced_at' => null,
            ])->save();

            return true;
        });
    }

    private function registerDelivery(
        int $companyId,
        int $buildingId,
        array $payload,
        string $deliveryToken
    ): void {
        $connection = DB::connection();
        $bindings = [
            $companyId,
            $buildingId,
            self::OPERATION,
            $deliveryToken,
            json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            SiteSyncOutbox::STATUS_PROCESSING,
            0,
            null,
            null,
            now(),
            null,
            now(),
            now(),
        ];

        if ($connection->getDriverName() === 'mysql') {
            $connection->statement(
                'insert into `site_sync_outboxes` '
                . '(`company_id`, `building_id`, `operation`, `delivery_token`, `payload`, `status`, `retry_count`, `last_error`, `next_retry_at`, `processing_started_at`, `synced_at`, `created_at`, `updated_at`) '
                . 'values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
                . 'on duplicate key update `delivery_token` = values(`delivery_token`), `payload` = values(`payload`), '
                . '`status` = values(`status`), `retry_count` = 0, `last_error` = null, '
                . '`next_retry_at` = null, `processing_started_at` = values(`processing_started_at`), '
                . '`synced_at` = null, `updated_at` = values(`updated_at`)',
                $bindings
            );

            return;
        }

        $connection->statement(
            'insert into site_sync_outboxes '
            . '(company_id, building_id, operation, delivery_token, payload, status, retry_count, last_error, next_retry_at, processing_started_at, synced_at, created_at, updated_at) '
            . 'values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
            . 'on conflict(company_id, building_id, operation) do update set delivery_token = excluded.delivery_token, '
            . 'payload = excluded.payload, status = excluded.status, retry_count = 0, last_error = null, '
            . 'next_retry_at = null, processing_started_at = excluded.processing_started_at, '
            . 'synced_at = null, updated_at = excluded.updated_at',
            $bindings
        );
    }

    private function markSucceeded(int $companyId, int $buildingId, string $deliveryToken): void
    {
        DB::table('site_sync_outboxes')->where([
            'company_id' => $companyId,
            'building_id' => $buildingId,
            'operation' => self::OPERATION,
            'delivery_token' => $deliveryToken,
        ])->update([
            'status' => SiteSyncOutbox::STATUS_SUCCEEDED,
            'last_error' => null,
            'next_retry_at' => null,
            'processing_started_at' => null,
            'synced_at' => now(),
            'updated_at' => now(),
        ]);
    }

    private function queueFailure(
        int $companyId,
        int $buildingId,
        string $deliveryToken,
        SiteSyncResult $result
    ): void {
        DB::table('site_sync_outboxes')->where([
            'company_id' => $companyId,
            'building_id' => $buildingId,
            'operation' => self::OPERATION,
            'delivery_token' => $deliveryToken,
        ])->update([
            'status' => SiteSyncOutbox::STATUS_PENDING,
            'retry_count' => 0,
            'last_error' => $this->errorSummary($result),
            'next_retry_at' => now()->addMinute(),
            'processing_started_at' => null,
            'synced_at' => null,
            'updated_at' => now(),
        ]);
    }

    private function errorSummary(SiteSyncResult $result): string
    {
        $statusCode = $result->statusCode();

        if (is_null($statusCode)) {
            return 'Site synchronization failed without an HTTP response.';
        }

        return 'Site synchronization failed with HTTP status ' . $statusCode . '.';
    }
}
