<?php

declare(strict_types=1);

namespace App\Command;

use App\Module\Core\Domain\Entity\User;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

use function sprintf;

/**
 * Recreates user rows that are still referenced by other tables but no longer
 * exist (legacy hard-deletes performed before the foreign keys enforced
 * ON DELETE SET NULL / CASCADE). Recreated accounts are archived: their data
 * (tasks, time entries, notifications…) is preserved and references become
 * valid again, fixing the serialization crash (EntityNotFoundException), but
 * the accounts cannot log in and are hidden from selectable user lists.
 *
 * Idempotent and non-destructive — nothing is deleted.
 */
#[AsCommand(
    name: 'app:restore-missing-users',
    description: 'Recreate (as archived) users that are still referenced but were hard-deleted, to restore referential integrity',
)]
final class RestoreMissingUsersCommand extends Command
{
    public function __construct(
        private readonly Connection $connection,
        private readonly UserPasswordHasherInterface $passwordHasher,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'List missing user ids without recreating them');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io     = new SymfonyStyle($input, $output);
        $dryRun = (bool) $input->getOption('dry-run');

        // 1. Discover every (table, column) that references "user" via a foreign key.
        $references = $this->connection->fetchAllAssociative(<<<'SQL'
            SELECT t.relname AS child_table, a.attname AS child_col
            FROM pg_constraint c
            JOIN pg_class t ON t.oid = c.conrelid
            JOIN pg_class rt ON rt.oid = c.confrelid
            JOIN unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
            JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
            WHERE c.contype = 'f' AND rt.relname = 'user'
            ORDER BY t.relname, a.attname
            SQL);

        // 2. Collect distinct orphan ids across all those columns.
        $missingIds = [];
        foreach ($references as $ref) {
            $table = $ref['child_table'];
            $col   = $ref['child_col'];

            $ids = $this->connection->fetchFirstColumn(sprintf(
                'SELECT DISTINCT %1$s FROM %2$s WHERE %1$s IS NOT NULL AND %1$s NOT IN (SELECT id FROM "user")',
                $this->connection->quoteIdentifier($col),
                $this->connection->quoteIdentifier($table),
            ));

            foreach ($ids as $id) {
                $missingIds[(int) $id] = true;
            }
        }

        $missingIds = array_keys($missingIds);
        sort($missingIds);

        $io->section(sprintf('%d foreign-key column(s) scanned', count($references)));

        if ([] === $missingIds) {
            $io->success('No missing users referenced. Nothing to restore.');

            return Command::SUCCESS;
        }

        $io->writeln(sprintf('Missing user id(s): %s', implode(', ', $missingIds)));

        if ($dryRun) {
            $io->note('Dry run — no user recreated.');

            return Command::SUCCESS;
        }

        // 3. Recreate each missing user as an archived placeholder, preserving its id.
        $hash    = $this->passwordHasher->hashPassword(new User(), bin2hex(random_bytes(16)));
        $created = 0;

        foreach ($missingIds as $id) {
            $inserted = $this->connection->executeStatement(
                <<<'SQL'
                    INSERT INTO "user"
                        (id, username, first_name, last_name, roles, password, created_at,
                         is_employee, work_time_ratio, annual_leave_days, reference_period_start,
                         initial_leave_balance, archived)
                    VALUES
                        (:id, :username, :firstName, :lastName, :roles, :password, NOW(),
                         false, 1.0, 25.0, '06-01', 0.0, true)
                    ON CONFLICT (id) DO NOTHING
                    SQL,
                [
                    'id'        => $id,
                    'username'  => sprintf('deleted-user-%d', $id),
                    'firstName' => 'Compte',
                    'lastName'  => sprintf('supprimé #%d', $id),
                    'roles'     => json_encode(['ROLE_USER'], JSON_THROW_ON_ERROR),
                    'password'  => $hash,
                ],
            );

            // ON CONFLICT may have skipped an already-present row — only count real inserts.
            if ($inserted > 0) {
                ++$created;
                $io->writeln(sprintf('  ✓ user #%d recreated (archived)', $id));
            } else {
                $io->writeln(sprintf('  • user #%d already present — skipped', $id));
            }
        }

        $io->success(sprintf('%d user(s) restored as archived. References are valid again — no data lost.', $created));

        return Command::SUCCESS;
    }
}
