<?php

declare(strict_types=1);

namespace App\Console\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class RepairLegacyUserJoinDatesCommand extends Command
{
    /** @var array<string, bool> */
    private array $legacyTableExistsCache = [];

    protected $signature = 'skinbase:repair-user-join-dates
        {--chunk=500 : Number of users to process per batch}
        {--legacy-connection=legacy : Legacy database connection name}
        {--legacy-table=users : Legacy users table name}
        {--only-null : Update only users whose current created_at is null}
        {--dry-run : Preview join date updates without writing changes}';

    protected $description = 'Backfill current users.created_at from legacy users.joinDate';

    public function handle(): int
    {
        $chunk = max(1, (int) $this->option('chunk'));
        $legacyConnection = (string) $this->option('legacy-connection');
        $legacyTable = (string) $this->option('legacy-table');
        $onlyNull = (bool) $this->option('only-null');
        $dryRun = (bool) $this->option('dry-run');

        if (! $this->legacyTableExists($legacyConnection, $legacyTable)) {
            $this->error("Legacy table {$legacyConnection}.{$legacyTable} does not exist or the connection is unavailable.");

            return self::FAILURE;
        }

        if ($dryRun) {
            $this->warn('[DRY RUN] No changes will be written.');
        }

        $query = DB::table('users')->select(['id', 'created_at']);

        if ($onlyNull) {
            $query->whereNull('created_at');
        }

        $this->info('Scanning current users for legacy joinDate backfill.');

        $processed = 0;
        $matched = 0;
        $updated = 0;
        $unchanged = 0;
        $skipped = 0;

        $query
            ->chunkById($chunk, function (Collection $rows) use (
                &$processed,
                &$matched,
                &$updated,
                &$unchanged,
                &$skipped,
                $legacyConnection,
                $legacyTable,
                $dryRun
            ): void {
                $legacyById = $this->loadLegacyUsersForChunk($rows, $legacyConnection, $legacyTable);
                $activityById = $this->loadLegacyActivityDatesForChunk($rows, $legacyConnection);

                foreach ($rows as $row) {
                    $processed++;

                    $legacyMatch = $legacyById[(int) $row->id] ?? null;

                    if ($legacyMatch === null) {
                        $skipped++;
                        continue;
                    }

                    $matched++;

                    $legacyJoinDate = $this->parseLegacyJoinDate($legacyMatch->joinDate ?? null);
                    $dateSource = 'joinDate';

                    if ($legacyJoinDate === null) {
                        $activityFallback = $activityById[(int) $row->id] ?? null;
                        $legacyJoinDate = $activityFallback['date'] ?? null;
                        $dateSource = $activityFallback['source'] ?? 'activity';
                    }

                    if ($legacyJoinDate === null) {
                        $skipped++;
                        continue;
                    }

                    $currentCreatedAt = $this->parseCurrentDate($row->created_at ?? null);
                    if ($currentCreatedAt !== null && $currentCreatedAt->equalTo($legacyJoinDate)) {
                        $unchanged++;
                        continue;
                    }

                    if ($dryRun) {
                        $this->line(sprintf(
                            '[dry] Would update user id=%d created_at %s => %s (%s)',
                            (int) $row->id,
                            $currentCreatedAt?->toDateTimeString() ?? '<null>',
                            $legacyJoinDate->toDateTimeString(),
                            $dateSource
                        ));
                        $updated++;
                        continue;
                    }

                    $affected = DB::table('users')
                        ->where('id', (int) $row->id)
                        ->update([
                            'created_at' => $legacyJoinDate->toDateTimeString(),
                        ]);

                    if ($affected > 0) {
                        $updated += $affected;
                        $this->line(sprintf(
                            '[update] user id=%d created_at => %s (%s)',
                            (int) $row->id,
                            $legacyJoinDate->toDateTimeString(),
                            $dateSource
                        ));
                    }
                }
            }, 'id');

        $this->info(sprintf(
            'Finished. processed=%d matched=%d updated=%d unchanged=%d skipped=%d',
            $processed,
            $matched,
            $updated,
            $unchanged,
            $skipped
        ));

        if ($processed === 0) {
            $this->info('No users matched the requested scope.');
        }

        return self::SUCCESS;
    }

    private function legacyTableExists(string $connection, string $table): bool
    {
        $cacheKey = strtolower($connection . ':' . $table);

        if (array_key_exists($cacheKey, $this->legacyTableExistsCache)) {
            return $this->legacyTableExistsCache[$cacheKey];
        }

        try {
            return $this->legacyTableExistsCache[$cacheKey] = DB::connection($connection)->getSchemaBuilder()->hasTable($table);
        } catch (\Throwable) {
            return $this->legacyTableExistsCache[$cacheKey] = false;
        }
    }

