<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * Reads plain-text passwords from the legacy `users` table, bcrypt-hashes
 * them, and writes a SQL UPDATE file ready to run against the new database.
 *
 * For users whose password is 'abc123' a strong random password is generated
 * first so they are not left with a known weak credential.
 *
 * Usage:
 *   php artisan skinbase:hash-legacy-plain-passwords
 *   php artisan skinbase:hash-legacy-plain-passwords --out=storage/app/hashed-passwords.sql
 *   php artisan skinbase:hash-legacy-plain-passwords --chunk=1000
 */
class HashLegacyPlainPasswordsCommand extends Command
{
    protected $signature = 'skinbase:hash-legacy-plain-passwords
        {--out=        : Output SQL file path (default: storage/app/hashed-plain-passwords.sql)}
        {--chunk=500   : Chunk size for reading legacy users}
        {--legacy-connection=legacy : Name of the legacy DB connection}
        {--legacy-table=users       : Name of the legacy users table}
        {--dry-run                  : Print row count without writing the SQL file}';

    protected $description = 'Hash plain-text legacy passwords with bcrypt and export UPDATE SQL. Randomises weak \'abc123\' passwords.';

    // Characters for random password generation (no ambiguous l/1/0/O)
    private const UPPER  = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
    private const LOWER  = 'abcdefghjkmnpqrstuvwxyz';
    private const DIGITS = '23456789';
    private const SPECIAL = '!@#$%^&*';

    public function handle(): int
    {
        $outPath    = $this->option('out') ?: storage_path('app/hashed-plain-passwords.sql');
        $chunk      = max(1, (int) ($this->option('chunk') ?? 500));
        $legacyConn = (string) ($this->option('legacy-connection') ?? 'legacy');
        $legacyTable = (string) ($this->option('legacy-table') ?? 'users');
        $dryRun     = (bool) $this->option('dry-run');

        // Verify legacy connection is available
        try {
            DB::connection($legacyConn)->getPdo();
        } catch (\Throwable $e) {
            $this->error('Cannot connect to legacy DB: ' . $e->getMessage());
            return self::FAILURE;
        }

        $now        = now()->format('Y-m-d H:i:s');
        $newDbName  = DB::getDatabaseName();

        $lines   = [];
        $lines[] = '-- Hashed plain-password export';
        $lines[] = '-- Generated: ' . $now;
        $lines[] = '-- Source: legacy DB (read-only) — passwords bcrypt-hashed for Laravel';
        $lines[] = '-- WARNING: this file contains sensitive data. Delete after applying.';
        $lines[] = '';
        $lines[] = 'SET NAMES utf8mb4;';
        $lines[] = 'USE `' . $newDbName . '`;';
        $lines[] = 'START TRANSACTION;';
        $lines[] = '';

        $processed  = 0;
        $randomised = 0;
        $skipped    = 0;
        $chunkNum   = 0;

        // Count total for progress bar
        $total = DB::connection($legacyConn)
            ->table($legacyTable)
            ->where('should_migrate', 1)
            ->count();

        $this->info("Legacy DB: {$total} users with should_migrate=1 found.");
        $this->info("Output   : " . ($dryRun ? '(dry-run, no file)' : $outPath));
        $this->newLine();

        $bar = $this->output->createProgressBar($total);
        $bar->setFormat(" %current%/%max% [%bar%] %percent:3s%%  mem:%memory:6s%\n  %message%");
        $bar->setMessage('Starting…');
        $bar->start();

        DB::connection($legacyConn)
            ->table($legacyTable)
            ->select(['user_id', 'password'])
            ->where('should_migrate', 1)
            ->orderBy('user_id')
            ->chunk($chunk, function ($rows) use (&$lines, &$processed, &$randomised, &$skipped, &$chunkNum, $now, $bar, $chunk) {
                $chunkNum++;
                $bar->setMessage("chunk #{$chunkNum} (chunk size {$chunk})");

                foreach ($rows as $row) {
                    $userId = (int) ($row->user_id ?? 0);
                    $plain  = trim((string) ($row->password ?? ''));

                    if ($userId <= 0 || $plain === '') {
                        $bar->setMessage("user_id={$userId}  SKIPPED (empty)");
                        $bar->advance();
                        $skipped++;
                        continue;
                    }

                    // Skip entries that already look like a bcrypt / argon hash
                    if (preg_match('/^\$2[aby]\$|^\$argon2/', $plain)) {
                        $lines[] = "-- USER ID: {$userId}  (already hashed — skipped)";
                        $lines[] = '';
                        $bar->setMessage("user_id={$userId}  SKIPPED (already hashed)");
                        $bar->advance();
                        $skipped++;
                        continue;
                    }

                    $commentPlain = $plain;
                    $tag          = 'hashed';

                    if ($plain === 'abc123') {
                        $newPlain     = $this->generateStrongPassword();
                        $commentPlain = "abc123 => {$newPlain}";
                        $plain        = $newPlain;
                        $tag          = 'RANDOMISED (was abc123)';
                        $randomised++;
                    }

                    $bcrypt  = Hash::make($plain);
                    $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $bcrypt);

                    $lines[] = "-- USER ID: {$userId}  PASS: {$commentPlain}";
                    $lines[] = "SAVEPOINT sp_{$userId};";
                    $lines[] = "UPDATE `users` SET `password` = '{$escaped}' WHERE `id` = {$userId};";
                    $lines[] = '';

                    $bar->setMessage("user_id={$userId}  {$tag}");
                    $bar->advance();
                    $processed++;
                }
            });

