<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * Loads, merges, redacts, and atomically persists a project's database profile.
 */
final class ProjectDatabaseProfile
{
    public static function load(string $projectDir, array $fallback = []): array
    {
        $path = self::configPath($projectDir);
        $raw = is_file($path) ? (array)require $path : $fallback;
        return DatabaseConnection::normalize($raw);
    }

    public static function mergeInput(array $input, array $current): array
    {
        $candidateInput = $input;
        $password = (string)($input['db_pass'] ?? $input['pass'] ?? '');
        $clearPassword = self::toBool($input['clear_password'] ?? false);
        if ($clearPassword) {
            $candidateInput['pass'] = '';
            $candidateInput['db_pass'] = '';
        } elseif ($password === '') {
            $candidateInput['pass'] = (string)($current['pass'] ?? '');
            $candidateInput['db_pass'] = (string)($current['pass'] ?? '');
        }
        return DatabaseConnection::normalize($candidateInput);
    }

    public static function publicPayload(array $config): array
    {
        $config = DatabaseConnection::normalize($config);
        return [
            'type' => $config['type'],
            'driver' => $config['driver'],
            'host' => $config['host'],
            'port' => $config['port'],
            'dbname' => $config['dbname'],
            'path' => $config['path'],
            'user' => $config['user'],
            'password_saved' => (string)$config['pass'] !== '',
            'schema' => $config['schema'],
            'service' => $config['service'],
            'charset' => $config['charset'],
            'ssl' => [
                'enabled' => !empty($config['ssl']['enabled']),
                'mode' => (string)$config['ssl']['mode'],
                'ca' => (string)$config['ssl']['ca'],
                'encrypt' => !empty($config['ssl']['encrypt']),
                'trust_server_certificate' => !empty($config['ssl']['trust_server_certificate']),
            ],
            'summary' => self::summary($config),
        ];
    }

    public static function sourceChanged(array $current, array $candidate): bool
    {
        $keys = ['type', 'host', 'port', 'dbname', 'path', 'schema', 'service'];
        foreach ($keys as $key) {
            if ((string)($current[$key] ?? '') !== (string)($candidate[$key] ?? '')) {
                return true;
            }
        }
        return false;
    }

    public static function writeAtomic(string $projectDir, array $config): void
    {
        $normalized = DatabaseConnection::normalize($config);
        $content = "<?php\n\ndeclare(strict_types=1);\n\nreturn " . var_export($normalized, true) . ";\n";
        self::writeContentAtomic($projectDir, $content);
    }

    public static function rawContent(string $projectDir): ?string
    {
        $path = self::configPath($projectDir);
        if (!is_file($path)) {
            return null;
        }
        $content = file_get_contents($path);
        return $content === false ? null : $content;
    }

    public static function restore(string $projectDir, ?string $content): void
    {
        $path = self::configPath($projectDir);
        if ($content === null) {
            if (is_file($path) && !unlink($path)) {
                throw new \RuntimeException('Could not remove the newly-created database profile during rollback.');
            }
            return;
        }
        self::writeContentAtomic($projectDir, $content);
    }

    public static function configPath(string $projectDir): string
    {
        return rtrim($projectDir, DIRECTORY_SEPARATOR) . '/config/database.php';
    }

    /**
     * A "standalone" project is not bound to any database — it is built entirely
     * from standalone/common Designer pages (no Tables/Query/Fields/Security).
     * This is the single authoritative signal used everywhere to gate DB-only
     * features.
     *
     * The absence of config/database.php alone is NOT sufficient: on a server-side
     * build the client's DB profile is (by design) never transmitted to the build
     * server, so a real data project is staged WITHOUT a database.php. Reading that
     * absence as "standalone" wrongly strips the DB connection from the generated
     * app (→ "Connection 'default' is not registered"). A project that defines at
     * least one table is therefore a database app regardless of the profile file.
     */
    public static function isStandalone(string $projectDir): bool
    {
        if (is_file(self::configPath($projectDir))) return false;
        return !self::hasTables($projectDir);
    }

    /**
     * True when the project defines at least one data table (config/tables.json
     * lists selectedTables or tableSchemas). Used to tell a real database app
     * apart from a database-free standalone app when no DB profile is present.
     */
    public static function hasTables(string $projectDir): bool
    {
        $file = rtrim($projectDir, DIRECTORY_SEPARATOR) . '/config/tables.json';
        if (!is_file($file)) return false;
        $data = json_decode((string) @file_get_contents($file), true);
        if (!is_array($data)) return false;
        if (!empty($data['selectedTables']) && is_array($data['selectedTables'])) return true;
        if (!empty($data['tableSchemas'])  && is_array($data['tableSchemas']))  return true;
        return false;
    }

    /** Slug convenience — resolves the project dir via ProjectLocator. */
    public static function isStandaloneSlug(string $slug): bool
    {
        return self::isStandalone(\PpPhp\Core\ProjectLocator::dir($slug));
    }

    private static function writeContentAtomic(string $projectDir, string $content): void
    {
        $configDir = rtrim($projectDir, DIRECTORY_SEPARATOR) . '/config';
        if (!is_dir($configDir) && !mkdir($configDir, 0755, true) && !is_dir($configDir)) {
            throw new \RuntimeException('Could not create the project configuration directory.');
        }
        $target = $configDir . '/database.php';
        $temporary = tempnam($configDir, '.database-');
        if ($temporary === false) {
            throw new \RuntimeException('Could not prepare the database configuration update.');
        }
        try {
            if (file_put_contents($temporary, $content, LOCK_EX) === false) {
                throw new \RuntimeException('Could not write the database configuration update.');
            }
            @chmod($temporary, 0640);
            if (!rename($temporary, $target)) {
                throw new \RuntimeException('Could not atomically replace the database configuration.');
            }
        } finally {
            if (is_file($temporary)) {
                @unlink($temporary);
            }
        }
    }

    private static function summary(array $config): string
    {
        if ($config['type'] === 'sqlite') {
            return (string)$config['path'];
        }
        $server = (string)$config['host'] . ((string)$config['port'] !== '' ? ':' . $config['port'] : '');
        $database = $config['type'] === 'oci' ? (string)$config['service'] : (string)$config['dbname'];
        return $database !== '' ? $database . ' at ' . $server : $server;
    }

    private static function toBool(mixed $value): bool
    {
        if (is_bool($value)) return $value;
        return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }
}
