<?php

declare(strict_types=1);

const DENTAL_ROOT = __DIR__;

spl_autoload_register(static function (string $class): void {
    $prefix = 'Dental\\';
    if (!str_starts_with($class, $prefix)) {
        return;
    }
    $relative = str_replace('\\', '/', substr($class, strlen($prefix)));
    $path = DENTAL_ROOT . '/app/' . $relative . '.php';
    if (is_file($path)) {
        require_once $path;
    }
});

function dental_distribution_profile(): array
{
    static $profile;
    if (is_array($profile)) return $profile;

    $path = DENTAL_ROOT . '/config/distribution.php';
    if (!is_file($path) || !is_readable($path)) {
        return $profile = [];
    }
    try {
        $loaded = require $path;
    } catch (Throwable $exception) {
        error_log('DentalCare distribution profile could not be loaded: ' . $exception->getMessage());
        return $profile = [];
    }
    if (!is_array($loaded)) return $profile = [];
    $mode = strtolower(trim((string)($loaded['installer_mode'] ?? '')));
    if (!in_array($mode, ['production','demo'], true)) return $profile = [];
    return $profile = [
        'format' => (string)($loaded['format'] ?? 'DentalCare Distribution Package'),
        'format_version' => (int)($loaded['format_version'] ?? 1),
        'package_id' => preg_match('/^[a-f0-9]{16}$/', (string)($loaded['package_id'] ?? '')) ? (string)$loaded['package_id'] : '',
        'package_type' => $mode,
        'installer_mode' => $mode,
        'application_version' => (string)($loaded['application_version'] ?? ''),
        'created_at' => (string)($loaded['created_at'] ?? ''),
        'requires_signed_license' => $mode === 'production',
        'demo_read_only' => $mode === 'demo',
        'allows_database_adoption' => $mode === 'production' && !empty($loaded['allows_database_adoption']),
        'allows_prefix_wipe' => $mode === 'production' && !empty($loaded['allows_prefix_wipe']),
        'allows_package_builder' => !array_key_exists('allows_package_builder', $loaded) ? false : !empty($loaded['allows_package_builder']),
        'allows_license_issuer' => !array_key_exists('allows_license_issuer', $loaded) ? false : !empty($loaded['allows_license_issuer']),
        'allows_public_release_downloads' => !array_key_exists('allows_public_release_downloads', $loaded) ? false : !empty($loaded['allows_public_release_downloads']),
    ];
}

function dental_distribution_feature_allowed(string $feature): bool
{
    $profile = dental_distribution_profile();
    if (!$profile) return true; // Owner/source installation; no distribution lock present.
    $key = match ($feature) {
        'package_builder', 'installer_builder' => 'allows_package_builder',
        'license_issuer', 'license_manager' => 'allows_license_issuer',
        'public_release_downloads' => 'allows_public_release_downloads',
        default => '',
    };
    if ($key === '') return false;
    return !empty($profile[$key]);
}

function dental_normalize_database_config(array $database): array
{
    return [
        'host' => (string) ($database['host'] ?? 'localhost'),
        'port' => (int) ($database['port'] ?? 3306),
        'name' => (string) ($database['name'] ?? $database['database'] ?? $database['dbname'] ?? ''),
        'username' => (string) ($database['username'] ?? $database['user'] ?? ''),
        'password' => (string) ($database['password'] ?? ''),
        'prefix' => (string) ($database['prefix'] ?? $database['table_prefix'] ?? 'dent_'),
    ];
}

function dental_config_is_valid(array $config): bool
{
    if (!isset($config['database']) || !is_array($config['database'])) {
        return false;
    }

    $database = dental_normalize_database_config($config['database']);
    return $database['host'] !== ''
        && $database['name'] !== ''
        && $database['username'] !== ''
        && preg_match('/^[A-Za-z][A-Za-z0-9_]{1,24}$/', $database['prefix']) === 1;
}

