<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\Event\Streaming\Dlq;

use PpPhp\Core\Platform\Event\Streaming\Publisher\PublishRequest;
use PpPhp\Core\Platform\Event\Streaming\Publisher\PublisherManager;

final class StreamDlqRuntime
{
    private static ?self $instance = null;

    private StreamDlqCollection $messages;
    /** @var StreamDlqResult[] */
    private array $results = [];
    private StreamDlqStore $store;

    private function __construct(
        private readonly string $basePath,
        private readonly StreamRetryPolicy $policy = new StreamRetryPolicy()
    ) {
        $this->messages = new StreamDlqCollection();
        $this->store = new StreamDlqStore($basePath);
        $this->hydrate();
    }

    public static function instance(?string $basePath = null): self
    {
        if (self::$instance === null) {
            $basePath ??= self::detectBasePath();
            self::$instance = new self($basePath);
        }
        return self::$instance;
    }

    public static function resetInstance(?string $basePath = null, ?StreamRetryPolicy $policy = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath, $policy ?? StreamRetryPolicy::default());
        return self::$instance;
    }

    public function deadLetter(array $message, string $reason = 'delivery failed'): StreamDlqMessage
    {
        $dead = $this->messages->add(StreamDlqMessage::dead($message, $reason));
        $this->results[] = StreamDlqResult::create('dead_letter', true, 1, $reason, [$dead->toArray()]);
        $this->save();
        return $dead;
    }

    public function retry(?string $dlqId = null): StreamDlqResult
    {
        $targets = [];
        if ($dlqId !== null && $dlqId !== '') {
            $message = $this->messages->get($dlqId);
            if ($message !== null) {
                $targets[] = $message;
            }
        } else {
            $targets = $this->messages->active();
        }

        $affected = 0;
        $items = [];
        foreach ($targets as $message) {
            if ($message->attempts() >= $this->policy->maxAttempts()) {
                continue;
            }

            $pending = $message->markRetryPending($this->policy);
            $this->messages->put($pending);
            $affected++;
            $items[] = $pending->toArray();
        }

        $result = StreamDlqResult::create('retry', true, $affected, 'retry scheduled', $items);
        $this->results[] = $result;
        $this->save();
        return $result;
    }

    public function replay(?string $dlqId = null): StreamDlqResult
    {
        $targets = [];
        if ($dlqId !== null && $dlqId !== '') {
            $message = $this->messages->get($dlqId);
            if ($message !== null) {
                $targets[] = $message;
            }
        } else {
            $targets = $this->messages->active();
        }

        $publisher = PublisherManager::instance($this->basePath);
        $affected = 0;
        $items = [];

        foreach ($targets as $message) {
            if ($message->status() === StreamDlqStatus::CLEARED) {
                continue;
            }

            $result = $publisher->publish(PublishRequest::create(
                'dlq.replay',
                $message->topic(),
                $message->type(),
                $message->payload(),
                $message->headers() + ['dlqId' => $message->dlqId(), 'replayedFromDlq' => true]
            ));

            if ($result->published()) {
                $replayed = $message->markReplayed();
                $this->messages->put($replayed);
                $affected++;
                $items[] = $replayed->toArray();
            }
        }

        $result = StreamDlqResult::create('replay', true, $affected, 'replayed from dlq', $items);
        $this->results[] = $result;
        $this->save();
        return $result;
    }

    public function clear(): StreamDlqResult
    {
        $affected = $this->messages->clear();
        $result = StreamDlqResult::create('clear', true, $affected, 'dlq cleared');
        $this->results[] = $result;
        $this->save();
        return $result;
    }

    public function hydrate(): void
    {
        $snapshot = $this->store->load();
        if ($snapshot->messages() !== []) {
            $this->messages = StreamDlqCollection::fromArray($snapshot->messages());
        }

        $this->results = [];
        foreach ($snapshot->results() as $result) {
            if (is_array($result)) {
                $this->results[] = StreamDlqResult::fromArray($result);
            }
        }
    }

    public function snapshot(): StreamDlqSnapshot
    {
        $messages = $this->messages->toArray();
        $results = array_map(static fn (StreamDlqResult $result): array => $result->toArray(), $this->results);

        return new StreamDlqSnapshot(
            $messages,
            $results,
            StreamDlqStatistics::from($messages, $results),
            $this->policy->toArray(),
            date('c')
        );
    }

    public function save(): StreamDlqSnapshot
    {
        return $this->store->save($this->snapshot());
    }

    public function reset(): void
    {
        $this->messages = new StreamDlqCollection();
        $this->results = [];
        $this->store->reset();
    }

    public function store(): StreamDlqStore { return $this->store; }

    public function summary(): array
    {
        $snapshot = $this->snapshot();
        return [
            'messages' => $snapshot->messages(),
            'results' => $snapshot->results(),
            'statistics' => $snapshot->statistics(),
            'persisted' => $this->store->exists(),
            'stateFile' => $this->store->stateFile(),
        ];
    }

    private static function detectBasePath(): string
    {
        $candidate = dirname(__DIR__, 5);
        $real = realpath($candidate);
        return $real !== false ? $real : $candidate;
    }
}
