<?php

declare(strict_types=1);

namespace Maestro\Core\Services;

use Illuminate\Support\Facades\File;
use Maestro\Core\Support\HostStubManifest;
use Maestro\Core\Support\MaestroCoreVersions;
use Maestro\Core\Support\PackagePaths;

/**
 * Read-only host scaffolding and link-state probes for CLI and Superadmin.
 */
final class HostIntegrityService
{
    /**
     * @return array{
     *   checked_at: string,
     *   overall: 'ok'|'warn'|'fail',
     *   checks: list<array{
     *     id: string,
     *     label: string,
     *     status: 'ok'|'warn'|'fail',
     *     detail: string,
     *     fix_commands: list<string>
     *   }>
     * }
     */
    public function report(): array
    {
        $checks = [
            $this->checkManagedBanner(),
            $this->checkManagedHashes(),
            $this->checkCoreViewForks(),
            $this->checkCustomServiceProvider(),
            $this->checkBusinessOutsideCustom(),
            $this->checkBootJsDangerous(),
            $this->checkComposerNpmVersionAlignment(),
            $this->checkMaestroLinked(),
        ];

        return [
            'checked_at' => now()->toIso8601String(),
            'overall' => $this->rollupOverall($checks),
            'checks' => $checks,
        ];
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkManagedBanner(): array
    {
        $missing = [];
        $tampered = [];

        foreach (HostStubManifest::MANAGED_STUBS as $destination) {
            $path = base_path($destination);
            if (! File::exists($path)) {
                $missing[] = $destination;

                continue;
            }

            if (! str_contains(File::get($path), HostStubManifest::MANAGED_BANNER)) {
                $tampered[] = $destination;
            }
        }

        if ($missing !== []) {
            return $this->result(
                'managed-banner',
                'Managed shell files (@maestro-core-managed)',
                'fail',
                'Missing managed files: '.implode(', ', $missing),
                ['php artisan core:install --force'],
            );
        }

        if ($tampered !== []) {
            return $this->result(
                'managed-banner',
                'Managed shell files (@maestro-core-managed)',
                'fail',
                'Managed files missing the @maestro-core-managed banner (customised shells): '.implode(', ', $tampered),
                ['php artisan maestro:host-integrity --fix-managed', 'php artisan core:install --force'],
            );
        }

        return $this->result(
            'managed-banner',
            'Managed shell files (@maestro-core-managed)',
            'ok',
            sprintf('All %d managed shell files are present and marked.', count(HostStubManifest::MANAGED_STUBS)),
            [],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkManagedHashes(): array
    {
        $drifted = [];

        foreach (HostStubManifest::MANAGED_STUBS as $stubRelative => $destination) {
            $source = PackagePaths::stubPath($stubRelative);
            $target = base_path($destination);

            if (! File::exists($source) || ! File::exists($target)) {
                continue;
            }

            if (! str_contains(File::get($target), HostStubManifest::MANAGED_BANNER)) {
                continue;
            }

            if (hash_file('sha256', $source) !== hash_file('sha256', $target)) {
                $drifted[] = $destination;
            }
        }

        if ($drifted === []) {
            return $this->result(
                'managed-hash',
                'Managed shells match package stubs',
                'ok',
                'Managed files match the installed package stub hashes.',
                [],
            );
        }

        return $this->result(
            'managed-hash',
            'Managed shells match package stubs',
            'warn',
            'Managed files differ from package stubs (may need refresh): '.implode(', ', $drifted),
            ['php artisan core:install --force', 'rm -rf node_modules/.vite'],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkCoreViewForks(): array
    {
        $found = [];
        foreach (HostStubManifest::HOST_CORE_FORK_FILES as $relative) {
            if (File::exists(base_path($relative))) {
                $found[] = $relative;
            }
        }

        if ($found === []) {
            return $this->result(
                'core-view-forks',
                'No CORE view forks in host tree',
                'ok',
                'No copied Maestro CORE views detected under resources/js/maestro/.',
                [],
            );
        }

        return $this->result(
            'core-view-forks',
            'No CORE view forks in host tree',
            'fail',
            'Host CORE view forks detected: '.implode(', ', $found),
            ['Remove forked files; use @maestro/core package instead'],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkCustomServiceProvider(): array
    {
        $path = app_path('Providers/AppServiceProvider.php');
        if (! File::exists($path)) {
            return $this->result(
                'custom-service-provider',
                'CustomServiceProvider registered',
                'warn',
                'AppServiceProvider.php not found.',
                [],
            );
        }

        $contents = File::get($path);
        $registered = str_contains($contents, 'CustomServiceProvider::class');

        if ($registered) {
            return $this->result(
                'custom-service-provider',
                'CustomServiceProvider registered',
                'ok',
                'AppServiceProvider registers CustomServiceProvider.',
                [],
            );
        }

        return $this->result(
            'custom-service-provider',
            'CustomServiceProvider registered',
            'fail',
            'CustomServiceProvider is not registered in AppServiceProvider.',
            ['$this->app->register(\\App\\Providers\\CustomServiceProvider::class); in AppServiceProvider::register()'],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkBusinessOutsideCustom(): array
    {
        $appDir = app_path();
        if (! File::isDirectory($appDir)) {
            return $this->result(
                'business-outside-custom',
                'Business PHP under app/Custom only',
                'ok',
                'No app/ directory to scan.',
                [],
            );
        }

        $suspicious = [];
        $customPrefix = $appDir.DIRECTORY_SEPARATOR.'Custom'.DIRECTORY_SEPARATOR;

        foreach (File::allFiles($appDir) as $file) {
            $full = $file->getPathname();
            if (str_starts_with($full, $customPrefix)) {
                continue;
            }

            $relative = 'app/'.str_replace($appDir.DIRECTORY_SEPARATOR, '', $full);
            $relative = str_replace('\\', '/', $relative);

            if (! str_ends_with($relative, '.php')) {
                continue;
            }

            $suffix = str_replace('app/', '', $relative);
            if (in_array($suffix, HostStubManifest::HOST_BUSINESS_PHP_ALLOWLIST, true)) {
                continue;
            }

            $suspicious[] = $relative;
        }

        if ($suspicious === []) {
            return $this->result(
                'business-outside-custom',
                'Business PHP under app/Custom only',
                'ok',
                'No unexpected PHP files outside app/Custom/Features/.',
                [],
            );
        }

        return $this->result(
            'business-outside-custom',
            'Business PHP under app/Custom only',
            'warn',
            'PHP files outside app/Custom/Features/ (review or move): '.implode(', ', array_slice($suspicious, 0, 8)),
            ['php artisan make:custom-feature {Name}'],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkBootJsDangerous(): array
    {
        $path = base_path('resources/js/maestro/boot.js');
        if (! File::exists($path)) {
            return $this->result(
                'boot-js-dangerous',
                'boot.js route overrides',
                'ok',
                'boot.js not present yet.',
                [],
            );
        }

        $contents = File::get($path);
        if (! str_contains($contents, 'removeRoute')) {
            return $this->result(
                'boot-js-dangerous',
                'boot.js route overrides',
                'ok',
                'boot.js does not call removeRoute.',
                [],
            );
        }

        if (preg_match_all('/removeRoute\s*\(\s*[\'"]([^\'"]+)[\'"]/m', $contents, $matches)) {
            $routes = $matches[1];
            $disallowed = array_values(array_diff($routes, HostStubManifest::BOOT_JS_ALLOWED_REMOVE_ROUTES));
            if ($disallowed !== []) {
                return $this->result(
                    'boot-js-dangerous',
                    'boot.js route overrides',
                    'warn',
                    'boot.js removes CORE routes other than dashboard: '.implode(', ', $disallowed),
                    ['Only removeRoute("dashboard") is recommended unless documented'],
                );
            }
        }

        return $this->result(
            'boot-js-dangerous',
            'boot.js route overrides',
            'ok',
            'boot.js removeRoute usage is within allowed routes.',
            [],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function checkComposerNpmVersionAlignment(): array
    {
        $versions = MaestroCoreVersions::toArray();
        $composer = $versions['composer'];
        $npm = $versions['npm'];

        if ($composer === null || $npm === null) {
            return $this->result(
                'composer-npm-version',
                'Composer and npm package versions aligned',
                'warn',
                'Could not read maestro/core versions from vendor and node_modules.',
                ['composer install', 'npm install'],
            );
        }

        if ($composer === $npm) {
            return $this->result(
                'composer-npm-version',
                'Composer and npm package versions aligned',
                'ok',
                sprintf('maestro/core %s (Composer) matches @maestro/core %s (npm).', $composer, $npm),
                [],
            );
        }

        return $this->result(
            'composer-npm-version',
            'Composer and npm package versions aligned',
            'warn',
            sprintf('Version mismatch: maestro/core %s vs @maestro/core %s.', $composer, $npm),
            ['composer update maestro/core', 'npm update @maestro/core'],
        );
    }

    /**
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    public function checkMaestroLinked(): array
    {
        $reasons = [];

        if (File::exists(base_path(HostStubManifest::LINK_STATE_FILE))) {
            $reasons[] = HostStubManifest::LINK_STATE_FILE.' present';
        }

        if ($this->vendorMaestroCoreIsSymlink()) {
            $reasons[] = 'vendor/maestro/core is a symlink';
        }

        if ($this->composerLockHasPathDist()) {
            $reasons[] = 'composer.lock uses path dist for maestro/core';
        }

        if ($this->packageLockHasFileResolved()) {
            $reasons[] = 'package-lock.json resolves @maestro/core from file:';
        }

        if ($reasons === []) {
            return $this->result(
                'maestro-linked',
                'Production registry install (not locally linked)',
                'ok',
                'maestro/core is installed from registry lockfiles.',
                [],
            );
        }

        return $this->result(
            'maestro-linked',
            'Production registry install (not locally linked)',
            'warn',
            'Local Maestro link detected: '.implode('; ', $reasons),
            ['php artisan maestro:unlink'],
        );
    }

    public function vendorMaestroCoreIsSymlink(): bool
    {
        $path = base_path('vendor/maestro/core');
        if (! file_exists($path)) {
            return false;
        }

        return is_link($path);
    }

    public function composerLockHasPathDist(): bool
    {
        $lockPath = base_path('composer.lock');
        if (! File::exists($lockPath)) {
            return false;
        }

        $json = json_decode(File::get($lockPath), true);
        if (! is_array($json)) {
            return false;
        }

        foreach (['packages', 'packages-dev'] as $section) {
            $packages = $json[$section] ?? [];
            if (! is_array($packages)) {
                continue;
            }
            foreach ($packages as $package) {
                if (($package['name'] ?? '') !== 'maestro/core') {
                    continue;
                }
                $distType = $package['dist']['type'] ?? null;
                if ($distType === 'path') {
                    return true;
                }
            }
        }

        return false;
    }

    public function packageLockHasFileResolved(): bool
    {
        $lockPath = base_path('package-lock.json');
        if (! File::exists($lockPath)) {
            return false;
        }

        $contents = File::get($lockPath);

        return (bool) preg_match('/"node_modules\/@maestro\/core"[^}]*"resolved"\s*:\s*"file:/s', $contents);
    }

    /**
     * @param  list<array{status: string}>  $checks
     * @return 'ok'|'warn'|'fail'
     */
    private function rollupOverall(array $checks): string
    {
        $worst = 'ok';
        foreach ($checks as $check) {
            if ($check['status'] === 'fail') {
                return 'fail';
            }
            if ($check['status'] === 'warn') {
                $worst = 'warn';
            }
        }

        return $worst;
    }

    /**
     * @param  list<string>  $fixCommands
     * @return array{id: string, label: string, status: 'ok'|'warn'|'fail', detail: string, fix_commands: list<string>}
     */
    private function result(
        string $id,
        string $label,
        string $status,
        string $detail,
        array $fixCommands,
    ): array {
        return [
            'id' => $id,
            'label' => $label,
            'status' => $status,
            'detail' => $detail,
            'fix_commands' => $fixCommands,
        ];
    }
}