function dental_read_config_file(string $path): ?array
{
    if (!is_file($path) || !is_readable($path)) {
        return null;
    }

    try {
        $config = require $path;
    } catch (Throwable $exception) {
        error_log('DentalCare configuration read failed for ' . $path . ': ' . $exception->getMessage());
        return null;
    }

    if (!is_array($config)) {
        return null;
    }

    if (isset($config['database']) && is_array($config['database'])) {
        $config['database'] = dental_normalize_database_config($config['database']);
    }

    return dental_config_is_valid($config) ? $config : null;
}

function dental_resolve_config(): array
{
    static $resolved;
    if (is_array($resolved)) {
        return $resolved;
    }

    $canonical = DENTAL_ROOT . '/config/generated.php';
    $candidates = [
        $canonical,
        DENTAL_ROOT . '/config/generated.php.bak',
        DENTAL_ROOT . '/config/generated.bak.php',
        DENTAL_ROOT . '/config/generated.php.old',
        DENTAL_ROOT . '/config/generated.php.backup',
        DENTAL_ROOT . '/storage/generated.php',
        DENTAL_ROOT . '/storage/config/generated.php',
        DENTAL_ROOT . '/generated.php',
    ];

    foreach ($candidates as $candidate) {
        $config = dental_read_config_file($candidate);
        if ($config !== null) {
            return $resolved = [
                'config' => $config,
                'source' => $candidate,
                'canonical' => $candidate === $canonical,
                'legacy' => false,
            ];
        }
    }

    // Compatibility with installations that used separate configuration files.
    $databasePath = DENTAL_ROOT . '/config/database.php';
    if (is_file($databasePath) && is_readable($databasePath)) {
        try {
            $database = require $databasePath;
            $app = is_file(DENTAL_ROOT . '/config/app.php') ? require DENTAL_ROOT . '/config/app.php' : [];
            $mail = is_file(DENTAL_ROOT . '/config/mail.php') ? require DENTAL_ROOT . '/config/mail.php' : [];
            $config = [
                'app' => is_array($app) ? $app : [],
                'database' => is_array($database) ? dental_normalize_database_config($database) : [],
                'mail' => is_array($mail) ? $mail : [],
                'security' => [],
            ];
            if (dental_config_is_valid($config)) {
                return $resolved = [
                    'config' => $config,
                    'source' => $databasePath,
                    'canonical' => false,
                    'legacy' => true,
                ];
            }
        } catch (Throwable $exception) {
            error_log('DentalCare legacy configuration read failed: ' . $exception->getMessage());
        }
    }

    return $resolved = [
        'config' => [],
        'source' => null,
        'canonical' => false,
        'legacy' => false,
    ];
}

function dental_write_generated_config(array $config): bool
{
    if (!dental_config_is_valid($config)) {
        return false;
    }

    $directory = DENTAL_ROOT . '/config';
    if (!is_dir($directory) || !is_writable($directory)) {
        return false;
    }

    $target = $directory . '/generated.php';
    $temporary = $target . '.repair-' . bin2hex(random_bytes(4));
    $content = "<?php\n\ndeclare(strict_types=1);\n\nreturn " . var_export($config, true) . ";\n";

    if (file_put_contents($temporary, $content, LOCK_EX) === false) {
        return false;
    }

    @chmod($temporary, 0640);
    if (!@rename($temporary, $target)) {
        @unlink($temporary);
        return false;
    }

    return true;
}

function dental_update_generated_config(callable $mutator): array
{
    $resolved = dental_resolve_config();
    $config = is_array($resolved['config'] ?? null) ? $resolved['config'] : [];
    if (!dental_config_is_valid($config)) {
        throw new RuntimeException('The installed configuration could not be loaded. Restore config/generated.php before changing system settings.');
    }

    $updated = $mutator($config);
    if (!is_array($updated) || !dental_config_is_valid($updated)) {
        throw new RuntimeException('The updated configuration is invalid and was not saved.');
    }
    if (!dental_write_generated_config($updated)) {
        throw new RuntimeException('Unable to write config/generated.php. Temporarily make the config directory writable by PHP, save the settings, then restore secure permissions.');
    }
    return $updated;
}

