<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Support\UsernamePolicy;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;

final class CheckArtworkUserReferencesCommand extends Command
{
    protected $signature = 'artworks:check-user-refs
                            {--chunk=1000 : Number of artworks to process per chunk}
                            {--show-missing=25 : Maximum number of missing references to print}
                            {--artwork-id= : Only check/copy the user referenced by this specific artwork ID}
                            {--copy-missing-from-legacy : Copy missing referenced users from the legacy users table into the new users table using the same id}
                            {--create-placeholder : Create a placeholder tmpu{id} stub user when the legacy user cannot be found}
                            {--dry-run-copy : Preview legacy user copies without writing them}
                            {--legacy-connection=legacy : Legacy database connection name}
                            {--legacy-users-table=users : Legacy users table name}
                            {--json : Output the summary as JSON}';

    protected $description = 'Check that every artworks.user_id points to an existing users.id row.';

    public function handle(): int
    {
        $chunkSize = max(1, (int) $this->option('chunk'));
        $showMissing = max(0, (int) $this->option('show-missing'));
        $copyMissingFromLegacy = (bool) $this->option('copy-missing-from-legacy');
        $createPlaceholder = (bool) $this->option('create-placeholder');
        $dryRunCopy = (bool) $this->option('dry-run-copy');
        $legacyConnection = (string) $this->option('legacy-connection');
        $legacyUsersTable = (string) $this->option('legacy-users-table');

        $artworkId = $this->option('artwork-id') !== null ? (int) $this->option('artwork-id') : null;

        $this->line(sprintf('Auditing artworks.user_id references in chunks of %d...', $chunkSize));

        $audit = $this->auditArtworkUserReferences($chunkSize, $showMissing, $artworkId);
        $copySummary = null;

        if ($copyMissingFromLegacy) {
            $this->newLine();
            $this->line(sprintf(
                '%s missing referenced users from legacy connection "%s" table "%s".',
                $dryRunCopy ? 'Previewing copy of' : 'Copying',
                $legacyConnection,
                $legacyUsersTable,
            ));

            try {
                $copySummary = $this->copyMissingUsersFromLegacy(
                    array_keys($audit['missing_user_ids']),
                    $legacyConnection,
                    $legacyUsersTable,
                    $dryRunCopy,
                    $createPlaceholder,
                );
            } catch (\Throwable $exception) {
                $this->error($exception->getMessage());

                return self::FAILURE;
            }

            if (! $dryRunCopy && ($copySummary['copied'] ?? 0) > 0) {
                $audit = $this->auditArtworkUserReferences($chunkSize, $showMissing, $artworkId);
            }
        }

        if ((bool) $this->option('json')) {
            $payload = [
                'summary' => $audit['summary'],
                'sample_missing' => $audit['sample_missing'],
            ];

            if ($copySummary !== null) {
                $payload['copy_summary'] = $copySummary;
            }

            $this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

            return ((int) ($audit['summary']['missing'] ?? 0)) === 0 ? self::SUCCESS : self::FAILURE;
        }

        $this->renderAuditSummary($audit['summary'], $audit['sample_missing']);

        if ($copySummary !== null) {
            $this->renderCopySummary($copySummary);
        }

        return ((int) ($audit['summary']['missing'] ?? 0)) === 0 ? self::SUCCESS : self::FAILURE;
    }

