<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class SyncGoogleFonts extends Command
{
    protected $signature = 'fonts:sync-google {--count=50 : Number of popular fonts to fetch} {--weights=400,700 : Comma separated numeric font weights to download} {--italic : Also download italic variants where available} {--force : Re-download files even if they exist} {--path= : Optional custom output directory (defaults to public/fonts/google)} {--family= : Download specific family name(s), comma separated (case-insensitive)} {--category= : Filter by category, comma separated (e.g. sans-serif,serif)} {--prune : Remove local font families not included in this sync} {--dry-run : Show what would be downloaded without writing files}';

    protected $description = 'Download the most popular Google Fonts to the local public/fonts/google directory and generate a manifest + CSS file.';

    private const API_ENDPOINT = 'https://www.googleapis.com/webfonts/v1/webfonts';

    public function handle(): int
    {
        $apiKey = config('services.google_fonts.key');

        if (! $apiKey) {
            $this->error('GOOGLE_FONTS_API_KEY is missing in the environment.');

            return self::FAILURE;
        }

        $count = max(1, (int) $this->option('count'));
        $weights = $this->prepareWeights($this->option('weights'));
        $includeItalic = (bool) $this->option('italic');
        $force = (bool) $this->option('force');
        $dryRun = (bool) $this->option('dry-run');
        $families = $this->normalizeFamilyOption($this->option('family'));
        $categories = $this->prepareCategories($this->option('category'));
        $prune = (bool) $this->option('prune');

        $pathOption = $this->option('path');
        $basePath = $pathOption
            ? (Str::startsWith($pathOption, DIRECTORY_SEPARATOR) ? $pathOption : base_path($pathOption))
            : public_path('fonts/google');

        if (count($families)) {
            $label = count($families) > 1 ? 'families' : 'family';
            $this->info(sprintf('Fetching Google Font %s "%s" (weights: %s, italic: %s)...', $label, implode(', ', $families), implode(', ', $weights), $includeItalic ? 'yes' : 'no'));
        } else {
            $this->info(sprintf('Fetching top %d Google Fonts (weights: %s, italic: %s)...', $count, implode(', ', $weights), $includeItalic ? 'yes' : 'no'));
        }

        if (count($categories)) {
            $this->line('Category filter: '.implode(', ', $categories));
        }

        if ($dryRun) {
            $this->warn('Dry run enabled: no files will be written.');
        }

        $response = Http::retry(2, 200)
            ->timeout(30)
            ->get(self::API_ENDPOINT, [
                'key' => $apiKey,
                'sort' => 'popularity',
            ]);

        if (! $response->ok()) {
            $this->error(sprintf('Google Fonts API failed: %s', $response->body()));

            return self::FAILURE;
        }

        $items = Arr::get($response->json(), 'items', []);
        if (! is_array($items) || ! count($items)) {
            $this->warn('Google Fonts API returned no items.');

            return self::SUCCESS;
        }

        $items = $this->filterFonts($items, $families, $categories);

        if (count($families)) {
            $matchedFamilies = collect($items)
                ->pluck('family')
                ->filter()
                ->map(fn ($family) => strtolower((string) $family))
                ->unique()
                ->all();

            $missingFamilies = collect($families)
                ->filter(fn ($family) => ! in_array(strtolower($family), $matchedFamilies, true))
                ->values()
                ->all();

            if (count($missingFamilies)) {
                $label = count($missingFamilies) > 1 ? 'families' : 'family';
                $this->error(sprintf('Font %s not found: %s.', $label, implode(', ', $missingFamilies)));

                return self::FAILURE;
            }
        }

        if (! count($items)) {
            $this->warn('No fonts matched the provided filters.');

            return self::SUCCESS;
        }

        $selected = count($families) ? $items : array_slice($items, 0, $count);
        $manifestFonts = [];
        $filesystem = new Filesystem;

        if (! $dryRun) {
            File::ensureDirectoryExists($basePath);
        }

        foreach ($selected as $index => $font) {
            if (! is_array($font) || ! isset($font['family'])) {
                continue;
            }

            $family = (string) $font['family'];
            $slug = Str::slug($family);
            $familyDir = $basePath.DIRECTORY_SEPARATOR.$slug;
            if (! $dryRun) {
                File::ensureDirectoryExists($familyDir);
            }

            $variantMap = $this->buildVariantMap($font, $weights, $includeItalic);
            if (! count($variantMap)) {
                $this->warn("Skipping {$family} (no matching variants)");

                continue;
            }

            $variants = [];
            foreach ($variantMap as $variantKey => $fileUrl) {
                $style = str_contains($variantKey, 'italic') ? 'italic' : 'normal';
                $weight = (int) preg_replace('/[^0-9]/', '', $variantKey) ?: 400;
                $extension = pathinfo(parse_url($fileUrl, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION) ?: 'woff2';
                $filename = sprintf('%s-%s-%s.%s', Str::studly($slug), $weight, $style, $extension);
                $targetPath = $familyDir.DIRECTORY_SEPARATOR.$filename;

                $alreadyExists = $filesystem->exists($targetPath);

                if ($dryRun) {
                    $this->line(sprintf('◦ DRY RUN: %s %s would %s (%s)', $family, $variantKey, $alreadyExists && ! $force ? 'reuse existing file' : 'download', $targetPath));
                } elseif (! $force && $alreadyExists) {
                    $this->line("✔  {$family} {$variantKey} already exists");
                } else {
                    $this->line("↓  Downloading {$family} {$variantKey}");
                    $fileResponse = Http::retry(2, 200)->timeout(30)->get($fileUrl);

                    if (! $fileResponse->ok()) {
                        $this->warn("  Skipped {$family} {$variantKey} (download failed)");

                        continue;
                    }

                    $filesystem->put($targetPath, $fileResponse->body());
                }

                $relativePath = sprintf('/fonts/google/%s/%s', $slug, $filename);
                $variants[] = [
                    'variant' => $variantKey,
                    'weight' => $weight,
                    'style' => $style,
                    'url' => $relativePath,
                ];
            }

            if (! count($variants)) {
                continue;
            }

            $manifestFonts[] = [
                'family' => $family,
                'slug' => $slug,
                'category' => $font['category'] ?? null,
                'variants' => $variants,
            ];
        }

        if ($dryRun) {
            $this->info(sprintf('Dry run complete: %d font families would be synced to %s', count($manifestFonts), $basePath));

            return self::SUCCESS;
        }

        $finalFonts = $manifestFonts;
        if (count($families) && ! $prune) {
            $existingFonts = $this->loadExistingManifestFonts($basePath);
            $finalFonts = $this->mergeManifestFonts($existingFonts, $manifestFonts);
        }

        if ($prune || ! count($families)) {
            $this->pruneStaleFamilies($basePath, $finalFonts);
        }

        $this->writeManifest($basePath, $finalFonts);
        $this->writeCss($basePath, $finalFonts);
        Cache::forget('fonts:manifest');

        $this->info(sprintf('Synced %d font families to %s', count($finalFonts), $basePath));

        return self::SUCCESS;
    }

    /**
     * @return array<int, string>
     */
    private function normalizeFamilyOption(?string $family): array
    {
        $family = trim((string) $family);
        if ($family === '') {
            return [];
        }

        $parts = array_filter(array_map('trim', explode(',', $family)));

        return collect($parts)
            ->unique(fn ($value) => strtolower((string) $value))
            ->values()
            ->all();
    }

    /**
     * @return array<int, string>
     */
    private function prepareCategories(?string $categories): array
    {
        $parts = array_filter(array_map('trim', explode(',', (string) $categories)));

        return array_values(array_unique(array_map(static function ($category) {
            $normalized = Str::of($category)->lower()->replace(' ', '-')->toString();

            return (string) $normalized;
        }, $parts)));
    }

    /**
     * @param  array<int, mixed>  $items
     * @param  array<int, string>  $families
     * @return array<int, mixed>
     */
    private function filterFonts(array $items, array $families, array $categories): array
    {
        $filtered = collect($items)
            ->filter(fn ($font) => is_array($font) && isset($font['family']))
            ->filter(function ($font) use ($categories) {
                if (! count($categories)) {
                    return true;
                }

                $category = strtolower((string) ($font['category'] ?? ''));

                return in_array($category, $categories, true);
            });

        if (count($families)) {
            $normalizedFamilies = collect($families)
                ->map(fn ($family) => strtolower((string) $family))
                ->all();

            $filtered = $filtered->filter(function ($font) use ($normalizedFamilies) {
                return in_array(strtolower((string) $font['family']), $normalizedFamilies, true);
            });
        }

        return $filtered->values()->all();
    }

    /**
     * @return array<int, int>
     */
    private function prepareWeights(string $weights): array
    {
        $parts = array_filter(array_map('trim', explode(',', $weights)));
        $numeric = array_map(fn ($weight) => (int) $weight, $parts);
        $numeric = array_filter($numeric, fn ($weight) => $weight >= 100 && $weight <= 900);

        return $numeric ? array_values(array_unique($numeric)) : [400, 700];
    }

    /**
     * @param  array<string, mixed>  $font
     * @return array<string, string>
     */
    private function buildVariantMap(array $font, array $weights, bool $includeItalic): array
    {
        $files = $font['files'] ?? [];
        if (! is_array($files)) {
            return [];
        }

        $variants = [];
        foreach ($weights as $weight) {
            $normalKey = $weight === 400 ? 'regular' : (string) $weight;
            $italicKey = $weight === 400 ? 'italic' : $weight.'italic';

            if (isset($files[$normalKey])) {
                $variants[$normalKey] = (string) $files[$normalKey];
            }

            if ($includeItalic && isset($files[$italicKey])) {
                $variants[$italicKey] = (string) $files[$italicKey];
            }
        }

        // Fallback: if no requested weights present, try any provided
        if (! count($variants)) {
            foreach ($files as $key => $url) {
                if (is_string($key) && is_string($url)) {
                    $variants[$key] = $url;
                }
            }
        }

        return $variants;
    }

    private function pruneStaleFamilies(string $basePath, array $manifestFonts): void
    {
        $keep = collect($manifestFonts)->pluck('slug')->filter()->all();
        $directories = File::directories($basePath);

        foreach ($directories as $dir) {
            $name = basename($dir);
            if (! in_array($name, $keep, true)) {
                File::deleteDirectory($dir);
                $this->line("🗑  Removed stale font directory {$name}");
            }
        }
    }

    /**
     * @return array<int, mixed>
     */
    private function loadExistingManifestFonts(string $basePath): array
    {
        $manifestPath = $basePath.'/manifest.json';
        if (! File::exists($manifestPath)) {
            return [];
        }

        $manifest = json_decode(File::get($manifestPath), true);
        if (! is_array($manifest)) {
            return [];
        }

        $fonts = $manifest['fonts'] ?? [];

        return is_array($fonts) ? $fonts : [];
    }

    /**
     * @param  array<int, mixed>  $existing
     * @param  array<int, mixed>  $incoming
     * @return array<int, mixed>
     */
    private function mergeManifestFonts(array $existing, array $incoming): array
    {
        $merged = collect($existing)
            ->filter(fn ($font) => is_array($font) && isset($font['family']))
            ->keyBy(fn ($font) => strtolower((string) $font['family']))
            ->all();

        foreach ($incoming as $font) {
            if (! is_array($font) || ! isset($font['family'])) {
                continue;
            }

            $merged[strtolower((string) $font['family'])] = $font;
        }

        return array_values($merged);
    }

    private function writeManifest(string $basePath, array $fonts): void
    {
        $manifest = [
            'generated_at' => now()->toIso8601String(),
            'source' => 'google-webfonts',
            'count' => count($fonts),
            'fonts' => $fonts,
        ];

        File::put($basePath.'/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }

    private function writeCss(string $basePath, array $fonts): void
    {
        $lines = [
            '/* Auto-generated by fonts:sync-google */',
        ];

        foreach ($fonts as $font) {
            $family = $font['family'] ?? null;
            $variants = $font['variants'] ?? [];
            if (! $family || ! is_array($variants)) {
                continue;
            }

            foreach ($variants as $variant) {
                if (! isset($variant['url'])) {
                    continue;
                }

                $lines[] = sprintf(
                    "@font-face {\n  font-family: '%s';\n  font-style: %s;\n  font-weight: %s;\n  font-display: swap;\n  src: url('%s') format('%s');\n}\n",
                    $family,
                    $variant['style'] ?? 'normal',
                    $variant['weight'] ?? 400,
                    $variant['url'],
                    $this->guessFormat((string) ($variant['url'] ?? ''))
                );
            }
        }

        File::put($basePath.'/fonts.css', implode("\n", $lines));
    }

    private function guessFormat(string $url): string
    {
        $extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));

        return match ($extension) {
            'ttf' => 'truetype',
            'otf' => 'opentype',
            'woff' => 'woff',
            default => 'woff2',
        };
    }
}
