<?php

namespace App\Shared\Command;

use App\Shared\Context\EntityContext;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
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\Process\Process;

/**
 * خروجی SQL از دادهٔ یک محیط — بکاپ یا تحویلِ per-tenant بدون اینکه لازم باشد هر
 * کلینیک دیتابیس جدا داشته باشد.
 *
 * جدول‌های محیط‌دار از metadata خوانده می‌شوند، پس جدولی که فردا جفت tenant بگیرد
 * خودکار وارد خروجی می‌شود و این کامند از قلم نمی‌افتد.
 *
 *   php bin/console app:tenant:dump --tenant=clinic:12 --output=/tmp/clinic12.sql
 *
 * ⚠️ فرزندان aggregate (نوت‌های بیمار، آیتم‌های صورتحساب، …) ستون محیط ندارند و در
 * خروجی نمی‌آیند — رجوع به docs/architecture/tenancy.md.
 */
#[AsCommand(
    name: 'app:tenant:dump',
    description: 'Dump one tenant\'s rows (doctor or clinic) to a SQL file',
)]
class TenantDumpCommand extends Command
{
    public function __construct(
        private readonly Connection             $conn,
        private readonly EntityManagerInterface $em,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('tenant', null, InputOption::VALUE_REQUIRED, 'محیط به شکل doctor:<id> یا clinic:<id>')
            ->addOption('output', null, InputOption::VALUE_REQUIRED, 'مسیر فایل خروجی');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $tenant = (string) $input->getOption('tenant');
        $target = (string) $input->getOption('output');

        if ($target === '') {
            $io->error('گزینهٔ --output الزامی است.');

            return Command::FAILURE;
        }

        $parsed = $this->parseTenant($tenant);
        if ($parsed === null) {
            $io->error('قالب --tenant باید doctor:<id> یا clinic:<id> باشد؛ id عدد صحیح مثبت.');

            return Command::FAILURE;
        }

        [$type, $id] = $parsed;

        if (!$this->tenantExists($type, $id)) {
            $io->error(sprintf('محیطی با شناسهٔ %s:%d وجود ندارد.', $type, $id));

            return Command::FAILURE;
        }

        $tables = $this->tenantTables();
        $io->text(sprintf('%d جدول محیط‌دار برای %s:%d', count($tables), $type, $id));

        $written = $this->dump($tables, $type, $id, $target);
        if ($written === null) {
            $io->error('mysqldump شکست خورد؛ خروجی ناقص نوشته نشد.');

            return Command::FAILURE;
        }

        $io->success(sprintf('نوشته شد: %s (%d جدول)', $target, count($tables)));

        return Command::SUCCESS;
    }

    /**
     * مقدارها مستقیم داخل شرط --where و نام جدول می‌روند، پس فقط از allowlist و
     * تبدیل به int عبور می‌کنند — escape کافی نیست.
     *
     * @return array{0: string, 1: int}|null
     */
    private function parseTenant(string $raw): ?array
    {
        if (!preg_match('/^(doctor|clinic):(\d+)$/', trim($raw), $m)) {
            return null;
        }

        $id = (int) $m[2];

        return $id > 0 ? [$m[1], $id] : null;
    }

    private function tenantExists(string $type, int $id): bool
    {
        $table = $type === EntityContext::TYPE_CLINIC ? 'clinics' : 'doctors';

        return (bool) $this->conn->fetchOne("SELECT 1 FROM {$table} WHERE id = ?", [$id]);
    }

    /**
     * جدول‌هایی که جفت (entity_type, entity_id) دارند — همان معیاری که TenantFilter
     * با آن تصمیم می‌گیرد، تا این دو هرگز از هم واگرا نشوند.
     *
     * @return string[]
     */
    private function tenantTables(): array
    {
        $tables = [];
        foreach ($this->em->getMetadataFactory()->getAllMetadata() as $meta) {
            if ($meta->hasField('entityType') && $meta->hasField('entityId')) {
                $tables[] = $meta->getTableName();
            }
        }
        sort($tables);

        return $tables;
    }

    /** @param string[] $tables @return int|null تعداد جدول‌های نوشته‌شده، یا null در شکست */
    private function dump(array $tables, string $type, int $id, string $target): ?int
    {
        $params = $this->conn->getParams();
        $where  = sprintf("entity_type='%s' AND entity_id=%d", $type, $id);

        $handle = @fopen($target, 'w');
        if ($handle === false) {
            return null;
        }

        try {
            foreach ($tables as $table) {
                $process = new Process([
                    'mysqldump',
                    '--host=' . ($params['host'] ?? 'db'),
                    '--user=' . ($params['user'] ?? 'db'),
                    '--password=' . ($params['password'] ?? 'db'),
                    '--no-create-info',
                    '--skip-add-locks',
                    '--complete-insert',
                    '--where=' . $where,
                    (string) ($params['dbname'] ?? 'db'),
                    $table,
                ]);
                $process->setTimeout(300);
                $process->run();

                if (!$process->isSuccessful()) {
                    return null;
                }

                fwrite($handle, $process->getOutput());
            }
        } finally {
            fclose($handle);
        }

        return count($tables);
    }
}