    /**
     * @return array{
     *   summary: array{checked:int, valid:int, missing:int, null_user_ids:int},
     *   sample_missing: array<int, array{artwork_id:int, user_id:string, title:string}>,
     *   missing_user_ids: array<int, true>
     * }
     */
    private function auditArtworkUserReferences(int $chunkSize, int $showMissing, ?int $artworkId = null): array
    {
        $checked = 0;
        $valid = 0;
        $missing = 0;
        $nullUserIds = 0;
        $sampleRows = [];
        $missingUserIds = [];

        DB::table('artworks')
            ->leftJoin('users', 'users.id', '=', 'artworks.user_id')
            ->select([
                'artworks.id',
                'artworks.user_id',
                'artworks.title',
                DB::raw('users.id as matched_user_id'),
            ])
            ->when($artworkId !== null, fn ($q) => $q->where('artworks.id', $artworkId))
            ->orderBy('artworks.id')
            ->chunkById($chunkSize, function ($artworks) use (&$checked, &$valid, &$missing, &$nullUserIds, &$sampleRows, &$missingUserIds, $showMissing): void {
                foreach ($artworks as $artwork) {
                    $checked++;

                    if ($artwork->matched_user_id !== null) {
                        $valid++;
                        continue;
                    }

                    $missing++;

                    if ($artwork->user_id === null) {
                        $nullUserIds++;
                    } else {
                        $missingUserIds[(int) $artwork->user_id] = true;
                    }

                    if (count($sampleRows) < $showMissing) {
                        $sampleRows[] = [
                            'artwork_id' => (int) $artwork->id,
                            'user_id' => $artwork->user_id === null ? '[null]' : (string) $artwork->user_id,
                            'title' => (string) ($artwork->title ?? ''),
                        ];
                    }
                }

                if ($this->isVerboseOutput()) {
                    $this->line(sprintf(
                        '  audited %d artworks so far; missing=%d, null_user_id=%d.',
                        $checked,
                        $missing,
                        $nullUserIds,
                    ));
                }
            }, 'artworks.id', 'id');

        return [
            'summary' => [
                'checked' => $checked,
                'valid' => $valid,
                'missing' => $missing,
                'null_user_ids' => $nullUserIds,
            ],
            'sample_missing' => $sampleRows,
            'missing_user_ids' => $missingUserIds,
        ];
    }

    /**
     * @param  array<int, int|string>  $legacyIds
     * @return array<string, mixed>
     */
    private function copyMissingUsersFromLegacy(array $legacyIds, string $legacyConnection, string $legacyUsersTable, bool $dryRun, bool $createPlaceholder = false): array
    {
        $result = [
            'requested_users' => count($legacyIds),
            'copied' => 0,
            'placeholders_created' => 0,
            'would_copy' => 0,
            'conflicts' => 0,
            'not_found_in_legacy' => 0,
            'errors' => 0,
            'dry_run' => $dryRun,
            'sample_copied_ids' => [],
            'sample_placeholder_ids' => [],
            'sample_conflict_ids' => [],
            'sample_not_found_ids' => [],
            'sample_error_messages' => [],
        ];

        if ($legacyIds === []) {
            if ($this->isVerboseOutput()) {
                $this->line('No missing non-null user ids were found to copy from the legacy users table.');
            }

            return $result;
        }

        $this->ensureLegacyConnectionIsUsable($legacyConnection, $legacyUsersTable);

        $normalizedLegacyIds = array_values(array_unique(array_map('intval', $legacyIds)));

        foreach (array_chunk($normalizedLegacyIds, 200) as $chunkIndex => $chunk) {
            $legacyRows = DB::connection($legacyConnection)
                ->table($legacyUsersTable)
                ->whereIn('user_id', $chunk)
                ->get()
                ->keyBy(fn (object $row): int => (int) $row->user_id);

            if ($this->isVerboseOutput()) {
                $this->line(sprintf(
                    '  processing legacy chunk %d with %d requested ids; found %d legacy rows.',
                    $chunkIndex + 1,
                    count($chunk),
                    $legacyRows->count(),
                ));
            }

            foreach ($chunk as $legacyId) {
                if (DB::table('users')->where('id', $legacyId)->exists()) {
                    $result['conflicts']++;
                    if (count($result['sample_conflict_ids']) < 10) {
                        $result['sample_conflict_ids'][] = $legacyId;
                    }

                    if ($this->isVerboseOutput()) {
                        $this->warn(sprintf('[skip-conflict] user #%d already exists in the new users table.', $legacyId));
                    }

                    continue;
                }

                $legacyUser = $legacyRows->get($legacyId);

                if (! $legacyUser) {
                    $result['not_found_in_legacy']++;
                    if (count($result['sample_not_found_ids']) < 10) {
                        $result['sample_not_found_ids'][] = $legacyId;
                    }

                    if ($this->isVerboseOutput()) {
                        $this->warn(sprintf(
                            '[missing-legacy] user #%d was not found in %s.%s.',
                            $legacyId,
                            $legacyConnection,
                            $legacyUsersTable,
                        ));
                    }

                    if ($createPlaceholder) {
                        if ($dryRun) {
                            $result['would_copy']++;
                            $this->line(sprintf('[dry-run] would create placeholder tmpu%d for user #%d', $legacyId, $legacyId));
                        } else {
                            try {
                                $this->createPlaceholderUser($legacyId);
                                $result['placeholders_created']++;
                                if (count($result['sample_placeholder_ids']) < 10) {
                                    $result['sample_placeholder_ids'][] = $legacyId;
                                }
                                $this->info(sprintf('[placeholder] created tmpu%d for user #%d', $legacyId, $legacyId));
                            } catch (\Throwable $exception) {
                                $result['errors']++;
                                $message = sprintf('#%d (placeholder): %s', $legacyId, $exception->getMessage());
                                if (count($result['sample_error_messages']) < 10) {
                                    $result['sample_error_messages'][] = $message;
                                }
                                $this->error('[placeholder-error] ' . $message);
                            }
                        }
                    }

                    continue;
                }

                if ($dryRun) {
                    $result['would_copy']++;
                    if (count($result['sample_copied_ids']) < 10) {
                        $result['sample_copied_ids'][] = $legacyId;
                    }

                    $this->line(sprintf('[dry-run] would import legacy user %s', $this->describeLegacyUser($legacyUser, $legacyId)));

                    continue;
                }

                try {
                    $this->importLegacyUserBySameId($legacyUser, $legacyId);
                    $result['copied']++;
                    if (count($result['sample_copied_ids']) < 10) {
                        $result['sample_copied_ids'][] = $legacyId;
                    }

                    $this->info(sprintf('[copied] imported legacy user %s', $this->describeLegacyUser($legacyUser, $legacyId)));
                } catch (\Throwable $exception) {
                    $result['errors']++;
                    $message = sprintf('#%d: %s', $legacyId, $exception->getMessage());
                    if (count($result['sample_error_messages']) < 10) {
                        $result['sample_error_messages'][] = $message;
                    }

                    $this->error('[copy-error] ' . $message);
                }
            }
        }

        return $result;
    }

