<?php

namespace App\Console\Commands;

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

class AuditOrphanedArtworksCommand extends Command
{
    protected $signature = 'skinbase:audit-orphaned-artworks
                            {--output= : Path to write CSV report (optional)}
                            {--sql= : Path to write SQL import script for recoverable users (optional)}
                            {--check-usernames : Compare usernames between legacy DB and new users table for the same IDs}
                            {--hide-missing : Suppress MISS lines in --check-usernames output}
                            {--username-output= : Path to write username-diff CSV (optional, used with --check-usernames)}
                            {--username-fix-sql= : Path to write SQL UPDATE script to align new usernames to legacy (optional, used with --check-usernames)}
                            {--chunk=500 : Chunk size for processing}';

    protected $description = 'Find artworks whose user_id does not exist in the users table (also checks legacy DB)';

    public function handle(): int
    {
        $this->info('Scanning for artworks with missing users…');

        $chunkSize  = (int) $this->option('chunk');
        $outputPath = $this->option('output');
        $sqlPath    = $this->option('sql');

        // ── Step 1: find user_ids in artworks missing from the NEW users table ──
        $missingFromNew = DB::table('artworks')
            ->select('user_id')
            ->distinct()
            ->whereNotIn('user_id', function ($sub) {
                $sub->select('id')->from('users');
            })
            ->pluck('user_id')
            ->sort()
            ->values();

        if ($missingFromNew->isEmpty()) {
            $this->info('✓ No orphaned artworks found — all user_ids exist in users.');
            return self::SUCCESS;
        }

        $this->warn("Found {$missingFromNew->count()} user_id(s) in artworks missing from the new users table.");

        // ── Step 2: cross-check against legacy DB ──
        $legacyAvailable = false;
        $legacyRows      = collect();

        try {
            DB::connection('legacy')->getPdo();
            $legacyAvailable = true;
        } catch (\Throwable) {
            $this->warn('Legacy DB connection is not available — skipping legacy cross-check.');
        }

        if ($legacyAvailable) {
            $this->info('Cross-checking against legacy DB…');

            // Legacy users table uses `user_id` as PK (not `id`).
            $legacyRows = DB::connection('legacy')
                ->table('users')
                ->whereIn('user_id', $missingFromNew->all())
                ->get()
                ->keyBy('user_id');

            $this->line("  • {$legacyRows->count()} of those exist in the legacy DB (recoverable).");
            $this->line('  • ' . $missingFromNew->diff($legacyRows->keys())->count() . ' do NOT exist in legacy DB either (truly orphaned).');
        }

        $this->newLine();

        // ── Step 3: collect artwork rows ──
        $rows = [];

        DB::table('artworks')
            ->whereIn('user_id', $missingFromNew->all())
            ->orderBy('user_id')
            ->orderBy('id')
            ->chunk($chunkSize, function ($artworks) use ($legacyRows, &$rows) {
                foreach ($artworks as $artwork) {
                    $inLegacy = $legacyRows->has($artwork->user_id);
                    $rows[]   = [
                        'user_id'       => $artwork->user_id,
                        'artwork_id'    => $artwork->id,
                        'artwork_slug'  => $artwork->slug ?? '',
                        'artwork_title' => $artwork->title ?? '',
                        'status'        => $artwork->status ?? '',
                        'created_at'    => $artwork->created_at ?? '',
                        'in_legacy_db'  => $inLegacy ? 'yes' : 'no',
                    ];
                }
            });

        // ── Step 4: console table ──
        $tableRows = collect($rows)->map(fn($r) => [
            $r['user_id'],
            $r['artwork_id'],
            $r['artwork_slug'],
            mb_strimwidth((string) $r['artwork_title'], 0, 48, '…'),
            $r['status'],
            $r['created_at'],
            $r['in_legacy_db'],
        ])->all();

        $this->table(
            ['user_id', 'artwork_id', 'slug', 'title', 'status', 'created_at', 'in_legacy_db'],
            $tableRows,
        );

        $this->newLine();

        // ── Step 5: per-user summary ──
        $countPerUser = collect($rows)->groupBy('user_id');
        $this->info('Summary per missing user:');
        foreach ($countPerUser as $userId => $group) {
            $inLegacy = $group->first()['in_legacy_db'];
            $this->line(sprintf(
                '  user_id %-8s  %3d artwork(s)  legacy_db: %s',
                $userId,
                $group->count(),
                $inLegacy,
            ));
        }

        $this->newLine();
        $this->warn('Total orphaned artworks: ' . count($rows));

        // ── Step 6: optional CSV export ──
        if ($outputPath) {
            $fp = fopen($outputPath, 'w');
            if ($fp === false) {
                $this->error("Could not open output file: {$outputPath}");
                return self::FAILURE;
            }

            fputcsv($fp, ['user_id', 'artwork_id', 'artwork_slug', 'artwork_title', 'status', 'created_at', 'in_legacy_db']);
            foreach ($rows as $row) {
                fputcsv($fp, array_values($row));
            }
            fclose($fp);

            $this->info("Report written to: {$outputPath}");
        }

        // ── Step 7: optional SQL import script for recoverable users ──
        if ($sqlPath) {
            if (! $legacyAvailable) {
                $this->error('Cannot generate SQL: legacy DB connection is not available.');
                return self::FAILURE;
            }

            $result = $this->generateImportSql($sqlPath, $legacyRows, $missingFromNew);
            if ($result !== self::SUCCESS) {
                return $result;
            }
        }

        // ── Step 8: optional username diff between legacy and new DB ──
        if ($this->option('check-usernames')) {
            $this->checkUsernameDiff((int) $this->option('chunk'), $this->option('username-output'), $this->option('username-fix-sql'));
        }

        return self::SUCCESS;
    }