    /**
     * @return array<int, object>
     */
    private function loadLegacyUsersForChunk(Collection $rows, string $legacyConnection, string $legacyTable): array
    {
        $legacyById = [];

        $ids = $rows
            ->pluck('id')
            ->map(static fn ($id): int => (int) $id)
            ->filter(static fn (int $id): bool => $id > 0)
            ->values()
            ->all();

        if ($ids !== []) {
            DB::connection($legacyConnection)
                ->table($legacyTable)
                ->select(['user_id', 'joinDate'])
                ->whereIn('user_id', $ids)
                ->get()
                ->each(function (object $legacyRow) use (&$legacyById): void {
                    $legacyById[(int) $legacyRow->user_id] = $legacyRow;
                });
        }

        return $legacyById;
    }

    /**
     * @return array<int, array{date: Carbon, source: string}>
     */
    private function loadLegacyActivityDatesForChunk(Collection $rows, string $legacyConnection): array
    {
        $activityById = [];

        $ids = $rows
            ->pluck('id')
            ->map(static fn ($id): int => (int) $id)
            ->filter(static fn (int $id): bool => $id > 0)
            ->values()
            ->all();

        if ($ids === []) {
            return $activityById;
        }

        if ($this->legacyTableExists($legacyConnection, 'wallz')) {
            $this->registerChunkActivityDates(
                $activityById,
                DB::connection($legacyConnection)
                    ->table('wallz')
                    ->selectRaw('user_id, MIN(datum) as first_at')
                    ->whereIn('user_id', $ids)
                    ->whereRaw("datum IS NOT NULL AND datum <> '0000-00-00 00:00:00'")
                    ->groupBy('user_id')
                    ->get(),
                'first upload'
            );
        }

        if ($this->legacyTableExists($legacyConnection, 'forum_topics')) {
            $this->registerChunkActivityDates(
                $activityById,
                DB::connection($legacyConnection)
                    ->table('forum_topics')
                    ->selectRaw('user_id, MIN(post_date) as first_at')
                    ->whereIn('user_id', $ids)
                    ->whereRaw("post_date <> '0000-00-00 00:00:00'")
                    ->groupBy('user_id')
                    ->get(),
                'first forum topic'
            );
        }

        if ($this->legacyTableExists($legacyConnection, 'forum_posts')) {
            $this->registerChunkActivityDates(
                $activityById,
                DB::connection($legacyConnection)
                    ->table('forum_posts')
                    ->selectRaw('user_id, MIN(post_date) as first_at')
                    ->whereIn('user_id', $ids)
                    ->whereRaw("post_date <> '0000-00-00 00:00:00'")
                    ->groupBy('user_id')
                    ->get(),
                'first forum post'
            );
        }

        if ($this->legacyTableExists($legacyConnection, 'artworks_comments')) {
            $this->registerChunkActivityDates(
                $activityById,
                DB::connection($legacyConnection)
                    ->table('artworks_comments')
                    ->selectRaw("user_id, MIN(TIMESTAMP(`date`, COALESCE(`time`, '00:00:00'))) as first_at")
                    ->whereIn('user_id', $ids)
                    ->whereRaw("`date` IS NOT NULL AND `date` <> '0000-00-00'")
                    ->groupBy('user_id')
                    ->get(),
                'first artwork comment'
            );
        }

        if ($this->legacyTableExists($legacyConnection, 'users_comments')) {
            $this->registerChunkActivityDates(
                $activityById,
                DB::connection($legacyConnection)
                    ->table('users_comments')
                    ->selectRaw("user_id, MIN(TIMESTAMP(`date`, COALESCE(`time`, '00:00:00'))) as first_at")
                    ->whereIn('user_id', $ids)
                    ->whereRaw("`date` IS NOT NULL AND `date` <> '0000-00-00'")
                    ->groupBy('user_id')
                    ->get(),
                'first profile comment'
            );
        }

        return $activityById;
    }

    /**
     * @param  array<int, array{date: Carbon, source: string}>  $activityById
     */
    private function registerChunkActivityDates(array &$activityById, iterable $rows, string $source): void
    {
        foreach ($rows as $row) {
            $candidate = $this->parseLegacyJoinDate($row->first_at ?? null);
            if ($candidate === null) {
                continue;
            }

            $userId = (int) ($row->user_id ?? 0);
            if ($userId <= 0) {
                continue;
            }

            $existing = $activityById[$userId]['date'] ?? null;
            if ($existing === null || $candidate->lt($existing)) {
                $activityById[$userId] = [
                    'date' => $candidate,
                    'source' => $source,
                ];
            }
        }
    }

    private function parseLegacyJoinDate(mixed $value): ?Carbon
    {
        $raw = trim((string) ($value ?? ''));

        if ($raw === '' || str_starts_with($raw, '0000-00-00')) {
            return null;
        }

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

    private function parseCurrentDate(mixed $value): ?Carbon
    {
        if ($value instanceof Carbon) {
            return $value;
        }

        $raw = trim((string) ($value ?? ''));
        if ($raw === '') {
            return null;
        }

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