    private function importLegacyUserBySameId(object $legacyUser, int $legacyId): void
    {
        $now = now();
        $username = $this->resolveImportUsername($legacyUser, $legacyId);
        $email = $this->resolveImportEmail($legacyUser, $legacyId);
        $name = (string) ($this->legacyField($legacyUser, 'real_name') ?: $username);
        $createdAt = $this->parseLegacyDate($this->legacyField($legacyUser, 'joinDate')) ?? $now;
        $lastVisitAt = $this->parseLegacyDate($this->legacyField($legacyUser, 'LastVisit'));
        $countryCode = $this->legacyField($legacyUser, 'country_code');

        DB::transaction(function () use ($legacyId, $legacyUser, $username, $email, $name, $createdAt, $lastVisitAt, $countryCode, $now): void {
            if (DB::table('users')->where('id', $legacyId)->exists()) {
                throw new RuntimeException(sprintf('Conflict: user id %d already exists in the new users table.', $legacyId));
            }

            DB::table('users')->insert([
                'id' => $legacyId,
                'username' => $username,
                'username_changed_at' => $now,
                'name' => $name,
                'email' => $email,
                'password' => Hash::make(Str::random(64)),
                'is_active' => (int) ($this->legacyField($legacyUser, 'active') ?? 1) === 1,
                'needs_password_reset' => true,
                'role' => 'user',
                'legacy_password_algo' => null,
                'last_visit_at' => $lastVisitAt,
                'created_at' => $createdAt,
                'updated_at' => $now,
            ]);

            if (Schema::hasTable('user_profiles')) {
                DB::table('user_profiles')->updateOrInsert(
                    ['user_id' => $legacyId],
                    [
                        'bio' => $this->legacyField($legacyUser, 'about_me') ?: $this->legacyField($legacyUser, 'description'),
                        'country' => $this->legacyField($legacyUser, 'country'),
                        'country_code' => is_string($countryCode) && $countryCode !== '' ? substr($countryCode, 0, 2) : null,
                        'website' => $this->legacyField($legacyUser, 'web'),
                        'gender' => $this->normalizeLegacyGender($this->legacyField($legacyUser, 'gender')),
                        'created_at' => $now,
                        'updated_at' => $now,
                    ],
                );
            }

            if (Schema::hasTable('user_statistics')) {
                DB::table('user_statistics')->updateOrInsert(
                    ['user_id' => $legacyId],
                    [
                        'uploads_count'                => 0,
                        'downloads_received_count'     => 0,
                        'artwork_views_received_count' => 0,
                        'awards_received_count'        => 0,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ],
                );
            }
        });
    }

