<?php

namespace NoahVet\Reef\Application\Security\UseCase;

use NoahVet\Reef\Application\Security\Service\PolicyOrchestratorInterface;
use NoahVet\Reef\Domain\Security\Policy\PolicyInterface;
use NoahVet\Reef\Domain\Security\Port\AsyncPolicyPublisherInterface;
use NoahVet\Reef\Domain\Security\Port\PolicyResultPublisherInterface;
use NoahVet\Reef\Domain\Security\Port\PolicyResultStoreInterface;

/**
 * Syncs all applicable policies for a batch of subjects.
 *
 * For each policy, fetches all stored etags in a single call, then only publishes
 * subjects whose etag has changed. If an AsyncPolicyPublisherInterface is provided,
 * results are enqueued and flushed at the end rather than published synchronously.
 */
class BatchSyncPoliciesUseCase implements BatchSyncPoliciesUseCaseInterface
{
    public function __construct(
        private readonly PolicyOrchestratorInterface $orchestrator,
        private readonly PolicyResultStoreInterface $store,
        private readonly PolicyResultPublisherInterface|AsyncPolicyPublisherInterface $publisher,
    ) {
    }

    /**
     * @param object[] $subjects
     */
    #[\Override]
    public function batchSync(array $subjects): void
    {
        /** @var array<string, array{policy: PolicyInterface<object>, subjects: object[]}> $policiesMap */
        $policiesMap = [];

        foreach ($subjects as $subject) {
            foreach ($this->orchestrator->getPoliciesFor($subject) as $policy) {
                $policyName = $policy->getName();

                if (!isset($policiesMap[$policyName])) {
                    $policiesMap[$policyName] = ['policy' => $policy, 'subjects' => []];
                }

                $policiesMap[$policyName]['subjects'][] = $subject;
            }
        }

        foreach ($policiesMap as $policyDetail) {
            /** @var PolicyInterface<object> $policy */
            $policy = $policyDetail['policy'];
            $this->batchSyncPolicy($policy, $policyDetail['subjects']);
        }

        if ($this->publisher instanceof AsyncPolicyPublisherInterface) {
            $this->publisher->flush();
        }
    }

    /**
     * @param PolicyInterface<object> $policy
     * @param object[]                $subjects
     */
    private function batchSyncPolicy(PolicyInterface $policy, array $subjects): void
    {
        /** @var array<string, array{etag: string, subject: object}> $subjectsEtags */
        $subjectsEtags = \array_reduce(
            $subjects,
            static function (array $carry, object $subject) use ($policy): array {
                $carry[$policy->subjectToString($subject)] = [
                    'etag' => $policy->computeEtag($subject),
                    'subject' => $subject,
                ];

                return $carry;
            },
            [],
        );

        $foundEtags = $this->store->findEtags($policy->getName(), \array_keys($subjectsEtags));

        foreach ($subjectsEtags as $subjectName => $subjectEtag) {
            if (($foundEtags[$subjectName] ?? null) === $subjectEtag['etag']) {
                continue;
            }

            $result = $policy->apply($subjectEtag['subject']);

            if ($this->publisher instanceof AsyncPolicyPublisherInterface) {
                $this->publisher->enqueue($result);
            } else {
                $this->publisher->publish($result);
            }
        }
    }
}
