<?php

declare(strict_types=1);

namespace App\Services\Countries;

use App\Models\Country;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use JsonException;
use RuntimeException;
use Throwable;

final class CountrySyncService
{
    public function __construct(
        private readonly CountryRemoteProviderInterface $remoteProvider,
        private readonly CountryCatalogService $catalog,
    ) {
    }

    /**
     * @return array<string, int|string|null>
     */
    public function sync(bool $allowFallback = true, ?bool $deactivateMissing = null): array
    {
        if (! (bool) config('skinbase-countries.enabled', true)) {
            throw new RuntimeException('Countries sync is disabled by configuration.');
        }

        $summary = [
            'source' => null,
            'total_fetched' => 0,
            'inserted' => 0,
            'updated' => 0,
            'skipped' => 0,
            'invalid' => 0,
            'deactivated' => 0,
            'backfilled_users' => 0,
        ];

        try {
            $records = $this->remoteProvider->fetchAll();
            $summary['source'] = (string) config('skinbase-countries.remote_source', 'remote');
        } catch (Throwable $exception) {
            if (! $allowFallback || ! (bool) config('skinbase-countries.fallback_seed_enabled', true)) {
                throw new RuntimeException('Country sync failed: '.$exception->getMessage(), previous: $exception);
            }

            $records = $this->loadFallbackRecords();
            $summary['source'] = 'fallback';
        }

        if ($records === []) {
            throw new RuntimeException('Country sync did not yield any valid country records.');
        }

        $summary['total_fetched'] = count($records);
        $seenIso2 = [];
        $featured = array_values(array_filter(array_map(
            static fn (mixed $iso2): string => strtoupper(trim((string) $iso2)),
            (array) config('skinbase-countries.featured_countries', []),
        )));
        $featuredOrder = array_flip($featured);

        DB::transaction(function () use (&$summary, $records, &$seenIso2, $featuredOrder, $deactivateMissing): void {
            foreach ($records as $record) {
                $iso2 = strtoupper((string) ($record['iso2'] ?? ''));

                if (! preg_match('/^[A-Z]{2}$/', $iso2)) {
                    $summary['invalid']++;
                    continue;
                }

                if (isset($seenIso2[$iso2])) {
                    $summary['skipped']++;
                    continue;
                }

                $seenIso2[$iso2] = true;

                $country = Country::query()->firstOrNew(['iso2' => $iso2]);
                $exists = $country->exists;
                $featuredIndex = $featuredOrder[$iso2] ?? null;

                $country->fill([
                    'iso' => $iso2,
                    'iso3' => $record['iso3'] ?? null,
                    'numeric_code' => $record['numeric_code'] ?? null,
                    'name' => $record['name_common'],
                    'native' => $record['name_official'] ?? null,
                    'continent' => $this->continentCode($record['region'] ?? null),
                    'name_common' => $record['name_common'],
                    'name_official' => $record['name_official'] ?? null,
                    'region' => $record['region'] ?? null,
                    'subregion' => $record['subregion'] ?? null,
                    'flag_svg_url' => $record['flag_svg_url'] ?? null,
                    'flag_png_url' => $record['flag_png_url'] ?? null,
                    'flag_emoji' => $record['flag_emoji'] ?? null,
                    'active' => true,
                    'is_featured' => $featuredIndex !== null,
                    'sort_order' => $featuredIndex !== null ? $featuredIndex + 1 : 1000,
                ]);

                if (! $exists) {
                    $country->save();
                    $summary['inserted']++;
                    continue;
                }

                if ($country->isDirty()) {
                    $country->save();
                    $summary['updated']++;
                    continue;
                }

                $summary['skipped']++;
            }

            if ($deactivateMissing ?? (bool) config('skinbase-countries.deactivate_missing', false)) {
                $summary['deactivated'] = Country::query()
                    ->where('active', true)
                    ->whereNotIn('iso2', array_keys($seenIso2))
                    ->update(['active' => false]);
            }
        });

        $summary['backfilled_users'] = $this->backfillUsersFromLegacyProfileCodes();
        $this->catalog->flushCache();

        return $summary;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadFallbackRecords(): array
    {
        $path = (string) config('skinbase-countries.fallback_seed_path', database_path('data/countries-fallback.json'));

        if (! is_file($path)) {
            throw new RuntimeException('Country fallback dataset is missing.');
        }

        try {
            $decoded = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new RuntimeException('Country fallback dataset is invalid JSON.', previous: $exception);
        }

        if (! is_array($decoded)) {
            throw new RuntimeException('Country fallback dataset is not a JSON array.');
        }

        return $this->remoteProvider->normalizePayload($decoded);
    }

    private function backfillUsersFromLegacyProfileCodes(): int
    {
        if (! Schema::hasTable('user_profiles') || ! Schema::hasTable('users') || ! Schema::hasColumn('users', 'country_id')) {
            return 0;
        }

        $rows = DB::table('users as users')
            ->join('user_profiles as profiles', 'profiles.user_id', '=', 'users.id')
            ->join('countries as countries', 'countries.iso2', '=', 'profiles.country_code')
            ->whereNull('users.country_id')
            ->whereNotNull('profiles.country_code')
            ->select(['users.id as user_id', 'countries.id as country_id'])
            ->get();

        foreach ($rows as $row) {
            DB::table('users')
                ->where('id', (int) $row->user_id)
                ->update(['country_id' => (int) $row->country_id]);
        }

        return $rows->count();
    }

    private function continentCode(?string $region): ?string
    {
        return Arr::get([
            'Africa' => 'AF',
            'Americas' => 'AM',
            'Asia' => 'AS',
            'Europe' => 'EU',
            'Oceania' => 'OC',
            'Antarctic' => 'AN',
        ], trim((string) $region));
    }
}