    private function createPlaceholderUser(int $legacyId): void
    {
        $now = now();
        $username = $this->uniquePlaceholderUsername($legacyId);
        $email = $username . '@users.skinbase.org';

        DB::transaction(function () use ($legacyId, $username, $email, $now): void {
            if (DB::table('users')->where('id', $legacyId)->exists()) {
                throw new RuntimeException(sprintf('Conflict: user id %d already exists in the new users table.', $legacyId));
            }

            DB::table('users')->insert([
                'id'                   => $legacyId,
                'username'             => $username,
                'username_changed_at'  => $now,
                'name'                 => $username,
                'email'                => $email,
                'password'             => Hash::make(Str::random(64)),
                'is_active'            => false,
                'needs_password_reset' => true,
                'role'                 => 'user',
                'legacy_password_algo' => null,
                'last_visit_at'        => null,
                'created_at'           => $now,
                'updated_at'           => $now,
            ]);

            if (Schema::hasTable('user_profiles')) {
                DB::table('user_profiles')->updateOrInsert(
                    ['user_id' => $legacyId],
                    ['created_at' => $now, 'updated_at' => $now],
                );
            }

            if (Schema::hasTable('user_statistics')) {
                DB::table('user_statistics')->updateOrInsert(
                    ['user_id' => $legacyId],
                    [
                        'uploads_count'                => 0,
                        'downloads_received_count'     => 0,
                        'artwork_views_received_count' => 0,
                        'awards_received_count'        => 0,
                        'created_at'                   => $now,
                        'updated_at'                   => $now,
                    ],
                );
            }
        });
    }

    private function resolveImportUsername(object $legacyUser, int $legacyId): string
    {
        $rawUsername = (string) ($this->legacyField($legacyUser, 'uname') ?: ('user' . $legacyId));
        $username = $this->sanitizeUsername($rawUsername);

        if (! $this->usernameExists($username, $legacyId)) {
            return $username;
        }

        return $this->uniquePlaceholderUsername($legacyId);
    }

    private function sanitizeUsername(string $username): string
    {
        return UsernamePolicy::sanitizeLegacy($username);
    }

    private function usernameExists(string $username, int $ignoreUserId): bool
    {
        return DB::table('users')
            ->whereRaw('LOWER(username) = ?', [strtolower($username)])
            ->where('id', '!=', $ignoreUserId)
            ->exists();
    }

    private function uniquePlaceholderUsername(int $legacyId): string
    {
        $base = 'tmpu' . $legacyId;
        $candidate = $base;
        $suffix = 1;

        while ($this->usernameExists($candidate, $legacyId)) {
            $suffixStr = (string) $suffix;
            $candidate = substr($base, 0, max(1, 20 - strlen($suffixStr))) . $suffixStr;
            $suffix++;
        }

        return $candidate;
    }

    private function renderAuditSummary(array $summary, array $sampleRows): void
    {
        $this->info(sprintf(
            'Checked %d artworks: %d valid, %d missing user references, %d null user_id values.',
            (int) ($summary['checked'] ?? 0),
            (int) ($summary['valid'] ?? 0),
            (int) ($summary['missing'] ?? 0),
            (int) ($summary['null_user_ids'] ?? 0),
        ));

        if ($sampleRows !== []) {
            $this->newLine();
            $this->warn('Sample missing references:');
            $this->table(['Artwork ID', 'user_id', 'Title'], array_map(
                static fn (array $row): array => [$row['artwork_id'], $row['user_id'], $row['title']],
                $sampleRows,
            ));
        }

        if ((int) ($summary['missing'] ?? 0) === 0) {
            $this->info('No missing user references found in artworks.user_id.');
        } else {
            $this->error('Found artworks with missing user references.');
        }
    }

