<?php
declare(strict_types=1);

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

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 DistributedScheduler
{
    private static ?self $instance = null;

    private DistributedSchedulerStore $store;
    /** @var array<string, DistributedScheduleAssignment> */
    private array $assignments = [];

    private function __construct(
        private readonly string $basePath,
        private readonly DistributedSchedulePolicy $policy = new DistributedSchedulePolicy()
    ) {
        $this->store = new DistributedSchedulerStore($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, ?DistributedSchedulePolicy $policy = null): self
    {
        $basePath ??= self::detectBasePath();
        self::$instance = new self($basePath, $policy ?? DistributedSchedulePolicy::default());

        return self::$instance;
    }

    public function schedule(DistributedScheduleJob $job): DistributedScheduleAssignment
    {
        $runtime = DistributedRuntimeCoordinator::instance($this->basePath);
        $selector = new DistributedWorkerSelector($runtime, $this->policy);
        $worker = $selector->select($job);

        if ($worker === null) {
            $assignment = DistributedScheduleAssignment::failed($job, 'no available worker');
            $this->assignments[$assignment->assignmentId()] = $assignment;
            $this->save();

            return $assignment;
        }

        $lease = DistributedLeaseManager::instance($this->basePath)->acquire(
            $job->queueName() . ':' . $job->jobId(),
            (string) ($worker['nodeId'] ?? ''),
            (string) ($worker['workerId'] ?? ''),
            $this->policy->defaultLeaseTtlSeconds()
        );

        if ($lease === null) {
            $assignment = DistributedScheduleAssignment::failed($job, 'lease denied');
            $this->assignments[$assignment->assignmentId()] = $assignment;
            $this->save();

            return $assignment;
        }

        $assignment = DistributedScheduleAssignment::assigned($job, $worker, $lease->leaseId());
        $this->assignments[$assignment->assignmentId()] = $assignment;
        $this->save();

        return $assignment;
    }

    public function rebalance(): array
    {
        return (new DistributedQueueBalancer(DistributedQueueCoordinator::instance($this->basePath)))->rebalance();
    }

    public function hydrate(): void
    {
        $snapshot = $this->store->load();

        $this->assignments = [];
        foreach ($snapshot->assignments() as $assignment) {
            if (is_array($assignment)) {
                $record = DistributedScheduleAssignment::fromArray($assignment);
                $this->assignments[$record->assignmentId()] = $record;
            }
        }
    }

    public function snapshot(): DistributedSchedulerSnapshot
    {
        $assignments = array_map(static fn (DistributedScheduleAssignment $assignment): array => $assignment->toArray(), array_values($this->assignments));

        return new DistributedSchedulerSnapshot($assignments, $this->statistics($assignments), $this->policy->toArray(), date('c'));
    }

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

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

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

    public function summary(): array
    {
        $snapshot = $this->snapshot();

        return [
            'assignments' => $snapshot->assignments(),
            'statistics' => $snapshot->statistics(),
            'policy' => $snapshot->policy(),
            'persisted' => $this->store->exists(),
            'stateFile' => $this->store->stateFile(),
        ];
    }

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

        foreach ($assignments as $assignment) {
            if (($assignment['assigned'] ?? false) === true) {
                $stats['assigned']++;
            } else {
                $stats['failed']++;
            }
        }

        return $stats;
    }

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

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