<?php

declare(strict_types=1);

namespace App\Support\Diff;

final class RbacRoleDefinitionDiffBuilder
{
    private const ROLE_FIELD_ORDER = [
        'Role definition > Display name' => 100,
        'Role definition > Description' => 200,
        'Role definition > Role source' => 300,
        'Role definition > Permission blocks' => 400,
        'Role definition > Scope tag IDs' => 500,
    ];

    private const PERMISSION_FIELD_ORDER = [
        'Allowed actions' => 100,
        'Denied actions' => 200,
        'Conditions' => 300,
    ];

    public function __construct(
        private readonly DiffPresenter $presenter,
    ) {}

    /**
     * @param  array<string, mixed>  $payload
     */
    public function build(array $payload): DiffPresentation
    {
        $baseline = $this->normalizeSide(
            is_array($payload['baseline'] ?? null) ? $payload['baseline'] : [],
        );
        $current = $this->normalizeSide(
            is_array($payload['current'] ?? null) ? $payload['current'] : [],
        );
        $changedKeys = $this->changedKeys($payload);

        $presentation = $this->presenter->present(
            baseline: $baseline,
            current: $current,
            changedKeys: $changedKeys,
            labels: $this->labelsFor($baseline, $current, $changedKeys),
        );

        if ($presentation->rows === []) {
            return $presentation;
        }

        $rows = array_map(
            fn (DiffRow $row): DiffRow => $this->normalizeRow($row),
            $presentation->rows,
        );

        usort($rows, fn (DiffRow $left, DiffRow $right): int => $this->sortKey($left->key) <=> $this->sortKey($right->key));

        return new DiffPresentation(
            summary: DiffSummary::fromRows($rows, $presentation->summary->message),
            rows: $rows,
        );
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return list<string>
     */
    private function changedKeys(array $payload): array
    {
        $changedKeys = is_array($payload['changed_keys'] ?? null) ? $payload['changed_keys'] : [];

        return array_values(array_filter(
            $changedKeys,
            fn (mixed $key): bool => is_string($key) && trim($key) !== '',
        ));
    }

    /**
     * @param  array<string, mixed>  $side
     * @return array<string, mixed>
     */
    private function normalizeSide(array $side): array
    {
        $normalized = [];
        $rows = is_array($side['normalized'] ?? null) ? $side['normalized'] : [];

        foreach ($rows as $key => $value) {
            if (! is_string($key) || trim($key) === '') {
                continue;
            }

            $normalized[trim($key)] = $value;
        }

        if (! array_key_exists('Role definition > Role source', $normalized)) {
            $roleSource = $this->roleSourceLabel($side['is_built_in'] ?? null);

            if ($roleSource !== null) {
                $normalized['Role definition > Role source'] = $roleSource;
            }
        }

        if (
            ! array_key_exists('Role definition > Permission blocks', $normalized)
            && is_numeric($side['role_permission_count'] ?? null)
        ) {
            $normalized['Role definition > Permission blocks'] = (int) $side['role_permission_count'];
        }

        return $normalized;
    }

    private function roleSourceLabel(mixed $isBuiltIn): ?string
    {
        return match ($isBuiltIn) {
            true => 'Built-in',
            false => 'Custom',
            default => null,
        };
    }

    /**
     * @param  array<string, mixed>  $baseline
     * @param  array<string, mixed>  $current
     * @param  list<string>  $changedKeys
     * @return array<string, string>
     */
    private function labelsFor(array $baseline, array $current, array $changedKeys): array
    {
        $labels = [];

        foreach ([array_keys($baseline), array_keys($current), $changedKeys] as $sourceKeys) {
            foreach ($sourceKeys as $key) {
                if (! is_string($key) || trim($key) === '') {
                    continue;
                }

                $labels[$key] = $key;
            }
        }

        return $labels;
    }

    private function normalizeRow(DiffRow $row): DiffRow
    {
        $shouldRenderAsList = $row->isListLike && $this->isDesignatedListField($row->key);

        if ($row->isListLike === $shouldRenderAsList) {
            return $row;
        }

        return new DiffRow(
            key: $row->key,
            label: $row->label,
            status: $row->status,
            oldValue: $row->oldValue,
            newValue: $row->newValue,
            isListLike: $shouldRenderAsList,
            addedItems: $shouldRenderAsList ? $row->addedItems : [],
            removedItems: $shouldRenderAsList ? $row->removedItems : [],
            unchangedItems: $shouldRenderAsList ? $row->unchangedItems : [],
            meta: $row->meta,
        );
    }

    private function isDesignatedListField(string $key): bool
    {
        if ($key === 'Role definition > Scope tag IDs') {
            return true;
        }

        return preg_match('/^Permission block \d+ > (Allowed actions|Denied actions|Conditions)$/', $key) === 1;
    }

    /**
     * @return array{int, int, int, string}
     */
    private function sortKey(string $key): array
    {
        if (array_key_exists($key, self::ROLE_FIELD_ORDER)) {
            return [0, self::ROLE_FIELD_ORDER[$key], 0, mb_strtolower($key)];
        }

        if (preg_match('/^Role definition > (?P<label>.+)$/', $key, $matches) === 1) {
            return [0, 900, 0, mb_strtolower((string) ($matches['label'] ?? $key))];
        }

        if (preg_match('/^Permission block (?P<block>\d+) > (?P<label>.+)$/', $key, $matches) === 1) {
            return [
                1,
                (int) ($matches['block'] ?? 0),
                $this->permissionFieldOrder((string) ($matches['label'] ?? '')),
                mb_strtolower((string) ($matches['label'] ?? $key)),
            ];
        }

        return [2, 0, 0, mb_strtolower($key)];
    }

    private function permissionFieldOrder(string $label): int
    {
        return self::PERMISSION_FIELD_ORDER[$label] ?? 900;
    }
}
