<?php
/**
 * Compares global class settings to find near-duplicate classes.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Governance;

use LolaCoreForBricks\Index\IndexRepository;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\SettingsKeyGrammar;

/**
 * Detects classes with equivalent normalized settings above a similarity threshold.
 */
class ClassSimilarity
{
    public const DEFAULT_THRESHOLD = 90;

    /**
     * @var string[]
     */
    private const META_KEYS = ['modified', 'user', 'id', 'name', 'category'];

    public function __construct(
        private readonly ?IndexRepository $repository = null
    ) {
    }

    /**
     * @param array<int, array<string, mixed>> $classes
     * @return array<int, array<string, mixed>>
     */
    public function findDuplicateGroups(array $classes, float $threshold = self::DEFAULT_THRESHOLD): array
    {
        $threshold = max(1.0, min(100.0, $threshold));
        $groups    = [];
        $seen      = [];

        $count = count($classes);
        for ($i = 0; $i < $count; ++$i) {
            for ($j = $i + 1; $j < $count; ++$j) {
                $left  = $classes[$i];
                $right = $classes[$j];
                $pair  = $this->comparePair($left, $right);

                if ($pair['match_percent'] < $threshold) {
                    continue;
                }

                $groupKey = $this->groupKey((string) $left['id'], (string) $right['id']);
                if (isset($seen[$groupKey])) {
                    continue;
                }

                $seen[$groupKey] = true;
                $members         = [$left, $right];
                $recommended     = $this->recommendKeeper($members);

                $groups[] = [
                    'match_percent'        => $pair['match_percent'],
                    'classes'              => array_map([$this, 'summarizeClass'], $members),
                    'differences'          => $pair['differences'],
                    'recommended_to_keep'  => $this->summarizeClass($recommended),
                    'recommendation_reason'=> $this->recommendationReason($members, $recommended),
                ];
            }
        }

        usort(
            $groups,
            static fn (array $a, array $b): int => ($b['match_percent'] <=> $a['match_percent'])
        );

        return $groups;
    }

    /**
     * @param array<string, mixed> $left
     * @param array<string, mixed> $right
     * @return array{match_percent: float, differences: array<int, array<string, mixed>>}
     */
    public function comparePair(array $left, array $right): array
    {
        $leftSettings  = $this->normalizedSettings(is_array($left['settings'] ?? null) ? $left['settings'] : []);
        $rightSettings = $this->normalizedSettings(is_array($right['settings'] ?? null) ? $right['settings'] : []);

        $unionKeys = array_unique(array_merge(array_keys($leftSettings), array_keys($rightSettings)));
        if ($unionKeys === []) {
            return ['match_percent' => 100.0, 'differences' => []];
        }

        $matching = 0;
        $differences = [];

        foreach ($unionKeys as $key) {
            $leftValue  = $leftSettings[$key] ?? null;
            $rightValue = $rightSettings[$key] ?? null;

            if ($leftValue !== null && $rightValue !== null && $leftValue === $rightValue) {
                ++$matching;
                continue;
            }

            $differences[] = [
                'key'        => $key,
                'left_value' => $leftValue,
                'right_value'=> $rightValue,
            ];
        }

        return [
            'match_percent' => round(($matching / count($unionKeys)) * 100, 2),
            'differences'   => $differences,
        ];
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, string>
     */
    public function normalizedSettings(array $settings): array
    {
        $normalized = [];

        foreach ($settings as $key => $value) {
            if (!is_string($key) || in_array($key, self::META_KEYS, true)) {
                continue;
            }

            $canonical = $this->canonicalKey($key);
            $normalized[$canonical] = ValueNormalizer::normalize($value);
        }

        return $normalized;
    }

    /**
     * @param array<int, array<string, mixed>> $members
     * @return array<string, mixed>
     */
    private function recommendKeeper(array $members): array
    {
        usort(
            $members,
            function (array $a, array $b): int {
                $usageA = $this->usageCount((string) ($a['id'] ?? ''));
                $usageB = $this->usageCount((string) ($b['id'] ?? ''));
                if ($usageA !== $usageB) {
                    return $usageB <=> $usageA;
                }

                $modifiedA = (int) ($a['modified'] ?? PHP_INT_MAX);
                $modifiedB = (int) ($b['modified'] ?? PHP_INT_MAX);

                return $modifiedA <=> $modifiedB;
            }
        );

        return $members[0];
    }

    /**
     * @param array<int, array<string, mixed>> $members
     * @return array<string, mixed>
     */
    private function recommendationReason(array $members, array $recommended): array
    {
        $recommendedId = (string) ($recommended['id'] ?? '');
        $usage         = $this->usageCount($recommendedId);
        $maxUsage      = max(array_map(fn (array $class): int => $this->usageCount((string) ($class['id'] ?? '')), $members));

        if ($usage === $maxUsage && $usage > 0) {
            return [
                'primary' => 'most_usage',
                'detail'  => 'Keeps the class applied to the most elements in the index.',
            ];
        }

        if ($usage > 0) {
            return [
                'primary' => 'usage_and_age',
                'detail'  => 'Keeps the class with the strongest indexed usage, then the oldest modified timestamp.',
            ];
        }

        return [
            'primary' => 'oldest',
            'detail'  => 'No indexed usage yet; keeps the oldest class as the safer default.',
        ];
    }

    private function usageCount(string $classId): int
    {
        if ($classId === '') {
            return 0;
        }

        return count(($this->repository ?? new IndexRepository())->findByRef(BricksMetaKeys::REF_CLASS, $classId));
    }

    /**
     * @param array<string, mixed> $class
     * @return array<string, mixed>
     */
    private function summarizeClass(array $class): array
    {
        return [
            'id'          => (string) ($class['id'] ?? ''),
            'name'        => (string) ($class['name'] ?? ''),
            'category'    => (string) ($class['category'] ?? ''),
            'modified'    => isset($class['modified']) ? (int) $class['modified'] : null,
            'usage_count' => $this->usageCount((string) ($class['id'] ?? '')),
        ];
    }

    private function canonicalKey(string $key): string
    {
        $parsed = SettingsKeyGrammar::parse($key);
        if ($parsed['legacy']) {
            return SettingsKeyGrammar::compose(
                $parsed['base'],
                $parsed['breakpoint'],
                $parsed['state'],
                $parsed['variant']
            );
        }

        return $key;
    }

    private function groupKey(string $leftId, string $rightId): string
    {
        $ids = [$leftId, $rightId];
        sort($ids);

        return implode(':', $ids);
    }
}
