<?php

declare(strict_types=1);

namespace App\Domain\Theme;

use Illuminate\Support\Facades\File;

/**
 * Compares theme.json "slots" with standard catalog + Blade usage.
 *
 * @phpstan-type Report array{
 *     slug: string,
 *     declared: list<string>,
 *     scanned: list<string>,
 *     status: 'full'|'partial'|'undeclared'|'mismatch',
 *     missing_standard: list<string>,
 *     declared_but_unused: list<string>,
 *     used_but_undeclared: list<string>,
 *     label: string,
 * }
 */
final class ThemeSlotReport
{
    /**
     * @param  array<string, mixed>  $manifest
     * @return Report
     */
    public static function analyze(
        string $slug,
        array $manifest,
        string $themePath,
        ?string $fallbackViewsPath = null,
    ): array {
        $declared = self::normalizeDeclared($manifest['slots'] ?? null);
        $scanned = self::scanViewsWithFallback($themePath.'/views', $fallbackViewsPath);
        $standard = ThemeSlot::all();

        if ($declared === null) {
            return [
                'slug' => $slug,
                'declared' => [],
                'scanned' => $scanned,
                'status' => 'undeclared',
                'missing_standard' => $standard,
                'declared_but_unused' => [],
                'used_but_undeclared' => $scanned,
                'label' => __('admin.slots.status_undeclared'),
            ];
        }

        $missingStandard = array_values(array_diff($standard, $declared));
        $declaredButUnused = array_values(array_diff($declared, $scanned));
        $usedButUndeclared = array_values(array_diff($scanned, $declared));

        if ($missingStandard === [] && $declaredButUnused === [] && $usedButUndeclared === []) {
            $status = 'full';
            $label = __('admin.slots.status_full');
        } elseif ($missingStandard !== []) {
            $status = 'partial';
            $label = __('admin.slots.status_partial', ['count' => count($missingStandard)]);
        } else {
            $status = 'mismatch';
            $label = __('admin.slots.status_mismatch');
        }

        return [
            'slug' => $slug,
            'declared' => $declared,
            'scanned' => $scanned,
            'status' => $status,
            'missing_standard' => $missingStandard,
            'declared_but_unused' => $declaredButUnused,
            'used_but_undeclared' => $usedButUndeclared,
            'label' => $label,
        ];
    }

    /**
     * @return list<string>|null  null = key absent
     */
    public static function normalizeDeclared(mixed $slots): ?array
    {
        if ($slots === null) {
            return null;
        }

        if ($slots === '*' || $slots === 'all') {
            return ThemeSlot::all();
        }

        if (! is_array($slots)) {
            return [];
        }

        $out = [];
        foreach ($slots as $item) {
            if (! is_string($item) || $item === '') {
                continue;
            }
            if ($item === '*' || $item === 'all') {
                foreach (ThemeSlot::all() as $standard) {
                    $out[$standard] = true;
                }

                continue;
            }
            $out[$item] = true;
        }

        $list = array_keys($out);
        sort($list);

        return $list;
    }

    /** @return list<string> */
    public static function scanViewsWithFallback(string $viewsPath, ?string $fallbackViewsPath): array
    {
        $found = [];
        foreach (self::scanViews($viewsPath) as $slot) {
            $found[$slot] = true;
        }

        if ($fallbackViewsPath !== null && is_dir($fallbackViewsPath) && realpath($fallbackViewsPath) !== realpath($viewsPath)) {
            // Count slots from default views that this theme does not override.
            $ownFiles = self::relativeBladeFiles($viewsPath);
            foreach (array_keys(self::relativeBladeFiles($fallbackViewsPath)) as $relative) {
                if (isset($ownFiles[$relative])) {
                    continue;
                }
                $path = $fallbackViewsPath.'/'.$relative;
                foreach (self::extractSlotsFromFile($path) as $slot) {
                    $found[$slot] = true;
                }
            }
        }

        $list = array_keys($found);
        sort($list);

        return $list;
    }

    /** @return list<string> */
    public static function scanViews(string $viewsPath): array
    {
        if (! is_dir($viewsPath)) {
            return [];
        }

        $found = [];
        foreach (array_keys(self::relativeBladeFiles($viewsPath)) as $relative) {
            foreach (self::extractSlotsFromFile($viewsPath.'/'.$relative) as $slot) {
                $found[$slot] = true;
            }
        }

        $list = array_keys($found);
        sort($list);

        return $list;
    }

    /** @return array<string, true> relative path => true */
    private static function relativeBladeFiles(string $viewsPath): array
    {
        if (! is_dir($viewsPath)) {
            return [];
        }

        $out = [];
        $root = realpath($viewsPath) ?: $viewsPath;
        $root = rtrim(str_replace('\\', '/', $root), '/');

        foreach (File::allFiles($viewsPath) as $file) {
            if (! str_ends_with(strtolower($file->getFilename()), '.blade.php')) {
                continue;
            }

            $full = str_replace('\\', '/', $file->getPathname());
            $real = realpath($full) ?: $full;
            $real = str_replace('\\', '/', $real);
            $relative = str_starts_with($real, $root.'/')
                ? substr($real, strlen($root) + 1)
                : $file->getFilename();
            $out[$relative] = true;
        }

        return $out;
    }

    /** @return list<string> */
    private static function extractSlotsFromFile(string $path): array
    {
        if (! is_file($path)) {
            return [];
        }

        $contents = (string) file_get_contents($path);
        $found = [];

        if (preg_match_all("/@themeslot\\(\\s*['\"]([a-z0-9_]+)['\"]\\s*\\)/i", $contents, $m)) {
            foreach ($m[1] as $slot) {
                $found[$slot] = true;
            }
        }
        if (preg_match_all("/ThemeSlot::render\\(\\s*['\"]([a-z0-9_]+)['\"]\\s*\\)/i", $contents, $m2)) {
            foreach ($m2[1] as $slot) {
                $found[$slot] = true;
            }
        }
        // Fully-qualified calls: \App\Domain\Theme\ThemeSlot::render('sidebar')
        if (preg_match_all("/\\\\ThemeSlot::render\\(\\s*['\"]([a-z0-9_]+)['\"]\\s*\\)/i", $contents, $m3)) {
            foreach ($m3[1] as $slot) {
                $found[$slot] = true;
            }
        }

        return array_keys($found);
    }
}
