<?php

declare(strict_types=1);

namespace NoahVet\Reef\Security\Policy;

use NoahVet\Reef\Exception\IAMException;
use NoahVet\Reef\Factory\ClientFactory;
use NoahVet\Reef\Jane\Client;
use NoahVet\Reef\Jane\Endpoint\PostPolicyResultCollection;
use NoahVet\Reef\Jane\Model\PolicyResultPolicyResultGet;
use NoahVet\Reef\Security\Authentication\BearerClientHMacComputer;
use NoahVet\Reef\Security\IAM\Transformer\PolicyResultTransformer;

class PolicyManager
{
    protected Client $client;

    /**
     * @var iterable<PolicyInterface<PolicySubjectInterface>>
     */
    protected iterable $policies;

    protected PolicyResultTransformer $policyResultTransformer;

    protected string $bearer;

    /**
     * @var array<string, string>
     */
    protected array $hmacHeaders;

    /**
     * @param iterable<PolicyInterface<PolicySubjectInterface>> $policies
     */
    public function __construct(
        string $bearer,
        BearerClientHMacComputer $bearerClientHMacComputer,
        ClientFactory $clientFactory,
        iterable $policies,
        PolicyResultTransformer $policyResultTransformer,
    ) {
        $this->bearer = $bearer;
        $this->client = $clientFactory->create($bearer);
        $this->hmacHeaders = $bearerClientHMacComputer->computeClientHMacHeader($bearer);
        $this->policies = $policies;
        $this->policyResultTransformer = $policyResultTransformer;
    }

    /**
     * Forcefully compute all policies for a subject.
     *
     * @return array<PolicyResult>
     */
    public function executeAllPolicies(mixed $subject): array
    {
        $results = [];

        foreach ($this->policies as $policy) {
            if ($policy->canHandle($subject)) {
                $results[] = $policy->apply($subject);
            }
        }

        return $results;
    }

    /**
     * @param PolicyInterface<PolicySubjectInterface> $policy
     */
    public function getIAMEtag(PolicyInterface $policy, mixed $subject): ?string
    {
        $subjectStr = $policy->subjectToString($subject);

        /** @var PolicyResultPolicyResultGet[] $results */
        $results = $this->client->getPolicyResultCollection(
            [
                'name' => $policy->getName(),
                'subject' => $subjectStr,
            ],
            $this->hmacHeaders,
        );

        if (1 === \count($results)) {
            return $results[0]->getEtag();
        }

        return null;
    }

    /**
     * Ensure everything is synced with the IAM about a list of resources.
     *
     * @param mixed[] $subjects
     */
    public function batchSyncPolicies(array $subjects): void
    {
        /** @var array<string, array{policy: PolicyInterface<PolicySubjectInterface>, subjects: mixed[]}> $policies */
        $policies = [];

        // Find each policy needed, and all subjects applicable for each policy
        foreach ($subjects as $subject) {
            foreach ($this->getPolicies($subject) as $policy) {
                $policyName = $policy->getName();

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

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

        // Process each policy 1 by 1
        foreach ($policies as $policyDetail) {
            /** @var PolicyInterface<PolicySubjectInterface> $policy */
            $policy = $policyDetail['policy'];

            $this->batchSyncPolicy($policy, $policyDetail['subjects']);
        }
    }

    /**
     * Ensure everything is synced with the IAM about this resource.
     *
     * @see batchSyncPolicies
     */
    public function syncPolicies(mixed $subject): void
    {
        // For each policy, apply it only if it was updated since the last call
        foreach ($this->policies as $policy) {
            if (
                $policy->canHandle($subject)
                && $policy->computeEtag($subject) !== $this->getIAMEtag($policy, $subject)
            ) {
                $this->syncPolicyResult($policy->apply($subject));
            }
        }
    }

    public function syncPolicyResult(PolicyResult $result): void
    {
        // Sync each policy with the IAM
        $response = $this->client->executeRawEndpoint(
            new PostPolicyResultCollection(
                $this->policyResultTransformer->toWebservice($result),
                $this->hmacHeaders,
            ),
        );

        if (200 > $response->getStatusCode() || 300 <= $response->getStatusCode()) {
            throw new IAMException(
                'Result from IAM not parsable',
                $response->getStatusCode(),
                $response->getBody()->getContents(),
            );
        }
    }

    /**
     * @return list<PolicyInterface<PolicySubjectInterface>>
     */
    protected function getPolicies(mixed $subject): array
    {
        $handlers = [];

        foreach ($this->policies as $policy) {
            if ($policy->canHandle($subject)) {
                $handlers[] = $policy;
            }
        }

        return $handlers;
    }

    /**
     * @param PolicyInterface<PolicySubjectInterface> $policy
     * @param string[]                                $subjects
     *
     * @return array<string, string>
     */
    protected function fetchAllEtags(PolicyInterface $policy, array $subjects): array
    {
        $ret = [];

        for ($page = 1; true; ++$page) {
            /** @var PolicyResultPolicyResultGet[] $policyResults */
            $policyResults = $this->client->getPolicyResultCollection(
                [
                    'name' => $policy->getName(),
                    'subject' => $subjects,
                    'page' => $page,
                ],
            );

            if (empty($policyResults)) {
                break;
            }

            $ret = \array_reduce(
                $policyResults,
                function (array $carry, PolicyResultPolicyResultGet $policyResultGet): array {
                    $carry[$policyResultGet->getSubject()] = $policyResultGet->getEtag();

                    return $carry;
                },
                $ret,
            );
        }

        return $ret;
    }

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

                return $carry;
            },
            [],
        );

        $foundEtags = $this->fetchAllEtags($policy, \array_keys($subjectsEtags));

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

            // Update the policy for this item
            $this->syncPolicyResult($policy->apply($subjectEtag['subject']));
        }
    }
}
