<?php

namespace App\Console\Commands;

use App\Services\Storage\StorageConfig;
use App\Services\Storage\TenantStorage;
use Illuminate\Console\Command;
use Illuminate\Support\Str;

/**
 * Answers "is storage actually working?" end to end.
 *
 * Three separate questions, in order, because each one can fail while the next
 * would have passed and the settings page looks identical in every case:
 *
 *   1. WHERE is each field coming from — the saved settings row or `.env`?
 *   2. Can the saved credentials be DECRYPTED under the current APP_KEY? (An
 *      APP_KEY change is what caused the "The MAC is invalid." upload failures;
 *      the app now degrades to `.env` instead of throwing, which means the
 *      breakage is silent and needs to be reported explicitly.)
 *   3. Does a real write → read → public-URL → delete round-trip succeed?
 *
 * Nothing is written outside `diagnostics/`, and the test object is removed
 * again even when a later step fails. Secrets are never printed.
 */
class CheckStorage extends Command
{
    protected $signature = 'storage:check {--keep : Leave the test object in the bucket instead of deleting it}';

    protected $description = 'Verify the platform storage configuration with a real write/read/delete round-trip';

    public function handle(StorageConfig $config, TenantStorage $storage): int
    {
        $this->components->info('Storage configuration');

        if ($config->hasUnreadableStoredConfig()) {
            $this->components->error(
                'Saved credentials exist but CANNOT be decrypted with this APP_KEY — they were encrypted under a '
                .'different one. The .env values are being used instead. Re-save them in Master Control → Storage.'
            );
        }

        $origins = $config->fieldOrigins();
        $stored = $config->storedConfig();
        $env = $config->envConfig();
        $resolved = $config->resolve();

        $rows = [];
        foreach ($origins as $field => $origin) {
            $value = $resolved[$field] ?? null;

            $rows[] = [
                $field,
                match ($origin) {
                    'database' => 'Master Control',
                    'env' => '.env',
                    default => '— not set —',
                },
                $field === 'secret'
                    ? (filled($value) ? '(set, '.strlen((string) $value).' chars)' : '(none)')
                    : Str::limit((string) ($value ?? ''), 60),
            ];
        }

        $this->table(['Field', 'Resolved from', 'Effective value'], $rows);

        if ($resolved === null) {
            $this->components->error(
                'Storage is NOT configured — a bucket and an access key are the minimum. '
                .'Set them in Master Control → Storage, or as CLOUDFLARE_R2_* values in .env. Uploads will fail until then.'
            );

            return self::FAILURE;
        }

        $quota = $config->quotaBytes();
        $this->line('  Platform quota: '.($quota === null ? 'unlimited' : number_format($quota / 1073741824, 2).' GB'));
        $this->newLine();

        // Fields present in BOTH layers: worth calling out, because the .env
        // value an operator is reading is not the one in effect.
        $shadowed = array_keys(array_filter(
            $origins,
            fn (string $origin, string $field): bool => $origin === 'database' && filled($env[$field] ?? null),
            ARRAY_FILTER_USE_BOTH
        ));

        if ($shadowed !== []) {
            $this->components->warn(
                'These fields are set in BOTH places; the saved setting wins and .env is ignored: '.implode(', ', $shadowed).'.'
            );
        }

        return $this->roundTrip($storage);
    }

