<?php

namespace App\Services;

use App\Models\Role;
use App\Models\User;
use App\Services\Authorization\AuthorizationAssignmentService;
use Database\Seeders\FactoryResetSeeder;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use RuntimeException;
use Throwable;

class SystemResetService
{
    public function __construct(
        private SystemBackupService $backups,
        private AuthorizationAssignmentService $authorization,
    ) {
    }

    /**
     * @return array{administrator: User, backup: array<string,mixed>}
     */
    public function reset(User $administrator): array
    {
        if (! $administrator->hasRole('super-administrator')) {
            throw new RuntimeException('Only a super administrator can perform a factory reset.');
        }

        @set_time_limit(0);
        ignore_user_abort(true);

        $lockPath = storage_path('framework/cache/system-factory-reset.lock');
        File::ensureDirectoryExists(dirname($lockPath), 0750, true);
        $lock = fopen($lockPath, 'c+');

        if ($lock === false || ! flock($lock, LOCK_EX | LOCK_NB)) {
            if (is_resource($lock)) {
                fclose($lock);
            }

            throw new RuntimeException('Another factory reset is already running.');
        }

        $snapshot = $this->captureAdministrator($administrator);
        $backup = null;
        $maintenanceEnabled = false;

        try {
            // A full recovery archive is mandatory. If this fails, nothing is erased.
            $backup = $this->backups->create('full');

            $this->runArtisan('down', ['--retry' => 60]);
            $maintenanceEnabled = true;

            $this->runArtisan('migrate:fresh', ['--force' => true]);
            $this->runArtisan('db:seed', [
                '--class' => FactoryResetSeeder::class,
                '--force' => true,
            ]);

            $restoredAdministrator = $this->restoreAdministrator($snapshot);
            $this->clearApplicationFiles();

            // Clear stale route/view/config/application caches after rebuilding the database.
            $this->runArtisan('optimize:clear');

            return [
                'administrator' => $restoredAdministrator,
                'backup' => $backup,
            ];
        } catch (Throwable $exception) {
            $backupName = is_array($backup) ? ($backup['filename'] ?? null) : null;
            $message = 'Factory reset failed.';
            if ($backupName) {
                $message .= ' Recovery backup: '.$backupName.'.';
            }
            $message .= ' '.$exception->getMessage();

            throw new RuntimeException($message, previous: $exception);
        } finally {
            if ($maintenanceEnabled) {
                try {
                    Artisan::call('up');
                } catch (Throwable) {
                    // Do not mask the original reset result if maintenance cleanup fails.
                }
            }

            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    /** @return array<string,mixed> */
    private function captureAdministrator(User $administrator): array
    {
        return [
            'name' => $administrator->name,
            'email' => $administrator->email,
            'username' => $administrator->username,
            'employee_number' => $administrator->employee_number,
            'phone' => $administrator->phone,
            'nin' => $administrator->nin,
            'position' => $administrator->position,
            'status' => 'active',
            'employed_at' => $administrator->getRawOriginal('employed_at'),
            'password' => $administrator->getRawOriginal('password'),
            'email_verified_at' => $administrator->getRawOriginal('email_verified_at'),
        ];
    }

    /** @param array<string,mixed> $snapshot */
    private function restoreAdministrator(array $snapshot): User
    {
        $administrator = new User();
        $administrator->forceFill([
            'name' => $snapshot['name'],
            'email' => $snapshot['email'],
            'username' => $snapshot['username'],
            'employee_number' => $snapshot['employee_number'],
            'phone' => $snapshot['phone'],
            'nin' => $snapshot['nin'],
            'position' => $snapshot['position'],
            'status' => 'active',
            'employed_at' => $snapshot['employed_at'],
            'primary_branch_id' => null,
            'profile_photo_path' => null,
            'password' => $snapshot['password'],
            'email_verified_at' => $snapshot['email_verified_at'] ?: now(),
            'remember_token' => null,
            'two_factor_secret' => null,
            'two_factor_recovery_codes' => null,
            'two_factor_confirmed_at' => null,
        ]);
        $administrator->save();

        $role = Role::query()->where('name', 'super-administrator')->first();
        if (! $role) {
            throw new RuntimeException('The super-administrator role was not restored by the factory reset seeder.');
        }

        $this->authorization->assignUserRole($administrator, $role);

        return $administrator->fresh();
    }

    private function clearApplicationFiles(): void
    {
        $publicStorage = storage_path('app/public');
        File::ensureDirectoryExists($publicStorage, 0750, true);
        File::cleanDirectory($publicStorage);

        $privateStorage = storage_path('app/private');
        File::ensureDirectoryExists($privateStorage, 0750, true);

        foreach (File::files($privateStorage) as $file) {
            File::delete($file->getPathname());
        }

        foreach (File::directories($privateStorage) as $directory) {
            if (basename($directory) === 'backups') {
                continue;
            }

            File::deleteDirectory($directory);
        }
    }

    /** @param array<string,mixed> $parameters */
    private function runArtisan(string $command, array $parameters = []): void
    {
        $exitCode = Artisan::call($command, $parameters);
        if ($exitCode === 0) {
            return;
        }

        $output = trim(Artisan::output());
        throw new RuntimeException(
            $command.' failed with exit code '.$exitCode.($output !== '' ? ': '.$output : '.')
        );
    }
}
