<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\Event\Distributed\Failover;

use PpPhp\Core\Platform\Event\Distributed\DistributedRuntimeCoordinator;
use PpPhp\Core\Platform\Event\Distributed\Lease\DistributedLeaseManager;
use PpPhp\Core\Platform\Event\Distributed\Queue\DistributedQueueCoordinator;

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

    private DistributedFailoverStore $store;

    private function __construct(
        private readonly string $basePath,
        private readonly DistributedFailoverPolicy $policy = new DistributedFailoverPolicy()
    ) {
        $this->store = new DistributedFailoverStore($basePath);
    }

    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, ?DistributedFailoverPolicy $policy = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath, $policy ?? DistributedFailoverPolicy::default());

        return self::$instance;
    }

    public function inspect(): DistributedFailoverReport
    {
        $runtime = DistributedRuntimeCoordinator::instance($this->basePath);
        $nodes = (new DistributedNodeFailureDetector($this->policy))->inspect($runtime);
        $failedNodeIds = [];

        foreach ($nodes as $node) {
            if (($node['failed'] ?? false) === true) {
                $failedNodeIds[] = (string) $node['nodeId'];
            }
        }

        $leaseRecovery = (new DistributedLeaseRecoveryManager(
            DistributedLeaseManager::instance($this->basePath),
            $this->policy
        ))->recover($failedNodeIds);

        $queueRecovery = (new DistributedQueueRecoveryManager(
            DistributedQueueCoordinator::instance($this->basePath),
            $this->policy
        ))->recover();

        $actions = [];
        foreach ($failedNodeIds as $nodeId) {
            $actions[] = DistributedFailoverAction::nodeRecovered($nodeId)->toArray();
        }

        if ($leaseRecovery->recovered() > 0 || $leaseRecovery->expired() > 0) {
            $actions[] = DistributedFailoverAction::leaseRecovered($leaseRecovery->recovered())->toArray();
        }

        $queueRecoveryData = $queueRecovery->toArray();
        if (($queueRecoveryData['moves'] ?? 0) > 0) {
            $actions[] = DistributedFailoverAction::queueRebalanced((int) $queueRecoveryData['moves'])->toArray();
        }

        $report = new DistributedFailoverReport(
            $nodes,
            $actions,
            $leaseRecovery->toArray(),
            $queueRecovery->toArray(),
            $this->statistics($nodes, $actions),
            $this->policy->toArray(),
            date('c')
        );

        $this->store->save($report);

        return $report;
    }

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

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

    public function clear(): void
    {
        $this->store->reset();
    }

    public function summary(): array
    {
        $report = $this->store->exists() ? $this->load() : $this->inspect();

        return $report->toArray() + [
            'persisted' => $this->store->exists(),
            'stateFile' => $this->store->stateFile(),
        ];
    }

    private function statistics(array $nodes, array $actions): array
    {
        $stats = [
            'nodes' => count($nodes),
            'healthy' => 0,
            'failed' => 0,
            'actions' => count($actions),
        ];

        foreach ($nodes as $node) {
            if (($node['failed'] ?? false) === true) {
                $stats['failed']++;
            } else {
                $stats['healthy']++;
            }
        }

        return $stats;
    }

    private static function detectBasePath(): string
    {
        $candidate = dirname(__DIR__, 5);
        $real = realpath($candidate);

        return $real !== false ? $real : $candidate;
    }
}