    /**
     * The part that cannot be faked: put an object, read it back byte for byte,
     * check the public host serves it, then delete it.
     */
    private function roundTrip(TenantStorage $storage): int
    {
        $this->components->info('Round-trip test');

        try {
            $disk = $storage->disk();
        } catch (\Throwable $e) {
            $this->components->error($e->getMessage());

            return self::FAILURE;
        }

        $path = 'diagnostics/storage-check-'.Str::random(12).'.txt';
        $body = 'vellix-point storage check '.now()->toIso8601String();
        $written = false;

        try {
            $this->components->task('write  '.$path, function () use ($disk, $path, $body, &$written): bool {
                $disk->put($path, $body);
                $written = true;

                return true;
            });

            $this->components->task('read back and compare', function () use ($disk, $path, $body): bool {
                if ($disk->get($path) !== $body) {
                    throw new \RuntimeException('The object read back did not match what was written.');
                }

                return true;
            });

            $url = $disk->url($path);
            $status = $this->probe($url);

            $this->components->twoColumnDetail('public URL', $url);
            $this->components->twoColumnDetail(
                'public URL status',
                match (true) {
                    $status === 200 => '<fg=green>200 OK</>',
                    $status === null => '<fg=yellow>unreachable from this server</>',
                    default => '<fg=red>HTTP '.$status.'</>',
                }
            );

            if (! $this->option('keep')) {
                $this->components->task('delete', function () use ($disk, $path, &$written): bool {
                    $disk->delete($path);
                    $written = false;

                    return true;
                });
            }

            $this->newLine();

            if ($status === 200) {
                $this->components->info('Storage is working. Uploads, reads and public delivery all succeeded.');

                return self::SUCCESS;
            }

            if ($status === null) {
                $this->components->warn(
                    'The bucket is writable and readable via the API, but this server could not reach the public URL, '
                    .'so browser delivery is unverified. Open the URL above manually to confirm.'
                );

                return self::SUCCESS;
            }

            $this->components->error(
                'The bucket is writable, but the public URL returned HTTP '.$status.'. Uploads will succeed while images '
                .'fail to load in the browser. The Public URL is per BUCKET — this one almost certainly belongs to a '
                .'different bucket than "'.($storage->isConfigured() ? 'the configured one' : 'unknown').'".'
            );

            return self::FAILURE;
        } catch (\Throwable $e) {
            if ($written) {
                try {
                    $disk->delete($path);
                } catch (\Throwable) {
                    $this->components->warn('Could not clean up the test object at '.$path.'.');
                }
            }

            $this->newLine();
            $this->components->error(get_class($e).': '.$e->getMessage());
            $this->line('  '.$this->hint($e));

            return self::FAILURE;
        }
    }

    /**
     * Translate the S3 client's least helpful failures into the thing to go and
     * check. These three account for most misconfigurations.
     */
    private function hint(\Throwable $e): string
    {
        $message = strtolower($e->getMessage());

        return match (true) {
            str_contains($message, 'signature') || str_contains($message, 'invalidaccesskey'),
            str_contains($message, '403'), str_contains($message, 'forbidden') => 'The access key or secret is wrong, or the R2 API token lacks Object Read & Write on this bucket.',
            str_contains($message, 'nosuchbucket'), str_contains($message, '404') => 'The bucket does not exist at this endpoint. Check the bucket name and that the endpoint is the right Cloudflare account.',
            str_contains($message, 'could not resolve'), str_contains($message, 'timed out'), str_contains($message, 'connection') => 'The endpoint host was unreachable from this server — DNS, firewall or outbound HTTPS is blocked.',
            str_contains($message, 'mac is invalid') => 'Saved credentials were encrypted under a different APP_KEY. Re-enter them in Master Control → Storage.',
            default => 'Check the bucket, endpoint and credentials in Master Control → Storage.',
        };
    }

    private function probe(string $url): ?int
    {
        try {
            $context = stream_context_create([
                'http' => ['method' => 'HEAD', 'timeout' => 10, 'ignore_errors' => true],
                'https' => ['method' => 'HEAD', 'timeout' => 10, 'ignore_errors' => true],
            ]);

            $headers = @get_headers($url, false, $context);

            if ($headers === false || ! isset($headers[0])) {
                return null;
            }

            preg_match('#HTTP/\S+\s+(\d{3})#', $headers[0], $matches);

            return isset($matches[1]) ? (int) $matches[1] : null;
        } catch (\Throwable) {
            return null;
        }
    }
}
