<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Config as ConfigModel;
use Illuminate\Database\QueryException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final class Config
{
    /** Typed public property for the content group's disclaimer field. */
    public string $contentDisclaimer = '';

    /** Indexed config class instances keyed by their group key string. */
    private array $configClasses = [];

    /**
     * Auto-discovers Config Field classes from app/Configs/, loads or creates
     * their database rows, and populates typed public properties with cast values.
     */
    public function __construct()
    {
        $this->discoverConfigClasses();
        $this->hydrateProperties();
    }

    /**
     * Scans app/Configs/ for PHP files, instantiates each class,
     * and indexes them by their key property.
     */
    private function discoverConfigClasses(): void
    {
        $files = glob(app_path('Configs/*.php')) ?: [];

        foreach ($files as $file) {
            $className = 'App\\Configs\\'.pathinfo($file, PATHINFO_FILENAME);
            $instance = new $className;
            $this->configClasses[$instance->key] = $instance;
        }
    }

    /**
     * Iterates over all discovered config classes, ensures a DB row exists,
     * resolves typed field values, and assigns them to public properties.
     */
    private function hydrateProperties(): void
    {
        try {
            $dbRecords = ConfigModel::all()->keyBy('key');

            foreach ($this->configClasses as $key => $instance) {
                $record = $this->ensureDbRecord($key, $dbRecords);
                $jsonValue = Arr::wrap((array) ($record->json_value ?? []));
                $this->assignFieldProperties($key, $instance, $jsonValue);
            }
        } catch (QueryException) {
            // Silently skip if configs table does not yet exist during migrations.
        }
    }

    /**
     * Ensures a database record exists for the given config key,
     * creating one with an empty json_value if it does not.
     */
    private function ensureDbRecord(string $key, Collection $dbRecords): ConfigModel
    {
        if ($dbRecords->has($key)) {
            return $dbRecords->get($key);
        }

        return ConfigModel::firstOrCreate(
            ['key' => $key],
            ['json_value' => ''],
        );
    }

    /**
     * Resolves each field value from json_value (with default fallback),
     * applies type casting, and assigns to the matching typed public property.
     */
    private function assignFieldProperties(string $key, object $instance, array $jsonValue): void
    {
        foreach ($instance->getFieldKeys() as $fieldKey => $definition) {
            $type = Arr::get($definition, 'type', 'string');
            $rawValue = Arr::get($jsonValue, $fieldKey);

            if ($rawValue === null || $rawValue === '') {
                $rawValue = $instance->getDefault($fieldKey);
            }

            $castedValue = $this->castValue($rawValue, $type);
            $propertyName = Str::camel($key).Str::studly($fieldKey);

            if (property_exists($this, $propertyName)) {
                $this->{$propertyName} = $castedValue;
            }
        }
    }

    /**
     * Casts a raw value to the PHP type defined by the config field type string.
     */
    private function castValue(mixed $value, string $type): mixed
    {
        return match ($type) {
            'string', 'markdown' => (string) $value,
            'int', 'integer' => (int) $value,
            'bool', 'boolean' => (bool) $value,
            'float' => (float) $value,
            default => $value,
        };
    }

    /**
     * Returns the Config Field class instance for a given group key,
     * or null when no class has been discovered for that key.
     */
    public function getConfigClassForGroup(string $group): ?object
    {
        return Arr::get($this->configClasses, $group);
    }
}
