<?php

namespace App\Console\Commands;

use App\Models\Company;
use App\Models\User;
use App\Support\Requisitos;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Throwable;

/**
 * Confere, item a item, se o servidor atende ao que o sistema precisa.
 *
 * Existe para transformar o manual de instalação em algo verificável: em vez
 * de confiar que a hospedagem tem `soap` ou que o cron foi criado, o comando
 * responde. Rode depois de instalar e sempre que algo parar de funcionar.
 */
class DiagnosticoCommand extends Command
{
    protected $signature = 'xml:diagnostico {--sem-cores : Saída sem formatação, para colar em chamado}';

    protected $description = 'Verifica requisitos, permissões, banco, filas e agendador';

    private int $falhas = 0;

    private int $alertas = 0;

    public function handle(): int
    {
        $this->newLine();
        $this->line('  <options=bold>Diagnóstico do XML Monitor</>');
        $this->line('  <fg=gray>'.now()->format('d/m/Y H:i').' · '.php_uname('s').'</>');

        $this->secao('Ambiente');
        $this->verificarPhp();
        $this->verificarExtensoes();

        $this->secao('Aplicação');
        $this->verificarAplicacao();

        $this->secao('Banco de dados');
        $this->verificarBanco();

        $this->secao('Armazenamento');
        $this->verificarArmazenamento();

        $this->secao('Monitor, filas e agendador');
        $this->verificarAgendador();
        $this->verificarDados();

        $this->newLine();

        if ($this->falhas > 0) {
            $this->components->error(sprintf('%d problema(s) impedem o funcionamento. Corrija antes de usar.', $this->falhas));

            return self::FAILURE;
        }

        if ($this->alertas > 0) {
            $this->components->warn(sprintf('Tudo essencial em ordem, com %d ponto(s) de atenção.', $this->alertas));

            return self::SUCCESS;
        }

        $this->components->info('Tudo certo. Sistema pronto para operar.');

        return self::SUCCESS;
    }

    /* ------------------------------------------------------------ Verificações */

    private function verificarPhp(): void
    {
        $this->checar(
            'PHP 8.3 ou superior',
            version_compare(PHP_VERSION, '8.3.0', '>='),
            PHP_VERSION,
            'Selecione o PHP 8.3+ em "Select PHP Version" no cPanel.',
        );

        $limite = (int) ini_get('max_execution_time');
        $this->checar(
            'Tempo de execução do PHP',
            $limite === 0 || $limite >= 60,
            $limite === 0 ? 'sem limite' : $limite.'s',
            'Menos de 60s pode cortar sincronizações longas e pacotes .zip grandes.',
            critico: false,
        );

        $upload = $this->paraBytes(ini_get('upload_max_filesize'));
        $this->checar(
            'Envio de arquivos',
            $upload >= 1024 * 1024,
            ini_get('upload_max_filesize'),
            'O certificado A1 e a logo precisam de pelo menos 1M em upload_max_filesize.',
            critico: false,
        );
    }

    private function verificarExtensoes(): void
    {
        // Mesma lista usada pelo instalador web (App\Support\Requisitos).
        $faltando = array_filter(
            Requisitos::EXTENSOES,
            fn ($para, $ext) => ! extension_loaded($ext),
            ARRAY_FILTER_USE_BOTH,
        );

        $this->checar(
            'Extensões do PHP ('.count(Requisitos::EXTENSOES).' exigidas)',
            $faltando === [],
            $faltando === [] ? 'todas presentes' : count($faltando).' faltando',
            'Ative em "Select PHP Version → Extensions".',
        );

        foreach ($faltando as $ext => $para) {
            $this->line("      <fg=red>falta</> <options=bold>{$ext}</> — {$para}");
        }
    }

    private function verificarAplicacao(): void
    {
        $this->checar(
            'Chave da aplicação (APP_KEY)',
            filled(config('app.key')),
            filled(config('app.key')) ? 'definida' : 'ausente',
            'Rode: php artisan key:generate. Sem ela as senhas de certificado não são descriptografadas.',
        );

        $producao = app()->environment('production');

        $this->checar(
            'APP_DEBUG',
            ! $producao || ! config('app.debug'),
            (config('app.debug') ? 'ligado' : 'desligado').' · ambiente '.app()->environment(),
            'Em produção o APP_DEBUG precisa ser false: a tela de erro do Laravel mostra as credenciais do .env.',
            critico: $producao,
        );

        $this->checar(
            'APP_URL configurada',
            filled(config('app.url')) && config('app.url') !== 'http://localhost',
            config('app.url'),
            'Links de e-mail e de recuperação de senha usam este endereço.',
            critico: false,
        );

        $this->checar(
            'Fuso horário e idioma',
            config('app.timezone') === 'America/Sao_Paulo' && str_starts_with(config('app.locale'), 'pt'),
            config('app.timezone').' · '.config('app.locale'),
            'Use APP_TIMEZONE=America/Sao_Paulo e APP_LOCALE=pt_BR: as datas fiscais dependem disso.',
            critico: false,
        );

        $this->checar(
            'Assets compilados',
            file_exists(public_path('build/manifest.json')),
            file_exists(public_path('build/manifest.json')) ? 'presentes' : 'ausentes',
            'Rode "npm run build" na sua máquina e envie a pasta public/build.',
        );

        $mailConfigurado = config('mail.default') !== 'log' && filled(config('mail.mailers.smtp.host'));

        $this->checar(
            'Envio de e-mail',
            $mailConfigurado,
            config('mail.default'),
            'Sem SMTP a recuperação de senha não chega ao usuário.',
            critico: false,
        );
    }

