<?php

declare(strict_types=1);

namespace Budget;

use PDO;

final class Settings
{
    private const DEFAULTS = [
        'app.name' => 'Neon Ledger',
        'app.currency_symbol' => '$',
        'app.timezone' => 'America/New_York',
        'theme.default' => 'dark',
        'modules.income' => true,
        'modules.bills' => true,
        'modules.calendar' => true,
        'modules.dead_account' => true,
        'modules.comparisons' => true,
        'modules.alerts' => true,
        'modules.import_export' => true,
        'dead.income_mode' => 'percent',
        'dead.income_value' => 0,
        'dead.transaction_mode' => 'fixed',
        'dead.transaction_value' => 0,
        'dead.hide_balance' => true,
        'alerts.email' => '',
        'alerts.windows' => [7, 3, 1],
        'alerts.times' => ['09:00'],
        'mailgun.api_key' => '',
        'mailgun.domain' => '',
        'mailgun.region' => 'us',
        'mailgun.from' => 'Budget Planner <budget@example.com>',
        'security.2fa_required' => false,
    ];

    public function __construct(private PDO $pdo)
    {
        $this->seed();
    }

    public function get(string $key, mixed $fallback = null): mixed
    {
        $statement = $this->pdo->prepare('SELECT setting_value FROM settings WHERE setting_key = :key');
        $statement->execute(['key' => $key]);
        $value = $statement->fetchColumn();
        if ($value === false) {
            return self::DEFAULTS[$key] ?? $fallback;
        }
        $decoded = json_decode((string) $value, true);
        return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
    }

    public function set(string $key, mixed $value): void
    {
        $encoded = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($encoded === false) {
            throw new \InvalidArgumentException('Setting value cannot be encoded.');
        }
        $sql = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql'
            ? 'INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)
               ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP'
            : 'INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)
               ON CONFLICT(setting_key) DO UPDATE SET setting_value = excluded.setting_value, updated_at = CURRENT_TIMESTAMP';
        $statement = $this->pdo->prepare($sql);
        $statement->execute(['key' => $key, 'value' => $encoded]);
    }

    /** @return array<string, mixed> */
    public function all(): array
    {
        $settings = self::DEFAULTS;
        foreach ($this->pdo->query('SELECT setting_key, setting_value FROM settings')->fetchAll() as $row) {
            $decoded = json_decode((string) $row['setting_value'], true);
            $settings[$row['setting_key']] = json_last_error() === JSON_ERROR_NONE ? $decoded : $row['setting_value'];
        }
        return $settings;
    }

    public function module(string $name): bool
    {
        return (bool) $this->get('modules.' . $name, true);
    }

    private function seed(): void
    {
        $count = (int) $this->pdo->query('SELECT COUNT(*) FROM settings')->fetchColumn();
        if ($count > 0) {
            return;
        }
        foreach (self::DEFAULTS as $key => $value) {
            $this->set($key, $value);
        }
    }
}
