<?php

declare(strict_types=1);

namespace App\Support\Governance\Controls;

use InvalidArgumentException;

final readonly class MicrosoftSubjectBinding
{
    /**
     * @param  list<string>  $signalKeys
     * @param  list<string>  $supportedContexts
     */
    public function __construct(
        public string $controlKey,
        public ?string $subjectFamilyKey,
        public ?string $workload,
        public array $signalKeys,
        public array $supportedContexts,
        public bool $primary = false,
        public ?string $notes = null,
    ) {
        if (trim($this->controlKey) === '') {
            throw new InvalidArgumentException('Microsoft subject bindings require a canonical control key.');
        }

        if ($this->subjectFamilyKey === null && $this->workload === null && $this->signalKeys === []) {
            throw new InvalidArgumentException(sprintf('Microsoft subject binding for [%s] requires at least one discriminator.', $this->controlKey));
        }

        if ($this->supportedContexts === []) {
            throw new InvalidArgumentException(sprintf('Microsoft subject binding for [%s] requires at least one supported context.', $this->controlKey));
        }
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public static function fromArray(string $controlKey, array $data): self
    {
        return new self(
            controlKey: $controlKey,
            subjectFamilyKey: self::optionalString($data['subject_family_key'] ?? null),
            workload: self::optionalString($data['workload'] ?? null),
            signalKeys: self::stringList($data['signal_keys'] ?? []),
            supportedContexts: self::stringList($data['supported_contexts'] ?? []),
            primary: (bool) ($data['primary'] ?? false),
            notes: self::optionalString($data['notes'] ?? null),
        );
    }

    public function supportsContext(string $consumerContext): bool
    {
        return in_array(trim($consumerContext), $this->supportedContexts, true);
    }

    public function matches(CanonicalControlResolutionRequest $request): bool
    {
        if ($request->provider !== 'microsoft') {
            return false;
        }

        if (! $this->supportsContext($request->consumerContext)) {
            return false;
        }

        if ($request->subjectFamilyKey !== null && $this->subjectFamilyKey !== $request->subjectFamilyKey) {
            return false;
        }

        if ($request->workload !== null && $this->workload !== $request->workload) {
            return false;
        }

        if ($request->signalKey !== null && ! in_array($request->signalKey, $this->signalKeys, true)) {
            return false;
        }

        return true;
    }

    /**
     * @return array{
     *     control_key: string,
     *     provider: string,
     *     subject_family_key: ?string,
     *     workload: ?string,
     *     signal_keys: list<string>,
     *     supported_contexts: list<string>,
     *     primary: bool,
     *     notes: ?string
     * }
     */
    public function toArray(): array
    {
        return [
            'control_key' => $this->controlKey,
            'provider' => 'microsoft',
            'subject_family_key' => $this->subjectFamilyKey,
            'workload' => $this->workload,
            'signal_keys' => $this->signalKeys,
            'supported_contexts' => $this->supportedContexts,
            'primary' => $this->primary,
            'notes' => $this->notes,
        ];
    }

    private static function optionalString(mixed $value): ?string
    {
        if (! is_string($value)) {
            return null;
        }

        $trimmed = trim($value);

        return $trimmed === '' ? null : $trimmed;
    }

    /**
     * @param  iterable<mixed>  $values
     * @return list<string>
     */
    private static function stringList(iterable $values): array
    {
        return collect($values)
            ->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
            ->map(static fn (string $value): string => trim($value))
            ->values()
            ->all();
    }
}