    private function verificarBanco(): void
    {
        try {
            DB::connection()->getPdo();
            $conectou = true;
            $detalhe = DB::connection()->getDriverName().' · '.DB::connection()->getDatabaseName();
        } catch (Throwable $e) {
            $conectou = false;
            $detalhe = $e->getMessage();
        }

        $this->checar('Conexão com o banco', $conectou, $detalhe, 'Confira DB_DATABASE, DB_USERNAME e DB_PASSWORD no .env.');

        if (! $conectou) {
            return;
        }

        if (DB::connection()->getDriverName() === 'sqlite') {
            $this->checar(
                'Driver do banco',
                false,
                'sqlite',
                'SQLite serve para testes. Em produção use MySQL/MariaDB: o SQLite trava com acessos simultâneos.',
                critico: false,
            );
        }

        $tabelas = ['companies', 'users', 'notas_fiscais', 'nota_eventos', 'sync_logs', 'dfe_pendentes', 'site_settings', 'jobs', 'sessions', 'cache'];
        $faltando = array_values(array_filter($tabelas, fn ($t) => ! Schema::hasTable($t)));

        $this->checar(
            'Tabelas criadas ('.count($tabelas).')',
            $faltando === [],
            $faltando === [] ? 'todas presentes' : 'faltam: '.implode(', ', $faltando),
            'Rode: php artisan migrate --force',
        );
    }

    private function verificarArmazenamento(): void
    {
        foreach (['storage/framework' => storage_path('framework'), 'storage/logs' => storage_path('logs'), 'bootstrap/cache' => base_path('bootstrap/cache')] as $rotulo => $caminho) {
            $this->checar(
                "Escrita em {$rotulo}",
                is_dir($caminho) && is_writable($caminho),
                is_dir($caminho) ? (is_writable($caminho) ? 'gravável' : 'somente leitura') : 'não existe',
                'Rode: chmod -R 775 storage bootstrap/cache',
            );
        }

        foreach (['certificados' => 'certificados digitais A1', 'xmls' => 'documentos fiscais', 'branding' => 'logo e favicon'] as $disco => $para) {
            try {
                Storage::disk($disco)->put('.diagnostico', 'ok');
                $ok = Storage::disk($disco)->get('.diagnostico') === 'ok';
                Storage::disk($disco)->delete('.diagnostico');
            } catch (Throwable) {
                $ok = false;
            }

            $this->checar("Disco \"{$disco}\" ({$para})", $ok, $ok ? 'gravável' : 'sem escrita', 'Verifique as permissões de storage/app.');
        }

        // Os certificados jamais podem estar dentro do document root.
        $certDir = realpath(storage_path('app/certificados')) ?: storage_path('app/certificados');
        $publico = realpath(public_path()) ?: public_path();

        $this->checar(
            'Certificados fora da pasta pública',
            ! str_starts_with($certDir, $publico),
            $certDir,
            'GRAVE: mova a aplicação para fora do document root. Certificado exposto permite emitir nota em nome do cliente.',
        );
    }

    private function verificarAgendador(): void
    {
        $ultima = Cache::get('agendador.ultima_execucao');

        if ($ultima === null) {
            $this->checar(
                'Agendador (cron)',
                false,
                'nunca executou',
                'Crie no cPanel, a cada minuto: * * * * * '.PHP_BINARY.' '.base_path('artisan').' schedule:run >/dev/null 2>&1',
            );

            return;
        }

        $minutos = now()->diffInMinutes($ultima, false) * -1;

        $this->checar(
            'Agendador (cron)',
            $minutos <= 10,
            'última execução há '.round($minutos).' min',
            'O cron parou. Sem ele o monitor não busca XML e a fila não anda.',
        );
    }

    private function verificarDados(): void
    {
        if (! Schema::hasTable('users')) {
            return;
        }

        $master = User::where('role', User::ROLE_MASTER)->count();

        $this->checar(
            'Usuário master',
            $master > 0,
            $master.' cadastrado(s)',
            'Rode: php artisan xml:instalar --somente-master',
        );

        if (! Schema::hasTable('companies')) {
            return;
        }

        $empresas = Company::count();
        $comCertificado = Company::whereNotNull('cert_path')->count();
        $vencidos = Company::whereNotNull('cert_valid_to')->where('cert_valid_to', '<', now())->count();

        $this->checar(
            'Empresas cadastradas',
            true,
            $empresas.' empresa(s), '.$comCertificado.' com certificado',
            '',
            critico: false,
        );

        if ($vencidos > 0) {
            $this->checar(
                'Certificados vencidos',
                false,
                $vencidos.' empresa(s)',
                'O monitor dessas empresas está parado até a renovação.',
                critico: false,
            );
        }
    }

    /* ------------------------------------------------------------------ Saída */

    private function secao(string $titulo): void
    {
        $this->newLine();
        $this->line("  <options=bold;fg=cyan>{$titulo}</>");
    }

    private function checar(string $item, bool $ok, string $valor, string $ajuda, bool $critico = true): void
    {
        if ($ok) {
            $simbolo = '<fg=green>OK  </>';
        } elseif ($critico) {
            $simbolo = '<fg=red>FALHA</>';
            $this->falhas++;
        } else {
            $simbolo = '<fg=yellow>AVISO</>';
            $this->alertas++;
        }

        $this->line(sprintf('    %s  %-42s <fg=gray>%s</>', $simbolo, $item, $valor));

        if (! $ok && filled($ajuda)) {
            $this->line("           <fg=gray>{$ajuda}</>");
        }
    }

    private function paraBytes(string $valor): int
    {
        $unidade = strtolower(substr(trim($valor), -1));
        $numero = (int) $valor;

        return match ($unidade) {
            'g' => $numero * 1024 ** 3,
            'm' => $numero * 1024 ** 2,
            'k' => $numero * 1024,
            default => $numero,
        };
    }
}