    private function renderCopySummary(array $copySummary): void
    {
        $this->newLine();
        $this->info(sprintf(
            'Legacy copy summary: requested %d users, copied %d, placeholders %d, would copy %d, conflicts %d, not found in legacy %d, errors %d.',
            (int) ($copySummary['requested_users'] ?? 0),
            (int) ($copySummary['copied'] ?? 0),
            (int) ($copySummary['placeholders_created'] ?? 0),
            (int) ($copySummary['would_copy'] ?? 0),
            (int) ($copySummary['conflicts'] ?? 0),
            (int) ($copySummary['not_found_in_legacy'] ?? 0),
            (int) ($copySummary['errors'] ?? 0),
        ));

        if (($copySummary['sample_copied_ids'] ?? []) !== []) {
            $this->line('Copied or would-copy user ids: ' . implode(', ', $copySummary['sample_copied_ids']));
        }

        if (($copySummary['sample_placeholder_ids'] ?? []) !== []) {
            $this->line('Placeholder users created for ids: ' . implode(', ', $copySummary['sample_placeholder_ids']));
        }

        if (($copySummary['sample_conflict_ids'] ?? []) !== []) {
            $this->warn('Conflicts: user ids already present in new DB: ' . implode(', ', $copySummary['sample_conflict_ids']));
        }

        if (($copySummary['sample_not_found_ids'] ?? []) !== []) {
            $this->warn('Not found in legacy: ' . implode(', ', $copySummary['sample_not_found_ids']));
        }

        if (($copySummary['sample_error_messages'] ?? []) !== []) {
            foreach ($copySummary['sample_error_messages'] as $message) {
                $this->warn($message);
            }
        }
    }

    private function ensureLegacyConnectionIsUsable(string $connection, string $table): void
    {
        try {
            DB::connection($connection)->getPdo();
        } catch (\Throwable $exception) {
            throw new RuntimeException(sprintf('Legacy DB connection "%s" is not configured or reachable.', $connection), 0, $exception);
        }

        if (! DB::connection($connection)->getSchemaBuilder()->hasTable($table)) {
            throw new RuntimeException(sprintf('Legacy users table "%s" was not found on connection "%s".', $table, $connection));
        }
    }

    private function resolveImportEmail(object $legacyUser, int $legacyId): string
    {
        $rawEmail = strtolower(trim((string) ($this->legacyField($legacyUser, 'email') ?? '')));
        $candidate = $rawEmail !== ''
            ? $rawEmail
            : ($this->sanitizeEmailLocal((string) ($this->legacyField($legacyUser, 'uname') ?: ('user' . $legacyId))) . '@users.skinbase.org');

        return $this->uniqueEmailCandidate($candidate, $legacyId);
    }

    private function uniqueEmailCandidate(string $email, int $legacyId): string
    {
        $candidate = strtolower(trim($email));
        $suffix = 1;

        while ($candidate === '' || DB::table('users')->whereRaw('LOWER(email) = ?', [$candidate])->where('id', '!=', $legacyId)->exists()) {
            $parts = explode('@', $email, 2);
            $local = $this->sanitizeEmailLocal($parts[0] ?? ('user' . $legacyId));
            $domain = $parts[1] ?? 'users.skinbase.org';
            $candidate = $local . '+' . $suffix . '@' . $domain;
            $suffix++;
        }

        return $candidate;
    }

    private function sanitizeEmailLocal(string $value): string
    {
        $local = strtolower(trim(Str::ascii($value)));
        $local = preg_replace('/[^a-z0-9._-]/', '-', $local) ?: 'user';

        return trim($local, '.-') ?: 'user';
    }

    private function normalizeLegacyGender(mixed $value): ?string
    {
        $normalized = strtoupper(trim((string) ($value ?? '')));

        return match ($normalized) {
            'M', 'MALE', 'MAN', 'BOY' => 'M',
            'F', 'FEMALE', 'WOMAN', 'GIRL' => 'F',
            default => null,
        };
    }

    private function isVerboseOutput(): bool
    {
        return $this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
    }

    private function describeLegacyUser(object $legacyUser, int $legacyId): string
    {
        $username = trim((string) ($this->legacyField($legacyUser, 'uname') ?? ''));
        $name = trim((string) ($this->legacyField($legacyUser, 'real_name') ?? ''));
        $email = trim((string) ($this->legacyField($legacyUser, 'email') ?? ''));

        return sprintf(
            '#%d username=%s name=%s email=%s',
            $legacyId,
            $username !== '' ? '@' . $username : '[missing]',
            $name !== '' ? '"' . $name . '"' : '[missing]',
            $email !== '' ? '<' . $email . '>' : '[missing]',
        );
    }

    private function parseLegacyDate(mixed $value): ?Carbon
    {
        if (! is_string($value) || trim($value) === '' || str_starts_with($value, '0000-00-00')) {
            return null;
        }

        try {
            return Carbon::parse($value);
        } catch (\Throwable) {
            return null;
        }
    }

    private function legacyField(object $row, string $field): mixed
    {
        return property_exists($row, $field) ? $row->{$field} : null;
    }
}