<?php

declare(strict_types=1);

namespace Maestro\Core\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Maestro\Core\Models\MaestroModule;
use Maestro\Core\Models\NavigationNode;

/**
 * Read-only probes for Superadmin installation checklist (no Artisan from HTTP).
 *
 * Status: ok satisfied, warn incomplete or informational, fail blocking misconfiguration.
 */
final class InstallationHealthService
{
    private const CUSTOMERS_SLUG = 'customers';

    private const HOURS_SLUG = 'hours';

    private const OFFERS_SLUG = 'offers';

    private const INVOICES_SLUG = 'invoices';

    private const ITEMS_SLUG = 'items';

    private const SEEDER_CMD = 'php artisan db:seed --class="Maestro\\Core\\Database\\Seeders\\ModuleAndNavigationSeeder"';

    /**
     * @return array{
     *   checked_at:string,
     *   overall:string,
     *   overall_core:string,
     *   maestro_core_versions:array{composer_package:?string,npm_package:?string},
     *   groups:list<array{id:string,title:string,steps:list<array<string,mixed>>}>
     * }
     */
    public function report(): array
    {
        $stepsCore = $this->coreSteps();
        $stepsCustomers = $this->customersSteps();
        $stepsHours = $this->hoursSteps();

        $stepsOffers = $this->offersSteps();

        $stepsInvoices = $this->invoicesSteps();

        $stepsItems = $this->itemsSteps();

        $groups = [
            [
                'id' => 'core',
                'title' => 'Database & CORE',
                'steps' => $stepsCore,
            ],
            [
                'id' => 'customers',
                'title' => 'Customers module',
                'steps' => $stepsCustomers,
            ],
            [
                'id' => self::HOURS_SLUG,
                'title' => 'Hour Registration module',
                'steps' => $stepsHours,
            ],
            [
                'id' => self::OFFERS_SLUG,
                'title' => 'Offers module',
                'steps' => $stepsOffers,
            ],
            [
                'id' => self::INVOICES_SLUG,
                'title' => 'Invoices module',
                'steps' => $stepsInvoices,
            ],
            [
                'id' => self::ITEMS_SLUG,
                'title' => 'Items catalog module',
                'steps' => $stepsItems,
            ],
        ];

        return [
            'checked_at' => now()->toIso8601String(),
            'overall' => $this->rollupOverall($groups),
            'overall_core' => $this->rollupOverall([['steps' => $stepsCore]]),
            'maestro_core_versions' => $this->maestroCorePackageVersions(),
            'groups' => $groups,
        ];
    }

    /**
     * @return array{composer_package: ?string, npm_package: ?string}
     */
    public function maestroCorePackageVersions(): array
    {
        return [
            'composer_package' => $this->readPackageJsonVersion(base_path('vendor/maestro/core/composer.json')),
            'npm_package' => $this->readPackageJsonVersion(base_path('node_modules/@maestro/core/package.json')),
        ];
    }

