<?php

namespace App\Console\Commands;

use App\Models\SiteSyncOutbox;
use App\Services\CompanyApiService;
use App\Services\SiteSyncResult;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class RetryMansionSiteSync extends Command
{
    protected $signature = 'site-sync:retry-mansions {--limit=50}';

    protected $description = 'Retry due mansion synchronization deliveries';

    private $companyApiService;

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

    public function handle(): int
    {
        $limit = max(1, min(500, (int) $this->option('limit')));
        $ids = SiteSyncOutbox::where('operation', 'edit_mansion')
            ->where(function ($query) {
                $query->where(function ($pending) {
                    $pending->where('status', SiteSyncOutbox::STATUS_PENDING)
                        ->whereNotNull('next_retry_at')
                        ->where('next_retry_at', '<=', now());
                })->orWhere(function ($processing) {
                    $processing->where('status', SiteSyncOutbox::STATUS_PROCESSING)
                        ->whereNotNull('processing_started_at')
                        ->where('processing_started_at', '<=', now()->subMinutes(5));
                });
            })
            ->orderBy('id')
            ->limit($limit)
            ->pluck('id');

        foreach ($ids as $id) {
            $delivery = $this->claim((int) $id);
            if (is_null($delivery)) {
                continue;
            }

            try {
                $result = $this->companyApiService
                    ->forCompany($delivery['company_id'])
                    ->editMansion($delivery['building_id'], $delivery['payload']);
            } catch (\Throwable $exception) {
                $result = SiteSyncResult::failure(null, 'Site synchronization failed.');
            }
            $this->finalize($delivery, $result);
        }

        return 0;
    }

    private function claim(int $id): ?array
    {
        return DB::transaction(function () use ($id) {
            $outbox = SiteSyncOutbox::where('id', $id)
                ->where(function ($query) {
                    $query->where(function ($pending) {
                        $pending->where('status', SiteSyncOutbox::STATUS_PENDING)
                            ->whereNotNull('next_retry_at')
                            ->where('next_retry_at', '<=', now());
                    })->orWhere(function ($processing) {
                        $processing->where('status', SiteSyncOutbox::STATUS_PROCESSING)
                            ->whereNotNull('processing_started_at')
                            ->where('processing_started_at', '<=', now()->subMinutes(5));
                    });
                })
                ->lockForUpdate()
                ->first();

            if (is_null($outbox)) {
                return null;
            }

            $token = Str::random(40);
            $outbox->fill([
                'status' => SiteSyncOutbox::STATUS_PROCESSING,
                'delivery_token' => $token,
                'processing_started_at' => now(),
            ])->save();

            return [
                'id' => (int) $outbox->id,
                'company_id' => (int) $outbox->company_id,
                'building_id' => (int) $outbox->building_id,
                'payload' => $outbox->payload,
                'retry_count' => (int) $outbox->retry_count,
                'delivery_token' => $token,
            ];
        });
    }

    private function finalize(array $delivery, SiteSyncResult $result): void
    {
        $query = DB::table('site_sync_outboxes')->where([
            'id' => $delivery['id'],
            'company_id' => $delivery['company_id'],
            'building_id' => $delivery['building_id'],
            'operation' => 'edit_mansion',
            'delivery_token' => $delivery['delivery_token'],
            'status' => SiteSyncOutbox::STATUS_PROCESSING,
        ]);

        if ($result->isSuccessful()) {
            $query->update([
                'status' => SiteSyncOutbox::STATUS_SUCCEEDED,
                'last_error' => null,
                'next_retry_at' => null,
                'processing_started_at' => null,
                'synced_at' => now(),
                'updated_at' => now(),
            ]);

            return;
        }

        $retryCount = $delivery['retry_count'] + 1;
        $query->update([
            'status' => $retryCount >= 3
                ? SiteSyncOutbox::STATUS_FAILED
                : SiteSyncOutbox::STATUS_PENDING,
            'retry_count' => $retryCount,
            'last_error' => $this->errorSummary($result),
            'next_retry_at' => $this->nextRetryAt($retryCount),
            'processing_started_at' => null,
            'synced_at' => null,
            'updated_at' => now(),
        ]);
    }

    private function nextRetryAt(int $retryCount)
    {
        if ($retryCount >= 3) {
            return null;
        }

        return now()->addMinutes($retryCount === 1 ? 5 : 15);
    }

    private function errorSummary(SiteSyncResult $result): string
    {
        if (is_null($result->statusCode())) {
            return 'Site synchronization failed without an HTTP response.';
        }

        return 'Site synchronization failed with HTTP status ' . $result->statusCode() . '.';
    }
}
