<?php

namespace Database\Seeders;

use App\Models\Central\Tenant;
use App\Models\MenuItem;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Services\CurrentContext;
use App\Services\Tenancy\TenantProvisioner;
use Database\Seeders\Menu\SystemRoleDefinition;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;

class PermissionsAndRolesSeeder extends Seeder
{
    public function run(): void
    {
        DB::transaction(function (): void {
            $this->derivePermissionsFromMenu();
        });

        $context = app(CurrentContext::class);

        if ($context->hasTenant()) {
            DB::transaction(fn () => $this->seedSystemRoles());
        } else {
            Tenant::all()->each(function (Tenant $tenant) use ($context): void {
                DB::transaction(fn () => $context->runForTenant($tenant, fn () => $this->seedSystemRoles()));
            });
        }

        User::query()->whereNotNull('role_id')->lazy()->each(function (User $user): void {
            $user->flushPermissionCache();
        });
    }

    private function derivePermissionsFromMenu(): void
    {
        $names = MenuItem::query()
            ->whereNotNull('module_key')
            ->get()
            ->flatMap(fn (MenuItem $item) => $item->permissionNames())
            ->unique();

        foreach ($names as $name) {
            Permission::firstOrCreate(['name' => $name]);
        }

        // `pos.discount` was one umbrella permission covering every manual
        // checkout discount; it has been split into `pos.cart_discount` and
        // `pos.line_discount`. Any role/user/tenant already holding the old
        // name is moved onto both new names (the closest reading of what the
        // old grant meant) before the stale-permission sweep below deletes it.
        $this->remapLegacyPosDiscount($names);

        // Permissions that no longer correspond to a menu action (renamed or
        // removed from MenuDefinition) are stale — drop them along with their
        // role/user grants (cascade-deleted via FK) so they can't linger as
        // unusable, ungoverned access in the permissions UI.
        Permission::query()
            ->whereNotIn('name', $names)
            ->get()
            ->each(fn (Permission $permission) => $permission->forceDelete());
    }

    /**
     * Port existing `pos.discount` grants onto `pos.cart_discount` +
     * `pos.line_discount` across every grant table that references
     * permissions by id (roles, per-user overrides, tenant entitlements).
     * Idempotent: re-running a seeder must never duplicate or lose a grant.
     *
     * @param  \Illuminate\Support\Collection<int, string>  $currentNames
     */
    private function remapLegacyPosDiscount($currentNames): void
    {
        if (! $currentNames->contains('pos.cart_discount') || ! $currentNames->contains('pos.line_discount')) {
            return;
        }

        $legacy = Permission::query()->where('name', 'pos.discount')->first();
        if (! $legacy) {
            return;
        }

        $targets = Permission::query()
            ->whereIn('name', ['pos.cart_discount', 'pos.line_discount'])
            ->pluck('id')
            ->all();

        $legacyGrants = function (string $table, string $ownerColumn) use ($legacy) {
            return DB::table($table)
                ->where('permission_id', $legacy->id)
                ->pluck($ownerColumn)
                ->all();
        };

        foreach ($targets as $targetId) {
            foreach ($legacyGrants('role_permissions', 'role_id') as $ownerId) {
                DB::table('role_permissions')->insertOrIgnore([
                    'role_id' => $ownerId,
                    'permission_id' => $targetId,
                ]);
            }

            if (Schema::hasTable('user_permission_overrides')) {
                foreach (DB::table('user_permission_overrides')
                    ->where('permission_id', $legacy->id)
                    ->get(['user_id', 'type', 'granted_by', 'granted_at']) as $row) {
                    DB::table('user_permission_overrides')->insertOrIgnore([
                        'user_id' => $row->user_id,
                        'permission_id' => $targetId,
                        'type' => $row->type,
                        'granted_by' => $row->granted_by,
                        'granted_at' => $row->granted_at,
                    ]);
                }
            }

            if (Schema::hasTable('tenant_permissions')) {
                foreach ($legacyGrants('tenant_permissions', 'tenant_id') as $ownerId) {
                    DB::table('tenant_permissions')->insertOrIgnore([
                        'tenant_id' => $ownerId,
                        'permission_id' => $targetId,
                    ]);
                }
            }
        }
    }

    /**
     * Seed the system roles (Full Administrator, Manager, …) for the CURRENT
     * tenant context. Public so {@see TenantProvisioner}
     * can reuse it when onboarding a new tenant. Global permissions must already
     * be derived (run() does that once, globally).
     */
    public function seedSystemRoles(): void
    {
        foreach (SystemRoleDefinition::roles() as $name => $config) {
            // withTrashed(): a system role soft-deleted in a prior run/UI action
            // still occupies the (tenant_id, name) unique key, which isn't
            // deleted_at-aware — firstOrNew() alone would miss it and collide
            // on insert.
            /** @var Role $role */
            $role = Role::withTrashed()->firstOrNew(['name' => $name]);
            if ($role->trashed()) {
                $role->restore();
            }
            $role->fill([
                'description' => $config['description'] ?? null,
                'is_system' => true,
                'is_full_admin' => $config['is_full_admin'] ?? false,
                'is_active' => true,
            ])->save();

            $permissionNames = $this->resolveGrants($config['permissions'] ?? []);

            $existing = Permission::query()->whereIn('name', $permissionNames)->pluck('name')->all();
            $missing = array_diff($permissionNames, $existing);

            if (! empty($missing)) {
                throw new RuntimeException(
                    "System role '{$name}' references unknown permissions: "
                    .implode(', ', $missing)
                    .'. Check MenuDefinition module_keys against SystemRoleDefinition.'
                );
            }

            $permissionIds = Permission::query()->whereIn('name', $permissionNames)->pluck('id')->all();
            $role->permissions()->sync($permissionIds);
        }
    }

    /**
     * @param  string|array<string, array<int, string>>  $grants
     * @return array<int, string>
     */
    private function resolveGrants(string|array $grants): array
    {
        if ($grants === 'all') {
            return Permission::query()->pluck('name')->all();
        }

        $resolved = [];
        foreach ((array) $grants as $moduleKey => $actions) {
            foreach ((array) $actions as $action) {
                $resolved[] = "{$moduleKey}.{$action}";
            }
        }

        return $resolved;
    }
}