    private function readPackageJsonVersion(string $path): ?string
    {
        if (! is_readable($path)) {
            return null;
        }

        $raw = @file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        $json = json_decode($raw, true);
        if (! is_array($json) || ! isset($json['version']) || ! is_string($json['version'])) {
            return null;
        }

        return $json['version'];
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function coreSteps(): array
    {
        $steps = [];

        $hasMigrateTable = Schema::hasTable('migrations');

        $hasMaestroModules = Schema::hasTable('maestro_modules');
        $hasNavigationNodes = Schema::hasTable('maestro_navigation_nodes');

        $steps[] = $this->makeStep(
            'migrations-table',
            'Laravel migrations table',
            $hasMigrateTable
                ? ['ok', 'The `migrations` table exists.', []]
                : ['fail', 'Database may be unreachable or Laravel was never migrated.', ['php artisan migrate']],
        );

        $steps[] = $this->makeStep(
            'maestro-modules-table',
            'Maestro CORE module registry table',
            $hasMaestroModules
                ? ['ok', 'Table `maestro_modules` exists.', []]
                : ['fail', 'CORE package migrations are missing — feature toggles cannot work.', ['php artisan migrate']],
        );

        $steps[] = $this->makeStep(
            'navigation-nodes-table',
            'Navigation table',
            $hasNavigationNodes
                ? ['ok', 'Table `maestro_navigation_nodes` exists.', []]
                : ['fail', 'CORE navigation migrations are missing.', ['php artisan migrate']],
        );

        $rootExists = $hasNavigationNodes && NavigationNode::query()->where('key', 'root')->exists();

        $steps[] = $this->makeStep(
            'navigation-root',
            'Default navigation tree (root)',
            $rootExists
                ? ['ok', 'Navigation root row is present.', []]
                : [
                    $hasMigrateTable ? 'warn' : 'fail',
                    $hasMigrateTable
                        ? 'No root row yet. Menus may be created on first `/api/navigation` request, but seeding is recommended.'
                        : 'Run migrations first.',
                    $hasMigrateTable ? [self::SEEDER_CMD] : ['php artisan migrate'],
                ],
        );

        $moduleCount = $hasMaestroModules ? MaestroModule::query()->count() : 0;

        $steps[] = $this->makeStep(
            'maestro-modules-rows',
            'Registered feature modules',
            $moduleCount > 0
                ? ['ok', sprintf('Found %d module definition(s).', $moduleCount), []]
                : [
                    'warn',
                    'No rows in `maestro_modules`. Sidebar and `/api/user` module slugs usually need seeded definitions.',
                    $hasMigrateTable ? [self::SEEDER_CMD] : ['php artisan migrate'],
                ],
        );

        $writableBootstrap = is_writable(base_path('bootstrap/cache'));

        $steps[] = $this->makeStep(
            'writable-bootstrap-cache',
            'Writable bootstrap cache',
            $writableBootstrap
                ? ['ok', '`bootstrap/cache` is writable.', []]
                : [
                    'warn',
                    '`bootstrap/cache` is not writable — Laravel may fail to cache config or routes.',
                    [],
                ],
        );

        return $steps;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function customersSteps(): array
    {
        $steps = [];

        $customersInConfig = config('core.modules.customers', true) === true;

        $hasMaestroModules = Schema::hasTable('maestro_modules');
        $hasCustomersTable = Schema::hasTable('customers');

        $customersModuleRow = $hasMaestroModules
            ? MaestroModule::query()->where('slug', self::CUSTOMERS_SLUG)->first()
            : null;

        if (! $customersInConfig) {
            $steps[] = $this->makeStep(
                'customers-config-off',
                'Customers module (config)',
                [
                    'ok',
                    '`core.modules.customers` is false — the Customers PHP module is not loaded for this deployment.',
                    [],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'customers-config-on',
            'Customers module (config)',
            [
                'ok',
                '`core.modules.customers` is enabled.',
                [],
            ],
        );

        if (! $hasCustomersTable) {
            $steps[] = $this->makeStep(
                'customers-schema',
                'Customers database tables',
                [
                    'fail',
                    '`customers` table is missing while the module is enabled in config.',
                    ['php artisan migrate'],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'customers-schema',
            'Customers database tables',
            ['ok', 'Core `customers` table exists.', []],
        );

        if ($customersModuleRow === null) {
            $steps[] = $this->makeStep(
                'customers-module-row',
                'Customers runtime toggle row',
                [
                    'warn',
                    '`maestro_modules` has no row for slug `customers`.',
                    [self::SEEDER_CMD],
                ],
            );
        } elseif (! $customersModuleRow->is_enabled) {
            $steps[] = $this->makeStep(
                'customers-runtime-off',
                'Customers enabled for users',
                [
                    'warn',
                    'The Customers module exists but `is_enabled` is false — normal users have no Customers access.',
                    [],
                ],
            );
        } else {
            $steps[] = $this->makeStep(
                'customers-runtime-on',
                'Customers enabled for users',
                ['ok', 'Customers row exists and is enabled for runtime.', []],
            );
        }

        return $steps;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function hoursSteps(): array
    {
        $steps = [];

        $hoursInConfig = config('core.modules.hours', true) === true;

        if (! $hoursInConfig) {
            $steps[] = $this->makeStep(
                'hours-config-off',
                'Hour Registration module (config)',
                ['ok', '`core.modules.hours` is false — the Hour Registration PHP module is not loaded.', []],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'hours-config-on',
            'Hour Registration module (config)',
            ['ok', '`core.modules.hours` is enabled.', []],
        );

        $hasMaestroModules = Schema::hasTable('maestro_modules');
        $hoursModuleRow = $hasMaestroModules
            ? MaestroModule::query()->where('slug', self::HOURS_SLUG)->first()
            : null;

        $hasCats = Schema::hasTable('hour_internal_categories');

        if (! $hasCats) {
            $steps[] = $this->makeStep(
                'hours-schema',
                'Hour Registration database tables',
                [
                    'fail',
                    '`hour_internal_categories` (or related tables) missing while Hours is enabled.',
                    ['php artisan migrate'],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'hours-schema',
            'Hour Registration core tables',
            ['ok', 'Hour Registration lookup tables appear present.', []],
        );

        if ($hoursModuleRow === null) {
            $steps[] = $this->makeStep(
                'hours-module-row',
                '`maestro_modules` row for Hours',
                [
                    'warn',
                    '`maestro_modules` has no row for slug `hours`.',
                    [self::SEEDER_CMD],
                ],
            );
        } elseif (! $hoursModuleRow->is_enabled) {
            $steps[] = $this->makeStep(
                'hours-runtime-off',
                'Hour Registration runtime toggle',
                [
                    'warn',
                    '`hours` module exists but `is_enabled` is false.',
                    [],
                ],
            );
        } else {
            $steps[] = $this->makeStep(
                'hours-runtime-on',
                'Hour Registration runtime toggle',
                ['ok', 'Hours module row exists and is enabled.', []],
            );
        }

        return $steps;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function offersSteps(): array
    {
        $steps = [];

        $offersInConfig = config('core.modules.offers', true) === true;

        if (! $offersInConfig) {
            $steps[] = $this->makeStep(
                'offers-config-off',
                'Offers module (config)',
                ['ok', '`core.modules.offers` is false — the Offers PHP module is not loaded for this deployment.', []],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'offers-config-on',
            'Offers module (config)',
            ['ok', '`core.modules.offers` is enabled.', []],
        );

        $hasMaestroModules = Schema::hasTable('maestro_modules');

        /** @phpstan-ignore-next-line */
        $offersModuleRow = $hasMaestroModules
            ? MaestroModule::query()->where('slug', self::OFFERS_SLUG)->first()
            : null;

        $hasOffersTable = Schema::hasTable('offers');

        if (! $hasOffersTable) {
            $steps[] = $this->makeStep(
                'offers-schema',
                'Offers database tables',
                [
                    'fail',
                    '`offers` table is missing while the module is enabled in config.',
                    ['php artisan migrate'],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'offers-schema',
            'Offers database tables',
            ['ok', 'Core `offers` table exists.', []],
        );

        if ($offersModuleRow === null) {
            $steps[] = $this->makeStep(
                'offers-module-row',
                'Offers runtime toggle row',
                [
                    'warn',
                    '`maestro_modules` has no row for slug `offers`.',
                    [self::SEEDER_CMD],
                ],
            );
        } elseif (! $offersModuleRow->is_enabled) {
            $steps[] = $this->makeStep(
                'offers-runtime-off',
                'Offers enabled for users',
                [
                    'warn',
                    'The Offers module exists but `is_enabled` is false — normal users have no Offers access.',
                    [],
                ],
            );
        } else {
            $steps[] = $this->makeStep(
                'offers-runtime-on',
                'Offers enabled for users',
                ['ok', 'Offers row exists and is enabled for runtime.', []],
            );
        }

        return $steps;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function invoicesSteps(): array
    {
        $steps = [];

        $inConfig = config('core.modules.invoices', true) === true;

        if (! $inConfig) {
            $steps[] = $this->makeStep(
                'invoices-config-off',
                'Invoices module (config)',
                ['ok', '`core.modules.invoices` is false — the Invoices PHP module is not loaded.', []],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'invoices-config-on',
            'Invoices module (config)',
            ['ok', '`core.modules.invoices` is enabled.', []],
        );

        $hasTable = Schema::hasTable('invoices') && Schema::hasTable('maestro_vat_rates');

        if (! $hasTable) {
            $steps[] = $this->makeStep(
                'invoices-schema',
                'Invoices database tables',
                [
                    'fail',
                    'Invoices or VAT tables are missing while the module is enabled in config.',
                    ['php artisan migrate'],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'invoices-schema',
            'Invoices database tables',
            ['ok', 'Core `invoices` / `maestro_vat_rates` tables exist.', []],
        );

        $row = Schema::hasTable('maestro_modules')
            ? MaestroModule::query()->where('slug', self::INVOICES_SLUG)->first()
            : null;

        if ($row === null) {
            $steps[] = $this->makeStep(
                'invoices-module-row',
                'Invoices runtime toggle row',
                [
                    'warn',
                    '`maestro_modules` has no row for slug `invoices`.',
                    [self::SEEDER_CMD],
                ],
            );
        } elseif (! $row->is_enabled) {
            $steps[] = $this->makeStep(
                'invoices-runtime-off',
                'Invoices enabled for users',
                [
                    'warn',
                    'The Invoices module exists but `is_enabled` is false.',
                    [],
                ],
            );
        } else {
            $steps[] = $this->makeStep(
                'invoices-runtime-on',
                'Invoices enabled for users',
                ['ok', 'Invoices row exists and is enabled for runtime.', []],
            );
        }

        return $steps;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function itemsSteps(): array
    {
        $steps = [];

        $inConfig = config('core.modules.items', true) === true;

        if (! $inConfig) {
            $steps[] = $this->makeStep(
                'items-config-off',
                'Items module (config)',
                ['ok', '`core.modules.items` is false — the Items catalog module is not loaded.', []],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'items-config-on',
            'Items module (config)',
            ['ok', '`core.modules.items` is enabled.', []],
        );

        $hasTable = Schema::hasTable('maestro_items');

        if (! $hasTable) {
            $steps[] = $this->makeStep(
                'items-schema',
                'Items database table',
                [
                    'fail',
                    '`maestro_items` is missing while the module is enabled in config.',
                    ['php artisan migrate'],
                ],
            );

            return $steps;
        }

        $steps[] = $this->makeStep(
            'items-schema',
            'Items database table',
            ['ok', 'Table `maestro_items` exists.', []],
        );

        $row = Schema::hasTable('maestro_modules')
            ? MaestroModule::query()->where('slug', self::ITEMS_SLUG)->first()
            : null;

        if ($row === null) {
            $steps[] = $this->makeStep(
                'items-module-row',
                'Items runtime toggle row',
                [
                    'warn',
                    '`maestro_modules` has no row for slug `items`.',
                    [self::SEEDER_CMD],
                ],
            );
        } elseif (! $row->is_enabled) {
            $steps[] = $this->makeStep(
                'items-runtime-off',
                'Items enabled for users',
                [
                    'warn',
                    'The Items module exists but `is_enabled` is false.',
                    [],
                ],
            );
        } else {
            $steps[] = $this->makeStep(
                'items-runtime-on',
                'Items enabled for users',
                ['ok', 'Items row exists and is enabled for runtime.', []],
            );
        }

        return $steps;
    }

    // -------------------------------------------------------------------------
    // Per-module structured health (4 signals: config, database, seeder, routes)
    // -------------------------------------------------------------------------

    private const MODULE_SEEDER_CMDS = [
        'php artisan db:seed --class="Maestro\\Core\\Database\\Seeders\\UserSeeder"',
        'php artisan db:seed --class="Maestro\\Core\\Database\\Seeders\\ModuleAndNavigationSeeder"',
    ];

    /**
     * @return array{config:array<string,mixed>,database:array<string,mixed>,seeder:array<string,mixed>,routes:array<string,mixed>,overall:string}
     */
    public function moduleReport(string $slug): array
    {
        $signals = match ($slug) {
            'customers' => $this->customersSignals(),
            'hours' => $this->hoursSignals(),
            'offers' => $this->offersSignals(),
            'invoices' => $this->invoicesSignals(),
            'items' => $this->itemsSignals(),
            default => $this->unknownSignals($slug),
        };

        $overall = 'ok';
        foreach ($signals as $signal) {
            if ($signal['status'] === 'fail') {
                $overall = 'fail';
                break;
            }
            if ($signal['status'] === 'warn') {
                $overall = 'warn';
            }
        }

        return array_merge($signals, ['overall' => $overall]);
    }

    /** @return array<string, array<string, mixed>> */
    private function customersSignals(): array
    {
        $configKey = 'core.modules.customers';
        $configOn = config($configKey, true) === true;

        $hasTable = Schema::hasTable('customers')
            && Schema::hasTable('contacts')
            && Schema::hasTable('addresses');

        return [
            'config' => $this->configSignal($configKey, $configOn),
            'database' => $hasTable
                ? $this->ok('Tables customers, contacts, addresses exist.')
                : $this->fail('Tables customers, contacts, addresses are missing.', ['php artisan migrate']),
            'seeder' => $this->seederSignal('customers.view', 'customers', 'customers'),
            'routes' => $this->routesSignal($configOn),
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function hoursSignals(): array
    {
        $configKey = 'core.modules.hours';
        $configOn = config($configKey, true) === true;

        $hasTable = Schema::hasTable('hour_timesheet_weeks')
            && Schema::hasTable('hour_timesheet_lines')
            && Schema::hasTable('hour_projects');

        return [
            'config' => $this->configSignal($configKey, $configOn),
            'database' => $hasTable
                ? $this->ok('Hour registration tables exist.')
                : $this->fail('Hour registration tables are missing.', ['php artisan migrate']),
            'seeder' => $this->seederSignal('hours.week.manage', 'hours-week', 'hours'),
            'routes' => $this->routesSignal($configOn),
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function offersSignals(): array
    {
        $configKey = 'core.modules.offers';
        $configOn = config($configKey, true) === true;

        $hasTable = Schema::hasTable('offers')
            && Schema::hasTable('offer_sections')
            && Schema::hasTable('offer_rows');

        return [
            'config' => $this->configSignal($configKey, $configOn),
            'database' => $hasTable
                ? $this->ok('Offers tables exist.')
                : $this->fail('Offers tables are missing.', ['php artisan migrate']),
            'seeder' => $this->seederSignal('offers.view', 'offers', 'offers'),
            'routes' => $this->routesSignal($configOn),
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function invoicesSignals(): array
    {
        $configKey = 'core.modules.invoices';
        $configOn = config($configKey, true) === true;

        $hasTable = Schema::hasTable('invoices')
            && Schema::hasTable('invoice_lines')
            && Schema::hasTable('maestro_vat_rates');

        return [
            'config' => $this->configSignal($configKey, $configOn),
            'database' => $hasTable
                ? $this->ok('Invoices tables exist.')
                : $this->fail('Invoices tables are missing.', ['php artisan migrate']),
            'seeder' => $this->seederSignal('invoices.view', 'invoices', 'invoices'),
            'routes' => $this->routesSignal($configOn),
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function itemsSignals(): array
    {
        $configKey = 'core.modules.items';
        $configOn = config($configKey, true) === true;

        $hasTable = Schema::hasTable('maestro_items');

        return [
            'config' => $this->configSignal($configKey, $configOn),
            'database' => $hasTable
                ? $this->ok('Items catalog table exists.')
                : $this->fail('Table `maestro_items` is missing.', ['php artisan migrate']),
            'seeder' => $this->seederSignal('items.manage', 'settings-items-catalog', 'items'),
            'routes' => $this->routesSignal($configOn),
        ];
    }

    /** @return array<string, array<string, mixed>> */
    private function unknownSignals(string $slug): array
    {
        $unknown = $this->fail("Unknown module slug: {$slug}.", []);

        return [
            'config' => $unknown,
            'database' => $unknown,
            'seeder' => $unknown,
            'routes' => $unknown,
        ];
    }

    /** @return array<string, mixed> */
    private function configSignal(string $key, bool $on): array
    {
        if ($on) {
            return $this->ok("{$key} is enabled.");
        }

        return $this->fail(
            "{$key} is not enabled in config/core.php.",
            ['Add the module key to config/core.php, then run: php artisan config:clear'],
        );
    }

    /** @return array<string, mixed> */
    private function seederSignal(string $permissionName, string $navKey, string $moduleSlug): array
    {
        $hasPermTable = Schema::hasTable('permissions');
        $hasNavTable = Schema::hasTable('maestro_navigation_nodes');
        $hasModTable = Schema::hasTable('maestro_modules');

        if (! $hasPermTable || ! $hasNavTable || ! $hasModTable) {
            return $this->fail('Core tables missing — run migrations first.', ['php artisan migrate']);
        }

        $permOk = DB::table('permissions')->where('name', $permissionName)->exists();
        $navOk = DB::table('maestro_navigation_nodes')->where('key', $navKey)->exists();
        $moduleOk = DB::table('maestro_modules')->where('slug', $moduleSlug)->exists();

        if ($permOk && $navOk && $moduleOk) {
            return $this->ok('Permissions and navigation nodes are present.');
        }

        $missing = [];
        if (! $permOk) {
            $missing[] = "permission `{$permissionName}`";
        }
        if (! $navOk) {
            $missing[] = "navigation node `{$navKey}`";
        }
        if (! $moduleOk) {
            $missing[] = "module registry row `{$moduleSlug}`";
        }

        return $this->warn(
            'Missing: '.implode(', ', $missing).'. Run the seeders.',
            self::MODULE_SEEDER_CMDS,
        );
    }

    /** @return array<string, mixed> */
    private function routesSignal(bool $configOn): array
    {
        if ($configOn) {
            return $this->ok('Module routes are registered at boot.');
        }

        return $this->fail('Routes not registered — config is off.', []);
    }

    /** @return array<string, mixed> */
    private function ok(string $detail): array
    {
        return ['status' => 'ok', 'detail' => $detail, 'commands' => []];
    }

    /** @return array<string, mixed> */
    private function warn(string $detail, array $commands): array
    {
        return ['status' => 'warn', 'detail' => $detail, 'commands' => $commands];
    }

    /** @return array<string, mixed> */
    private function fail(string $detail, array $commands): array
    {
        return ['status' => 'fail', 'detail' => $detail, 'commands' => $commands];
    }

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

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

        return $worst;
    }
}