        $bar->setMessage("Done.");
        $bar->finish();
        $this->newLine(2);

        $lines[] = 'COMMIT;';
        $lines[] = '';
        $lines[] = "-- Total processed : {$processed}";
        $lines[] = "-- Passwords randomised (abc123) : {$randomised}";
        $lines[] = "-- Rows skipped (empty / already hashed) : {$skipped}";

        $this->table(
            ['Metric', 'Count'],
            [
                ['Processed (hashed)',       $processed],
                ['Randomised (abc123)',       $randomised],
                ['Skipped',                  $skipped],
                ['Total should_migrate=1',   $total],
            ]
        );

        if ($dryRun) {
            $this->info('Dry-run mode — SQL file not written.');
            return self::SUCCESS;
        }

        $dir = dirname($outPath);
        if (!is_dir($dir) && !mkdir($dir, 0750, true)) {
            $this->error("Cannot create output directory: {$dir}");
            return self::FAILURE;
        }

        $sql = implode("\n", $lines) . "\n";

        if (file_put_contents($outPath, $sql) === false) {
            $this->error("Cannot write SQL file: {$outPath}");
            return self::FAILURE;
        }

        $this->info("SQL written to: {$outPath}");
        return self::SUCCESS;
    }

    /**
     * Generate a cryptographically random strong password.
     * Format: 4 upper + 4 lower + 3 digits + 2 special = 13 chars, then shuffled.
     */
    private function generateStrongPassword(): string
    {
        $password  = '';
        $password .= $this->randomChars(self::UPPER,   4);
        $password .= $this->randomChars(self::LOWER,   4);
        $password .= $this->randomChars(self::DIGITS,  3);
        $password .= $this->randomChars(self::SPECIAL, 2);

        // Shuffle with a cryptographically random permutation
        $chars = str_split($password);
        for ($i = count($chars) - 1; $i > 0; $i--) {
            $j = random_int(0, $i);
            [$chars[$i], $chars[$j]] = [$chars[$j], $chars[$i]];
        }

        return implode('', $chars);
    }

    private function randomChars(string $pool, int $count): string
    {
        $out    = '';
        $max    = strlen($pool) - 1;
        for ($i = 0; $i < $count; $i++) {
            $out .= $pool[random_int(0, $max)];
        }
        return $out;
    }
}