    /**
     * Generate a self-contained SQL script that re-inserts recoverable users
     * (plus their user_profiles and user_statistics rows) into the new DB.
     *
     * @param  \Illuminate\Support\Collection  $legacyRows  keyed by user_id
     * @param  \Illuminate\Support\Collection  $missingFromNew
     */
    protected function generateImportSql(string $path, $legacyRows, $missingFromNew): int
    {
        // Also pull statistics from legacy for each recoverable user.
        $legacyStats = DB::connection('legacy')
            ->table('users_statistics')
            ->whereIn('user_id', $legacyRows->keys()->all())
            ->get()
            ->keyBy('user_id');

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

        $lines = [];
        $lines[] = '-- ============================================================';
        $lines[] = '-- Skinbase orphaned-artwork user recovery script';
        $lines[] = '-- Generated: ' . $now;
        $lines[] = '-- Source:    legacy DB  →  new users / user_profiles / user_statistics';
        $lines[] = '-- REVIEW CAREFULLY before running on production.';
        $lines[] = '-- ============================================================';
        $lines[] = '';
        $lines[] = 'SET NAMES utf8mb4;';
        $lines[] = 'USE `' . config('database.connections.mysql.database') . '`;';
        $lines[] = 'START TRANSACTION;';
        $lines[] = '';

        $recovered = 0;
        $skipped   = 0;

        foreach ($legacyRows as $userId => $row) {
            $userId = (int) $userId;
            $stat   = $legacyStats->get($userId);

            // --- resolve username (mirrors ImportLegacyUsers: lowercase, alnum+dash+underscore, max 20) ---
            $rawUsername = trim((string) ($row->uname ?? ''));
            $username    = $rawUsername !== '' ? $rawUsername : ('user' . $userId);
            $username    = strtolower(preg_replace('/[^A-Za-z0-9_\-]/', '', $username));
            $username    = substr($username !== '' ? $username : ('user' . $userId), 0, 20);

            // --- resolve email ---
            // Use a guaranteed-unique synthetic email for the INSERT so it never conflicts
            // with an existing account (email has a unique constraint). The real legacy email
            // is stored in a comment — patch it manually after verifying no conflict.
            $rawEmail    = strtolower(trim((string) ($row->email ?? '')));
            $safeEmail   = $userId . '@legacy.skinbase.org'; // collision-free, always unique

            // --- dates ---
            $createdAt   = $this->sqlDate($row->joinDate ?? null, $now);
            $lastVisitAt = $this->sqlDate($row->LastVisit ?? null, null);

            // --- stats ---
            $uploads   = $this->safeInt($stat->uploads ?? 0);
            $downloads = $this->safeInt($stat->downloads ?? 0);
            $pageviews = $this->safeInt($stat->pageviews ?? 0);
            $awards    = $this->safeInt($stat->awards ?? 0);

            // --- profile fields ---
            $about       = $this->sqlString($row->about_me ?? $row->description ?? null);
            $country     = $this->sqlString($row->country ?? null);
            $countryCode = $this->sqlString($row->country_code ? substr($row->country_code, 0, 2) : null);
            $language    = $this->sqlString($row->lang ?? null);
            $website     = $this->sqlString($row->web ?? null);
            $gender      = $this->sqlString($this->normalizeLegacyGender($row->gender ?? null));
            $birthdate   = $this->sqlDate($row->birth ?? null, null);
            $avatarLegacy = $this->sqlString($row->picture ?? null);
            $coverImage  = $this->sqlString($row->cover_art ?? null);

            $lines[] = "-- user_id={$userId}  username={$username}  real_email={$rawEmail}  (password placeholder; user must reset)";
            $lines[] = "-- To restore real email after import: UPDATE \`users\` SET \`email\`=" . $this->sqlString($rawEmail) . " WHERE \`id\`={$userId} AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM \`users\` WHERE \`email\`=" . $this->sqlString($rawEmail) . " AND \`id\`!={$userId}) _c);";
            $lines[] = "SAVEPOINT sp_{$userId};";

            // users: synthetic email guarantees no unique-constraint conflict on INSERT
            $name       = $this->sqlString($row->real_name ?: $username);
            $usernameQ  = $this->sqlString($username);
            $emailQ     = $this->sqlString($safeEmail);
            $passwordQ  = $this->sqlString('$2y$12$' . Str::random(53));
            $isActive   = ($row->active ?? 1) ? '1' : '0';
            $lastVisitQ = $lastVisitAt ? "'$lastVisitAt'" : 'NULL';

            $lines[] = "INSERT IGNORE INTO `users`"
                . " (`id`, `username`, `username_changed_at`, `name`, `email`, `password`, `role`,"
                . " `is_active`, `needs_password_reset`, `legacy_password_algo`, `last_visit_at`, `created_at`, `updated_at`)"
                . " VALUES ({$userId}, {$usernameQ}, '{$now}', {$name}, {$emailQ}, {$passwordQ},"
                . " 'user', {$isActive}, 1, NULL, {$lastVisitQ}, '{$createdAt}', '{$now}');";
            $lines[] = "UPDATE `users` SET `username`={$usernameQ}, `username_changed_at`='{$now}',"
                . " `name`={$name}, `updated_at`='{$now}' WHERE `id` = {$userId};";

            // user_profiles: INSERT IGNORE (FK-safe — silently skipped if parent missing) + UPDATE
            $lines[] = "INSERT IGNORE INTO `user_profiles`"
                . " (`user_id`, `about`, `avatar_legacy`, `cover_image`,"
                . " `country`, `country_code`, `language`, `website`, `gender`, `birthdate`, `updated_at`)"
                . " VALUES ({$userId}, {$about}, {$avatarLegacy}, {$coverImage},"
                . " {$country}, {$countryCode}, {$language}, {$website}, {$gender},"
                . " " . ($birthdate ? "'$birthdate'" : 'NULL') . ", '{$now}');";
            $lines[] = "UPDATE `user_profiles` SET"
                . " `about`={$about}, `avatar_legacy`={$avatarLegacy}, `cover_image`={$coverImage},"
                . " `country`={$country}, `country_code`={$countryCode}, `language`={$language},"
                . " `website`={$website}, `updated_at`='{$now}' WHERE `user_id` = {$userId};";

            // user_statistics: same INSERT IGNORE + UPDATE pattern
            $lines[] = "INSERT IGNORE INTO `user_statistics`"
                . " (`user_id`, `uploads_count`, `downloads_received_count`,"
                . " `artwork_views_received_count`, `awards_received_count`, `updated_at`)"
                . " VALUES ({$userId}, {$uploads}, {$downloads}, {$pageviews}, {$awards}, '{$now}');";
            $lines[] = "UPDATE `user_statistics` SET"
                . " `uploads_count`={$uploads}, `downloads_received_count`={$downloads},"
                . " `artwork_views_received_count`={$pageviews}, `awards_received_count`={$awards},"
                . " `updated_at`='{$now}' WHERE `user_id` = {$userId};";

            $lines[] = '';
            $recovered++;
        }

        // List truly-orphaned user_ids (not in legacy DB either) as comments.
        $trulyOrphaned = $missingFromNew->diff($legacyRows->keys());
        if ($trulyOrphaned->isNotEmpty()) {
            $lines[] = '-- ============================================================';
            $lines[] = '-- The following user_ids were NOT found in legacy DB.';
            $lines[] = '-- Their artworks are truly orphaned and cannot be auto-recovered.';
            $lines[] = '-- ============================================================';
            foreach ($trulyOrphaned as $uid) {
                $lines[] = "-- user_id={$uid}";
                $skipped++;
            }
            $lines[] = '';
        }

        $lines[] = 'COMMIT;';
        $lines[] = '';
        $lines[] = "-- Recovered: {$recovered}  |  Truly orphaned (no SQL generated): {$skipped}";

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

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

        $this->info("SQL import script written to: {$path}  ({$recovered} users)");
        if ($skipped > 0) {
            $this->warn("{$skipped} user_id(s) not found in legacy DB — listed as comments at the bottom of the SQL file.");
        }

        return self::SUCCESS;
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    protected function checkUsernameDiff(int $chunkSize, ?string $outputPath, ?string $fixSqlPath): void
    {
        try {
            DB::connection('legacy')->getPdo();
        } catch (\Throwable) {
            $this->error('--check-usernames: legacy DB connection is not available.');
            return;
        }

        $this->info('Comparing usernames between legacy and new DB…');
        $this->newLine();

        $diffs   = [];
        $matches = 0;
        $missing = 0;

        // Stream legacy users in chunks; for each one look up the new users table by same ID.
        DB::connection('legacy')
            ->table('users')
            ->orderBy('user_id')
            ->chunk($chunkSize, function ($legacyUsers) use (&$diffs, &$matches, &$missing) {
                $ids = $legacyUsers->pluck('user_id')->all();

                $newUsers = DB::table('users')
                    ->whereIn('id', $ids)
                    ->pluck('username', 'id'); // keyed by id

                foreach ($legacyUsers as $lu) {
                    $id = (int) $lu->user_id;

                    if (! $newUsers->has($id)) {
                        $legacyUsername = strtolower(preg_replace('/[^A-Za-z0-9_\-]/', '', trim((string) ($lu->uname ?? ''))));
                        $legacyUsername = substr($legacyUsername !== '' ? $legacyUsername : ('user' . $id), 0, 20);
                        if (! $this->option('hide-missing')) {
                            $this->line(sprintf(
                                '  <fg=gray>MISS</> id=%-8d  legacy=<fg=yellow>%s</>',
                                $id, $legacyUsername,
                            ));
                        }
                        $missing++;
                        continue;
                    }

                    $legacyUsername = strtolower(preg_replace('/[^A-Za-z0-9_\-]/', '', trim((string) ($lu->uname ?? ''))));
                    $legacyUsername = substr($legacyUsername !== '' ? $legacyUsername : ('user' . $id), 0, 20);
                    $newUsername    = (string) $newUsers->get($id);

                    if ($legacyUsername !== $newUsername) {
                        $this->line(sprintf(
                            '  <fg=red>FAIL</> id=%-8d  legacy=<fg=yellow>%-20s</>  new=<fg=cyan>%s</>',
                            $id, $legacyUsername, $newUsername,
                        ));
                        $diffs[] = [
                            'user_id'         => $id,
                            'legacy_username' => $legacyUsername,
                            'new_username'    => $newUsername,
                            'legacy_email'    => strtolower(trim((string) ($lu->email ?? ''))),
                        ];
                    } else {
                        $matches++;
                    }
                }
            });

        $this->newLine();

        if (empty($diffs) && $missing === 0) {
            $this->info("✓ All {$matches} checked username(s) match.");
            return;
        }

        $this->warn(count($diffs) . ' FAIL  /  ' . $missing . ' MISSING  /  ' . $matches . ' OK');

        if ($outputPath) {
            $fp = fopen($outputPath, 'w');
            if ($fp === false) {
                $this->error("Could not open output file: {$outputPath}");
                return;
            }
            fputcsv($fp, ['user_id', 'legacy_username', 'new_username', 'legacy_email']);
            foreach ($diffs as $d) {
                fputcsv($fp, array_values($d));
            }
            fclose($fp);
            $this->info("Username diff written to: {$outputPath}");
        }

        if ($fixSqlPath && ! empty($diffs)) {
            $lines   = [];
            $lines[] = "-- Username fix: update new DB usernames to match normalized legacy usernames";
            $lines[] = "-- Generated: " . now()->toDateTimeString();
            $lines[] = "-- " . count($diffs) . " row(s) affected";
            $lines[] = "SET NAMES utf8mb4;";
            $lines[] = "USE `projekti_2026_skinbase`;";
            $lines[] = "START TRANSACTION;";
            $lines[] = '';
            foreach ($diffs as $d) {
                $newUsername    = addslashes($d['new_username']);
                $legacyUsername = addslashes($d['legacy_username']);
                $lines[] = "-- id={$d['user_id']}  old='{$newUsername}'  ->  new='{$legacyUsername}'";
                $lines[] = "UPDATE `users` SET `username` = '{$legacyUsername}', `updated_at` = NOW() WHERE `id` = {$d['user_id']} AND `username` = '{$newUsername}';";
            }
            $lines[] = '';
            $lines[] = 'COMMIT;';

            $written = file_put_contents($fixSqlPath, implode("\n", $lines) . "\n");
            if ($written === false) {
                $this->error("Could not write SQL fix file: {$fixSqlPath}");
            } else {
                $this->info("Username fix SQL written to: {$fixSqlPath}");
            }
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    private function sqlString(?string $value): string
    {
        if ($value === null || trim($value) === '') {
            return 'NULL';
        }
        // Escape single-quotes and backslashes for SQL.
        $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $value);
        return "'{$escaped}'";
    }

    private function sqlDate($value, ?string $fallback): ?string
    {
        if (! $value) {
            return $fallback;
        }
        try {
            return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
        } catch (\Throwable) {
            return $fallback;
        }
    }

    private function safeInt($value): int
    {
        $n = (int) $value;
        return $n < 0 ? 0 : $n;
    }

    private function normalizeLegacyGender(?string $value): ?string
    {
        return match (strtolower(trim((string) $value))) {
            'm', 'male'   => 'M',
            'f', 'female' => 'F',
            default       => null,
        };
    }
}