function dental_installation_state(bool $repair = true): array
{
    $resolved = dental_resolve_config();
    $config = is_array($resolved['config'] ?? null) ? $resolved['config'] : [];
    $configValid = dental_config_is_valid($config);

    $lockCandidates = [
        DENTAL_ROOT . '/storage/installed.lock',
        DENTAL_ROOT . '/config/installed.lock',
    ];
    $lockPath = null;
    foreach ($lockCandidates as $candidate) {
        if (is_file($candidate)) {
            $lockPath = $candidate;
            break;
        }
    }

    $repairedConfig = false;
    $repairedLock = false;

    if ($repair && $configValid && !($resolved['canonical'] ?? false)) {
        $repairedConfig = dental_write_generated_config($config);
    }

    // A valid generated configuration proves that installation already
    // occurred. The lock protects the installer, but must not be used as the
    // sole database-installation signal.
    if ($repair && $configValid && $lockPath === null) {
        $storage = DENTAL_ROOT . '/storage';
        if (!is_dir($storage)) {
            @mkdir($storage, 0750, true);
        }
        $targetLock = $storage . '/installed.lock';
        if (is_dir($storage) && is_writable($storage)) {
            $payload = json_encode([
                'installed_at' => (string) ($config['app']['installed_at'] ?? date(DATE_ATOM)),
                'version' => (string) ($config['app']['version'] ?? 'recovered'),
                'database_action' => 'installation_state_recovered',
                'recovered_at' => date(DATE_ATOM),
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
            if (is_string($payload) && file_put_contents($targetLock, $payload, LOCK_EX) !== false) {
                @chmod($targetLock, 0640);
                $lockPath = $targetLock;
                $repairedLock = true;
            }
        }
    }

    return [
        'installed' => $configValid,
        'config_valid' => $configValid,
        'config_source' => $resolved['source'] ?? null,
        'canonical_config' => ($resolved['canonical'] ?? false) || $repairedConfig,
        'lock_exists' => $lockPath !== null,
        'lock_path' => $lockPath,
        'repaired_config' => $repairedConfig,
        'repaired_lock' => $repairedLock,
        'recovery_required' => !$configValid && $lockPath !== null,
        'fresh_install' => !$configValid && $lockPath === null,
    ];
}

function dental_config(): array
{
    $resolved = dental_resolve_config();
    return is_array($resolved['config'] ?? null) ? $resolved['config'] : [];
}



function dental_base64url_encode(string $value): string
{
    return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
}

function dental_base64url_decode(string $value): string|false
{
    $padding = strlen($value) % 4;
    if ($padding) {
        $value .= str_repeat('=', 4 - $padding);
    }
    return base64_decode(strtr($value, '-_', '+/'), true);
}

function dental_current_host(): string
{
    $host = strtolower(trim((string) ($_SERVER['HTTP_HOST'] ?? '')));
    if ($host === '' && PHP_SAPI === 'cli') {
        $url = (string) (dental_config()['app']['url'] ?? '');
        $host = strtolower((string) parse_url($url, PHP_URL_HOST));
    }
    if (str_contains($host, ':')) {
        $host = explode(':', $host, 2)[0];
    }
    return trim($host, '.');
}

function dental_domain_matches(string $host, string $licensedDomain): bool
{
    $host = strtolower(trim($host, '.'));
    $licensedDomain = strtolower(trim($licensedDomain, '.'));
    if ($licensedDomain === '') {
        return false;
    }
    if (str_starts_with($licensedDomain, '*.')) {
        $base = substr($licensedDomain, 2);
        return $host !== $base && str_ends_with($host, '.' . $base);
    }
    return hash_equals($licensedDomain, $host);
}

function dental_installation_mode(): string
{
    $config = dental_config();
    $mode = strtolower(trim((string) ($config['app']['installation_mode'] ?? $config['app']['mode'] ?? 'production')));
    return $mode === 'demo' ? 'demo' : 'production';
}

function dental_demo_read_only(): bool
{
    $config = dental_config();
    return dental_installation_mode() === 'demo'
        && (bool) ($config['security']['demo_read_only'] ?? true);
}

function dental_license_status(?string $token = null): array
{
    $host = dental_current_host();
    if (dental_installation_mode() === 'demo') {
        return [
            'valid' => true,
            'mode' => 'demo_read_only',
            'host' => $host,
            'message' => 'Read-only demonstration authorization is active.',
            'features' => ['white_label' => false, 'multi_branch' => false, 'demo_read_only' => true],
        ];
    }
    $explicitToken = $token !== null;
    if (!$explicitToken && $host === 'apps.syllabuild.org') {
        return [
            'valid' => true,
            'mode' => 'owner_domain',
            'host' => $host,
            'message' => 'Owner domain authorization is active.',
            'features' => ['white_label' => true, 'multi_branch' => true],
        ];
    }

    if ($token === null) {
        $path = DENTAL_ROOT . '/storage/license.key';
        $token = is_file($path) ? trim((string) file_get_contents($path)) : '';
    }
    if ($token === '') {
        return ['valid' => false, 'mode' => 'license_required', 'host' => $host, 'message' => 'A license is required for this domain.'];
    }

    $parts = explode('.', $token);
    if (count($parts) !== 2) {
        return ['valid' => false, 'mode' => 'invalid', 'host' => $host, 'message' => 'The license format is invalid.'];
    }
    [$payloadPart, $signaturePart] = $parts;
    $payloadJson = dental_base64url_decode($payloadPart);
    $signature = dental_base64url_decode($signaturePart);
    if ($payloadJson === false || $signature === false) {
        return ['valid' => false, 'mode' => 'invalid', 'host' => $host, 'message' => 'The license encoding is invalid.'];
    }

    $publicKeyPath = DENTAL_ROOT . '/config/license_public.pem';
    $publicKey = is_file($publicKeyPath) ? openssl_pkey_get_public((string) file_get_contents($publicKeyPath)) : false;
    if ($publicKey === false || openssl_verify($payloadJson, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
        return ['valid' => false, 'mode' => 'invalid_signature', 'host' => $host, 'message' => 'The license signature could not be verified.'];
    }

    $payload = json_decode($payloadJson, true);
    if (!is_array($payload) || ($payload['product'] ?? '') !== 'dentalcare') {
        return ['valid' => false, 'mode' => 'invalid_product', 'host' => $host, 'message' => 'The license is not valid for this product.'];
    }
    $domains = array_values(array_filter(array_map('strval', (array) ($payload['domains'] ?? []))));
    $domainValid = false;
    foreach ($domains as $domain) {
        if (dental_domain_matches($host, $domain)) {
            $domainValid = true;
            break;
        }
    }
    if (!$domainValid) {
        return ['valid' => false, 'mode' => 'domain_mismatch', 'host' => $host, 'message' => 'This license is not assigned to ' . ($host ?: 'the current domain') . '.', 'payload' => $payload];
    }
    $expiresAt = trim((string) ($payload['expires_at'] ?? ''));
    if ($expiresAt !== '' && strtotime($expiresAt . ' 23:59:59') < time()) {
        return ['valid' => false, 'mode' => 'expired', 'host' => $host, 'message' => 'The license expired on ' . $expiresAt . '.', 'payload' => $payload];
    }

    return [
        'valid' => true,
        'mode' => 'signed_license',
        'host' => $host,
        'message' => 'Licensed to ' . (string) ($payload['customer'] ?? $host) . '.',
        'payload' => $payload,
        'features' => (array) ($payload['features'] ?? []),
    ];
}

function dental_pdo(array $config): PDO
{
    $db = $config['database'] ?? $config;
    $dsn = sprintf(
        'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
        $db['host'],
        (int) ($db['port'] ?? 3306),
        $db['name']
    );
    $pdo = new PDO($dsn, $db['username'], $db['password'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    // Keep MySQL NOW()/CURDATE() aligned with PHP and the clinic configuration.
    // Use a numeric UTC offset so this also works on shared hosting where named
    // MySQL time-zone tables are not installed.
    try {
        $timezoneName = (string) (($config['app']['timezone'] ?? null) ?: 'Asia/Manila');
        $timezone = new DateTimeZone($timezoneName);
        $offset = (new DateTimeImmutable('now', $timezone))->format('P');
        $pdo->exec('SET SESSION time_zone=' . $pdo->quote($offset));
    } catch (Throwable $exception) {
        error_log('DentalCare database timezone alignment failed: ' . $exception->getMessage());
    }

    return $pdo;
}
