<?php

declare(strict_types=1);

namespace PpPhp\Core\Databases\Drivers;

/**
 * DirectAdmin driver (legacy CMD_API_* endpoints).
 *
 *   POST https://<host>:2222/CMD_API_DATABASES
 *   Basic auth: <account> : <LOGIN KEY>
 *
 * Unlike cPanel this creates the database AND its user in a single call, so
 * there is no half-provisioned state to unwind.
 *
 * Authenticate with a LOGIN KEY, never the account password: a key can be
 * limited to specific commands and source IPs, so a leaked one is far less
 * damaging. DirectAdmin also prefixes every name with `<account>_`.
 *
 * Legacy responses are URL-encoded, not JSON:
 *     error=0&text=...&details=...
 * so `error=1` is the failure signal — an HTTP 200 alone means nothing here,
 * which is the usual trap when integrating this API.
 */
final class DirectAdminDriver implements ProvisionDriver
{
    public function __construct(
        private string $host,
        private string $account,
        private string $loginKey,
        private int $port = 2222,
        private string $dbHost = 'localhost',
        private int $dbPort = 3306,
    ) {}

    public function id(): string { return 'directadmin'; }

    public function connectionTarget(): array
    {
        return ['host' => $this->dbHost !== '' ? $this->dbHost : 'localhost', 'port' => $this->dbPort ?: 3306];
    }

    public function prefix(): string
    {
        return $this->account !== '' ? $this->account . '_' : '';
    }

    public function test(): array
    {
        $r = $this->call('CMD_API_DATABASES', []);          // GET = list
        if (!$r['ok']) {
            return ['success' => false, 'message' => $r['message'],
                    'canCreateDb' => false, 'canCreateUser' => false,
                    'prefix' => $this->prefix(), 'maxNameLength' => 64];
        }
        $count = isset($r['fields']['list']) ? count((array) $r['fields']['list']) : max(0, count($r['fields']) - 1);
        return [
            'success' => true,
            'message' => sprintf('Connected to DirectAdmin as “%s”. New names will be prefixed “%s”.',
                $this->account, $this->prefix()),
            'canCreateDb' => true, 'canCreateUser' => true,
            'prefix' => $this->prefix(), 'maxNameLength' => 64,
            'detail' => ['databases' => $count],
        ];
    }

    public function createDatabase(string $desiredDb, string $desiredUser, string $password): array
    {
        // DirectAdmin wants the name WITHOUT the prefix and adds it itself, which
        // is the opposite of cPanel — hence stripping rather than qualifying.
        $db   = $this->bare($desiredDb);
        $user = $this->bare($desiredUser);

        $r = $this->call('CMD_API_DATABASES', [
            'action'  => 'create',
            'name'    => $db,
            'user'    => $user,
            'passwd'  => $password,
            'passwd2' => $password,
            'create'  => 'Create',
        ], true);

        if (!$r['ok']) {
            return ['success' => false, 'message' => 'DirectAdmin refused the request: ' . $r['message']];
        }

        return [
            'success' => true,
            'message' => 'Database created via DirectAdmin.',
            'dbName'  => $this->prefix() . $db,
            'dbUser'  => $this->prefix() . $user,
            'dedicated' => true,
        ];
    }

    public function dropDatabase(string $dbName, string $dbUser = ''): array
    {
        // Deleting the database removes its users with it, so one call is enough.
        $r = $this->call('CMD_API_DATABASES', [
            'action'  => 'delete',
            'delete'  => 'anything',
            'select0' => $dbName,          // already the full prefixed name
        ], true);

        return $r['ok']
            ? ['success' => true, 'message' => 'Removed via DirectAdmin.']
            : ['success' => false, 'message' => $r['message']];
    }

    /** Strip our own or the panel's prefix — DirectAdmin adds it back itself. */
    private function bare(string $name): string
    {
        $name = preg_replace('/[^A-Za-z0-9_]/', '', $name) ?? $name;
        $p = $this->prefix();
        if ($p !== '' && str_starts_with($name, $p)) $name = substr($name, strlen($p));
        // Leave room for the prefix inside MySQL's 64-character limit.
        return substr($name, 0, max(8, 60 - strlen($p)));
    }

    /**
     * One legacy API call.
     *
     * @return array{ok:bool, message:string, fields:array<string,mixed>}
     */
    private function call(string $command, array $params, bool $post = false): array
    {
        $url = sprintf('https://%s:%d/%s', $this->host, $this->port, $command);
        if (!$post && $params !== []) $url .= '?' . http_build_query($params);

        $raw = $this->http($url, $post ? $params : null);
        if ($raw['error'] !== '') {
            return ['ok' => false, 'message' => $raw['error'], 'fields' => []];
        }
        if ($raw['status'] === 401) {
            return ['ok' => false, 'fields' => [],
                    'message' => 'DirectAdmin rejected the credentials. Check the username and the login key.'];
        }

        $body = trim((string) $raw['body']);

        // A login page instead of an API answer means the key is wrong or the
        // command is not permitted for it.
        if (stripos($body, '<html') !== false) {
            return ['ok' => false, 'fields' => [],
                    'message' => 'DirectAdmin returned a web page rather than an API response — the login key is probably invalid, or not allowed to run ' . $command . '.'];
        }

        parse_str($body, $fields);
        if (!is_array($fields)) $fields = [];

        // error=1 is the failure signal; HTTP 200 on its own proves nothing.
        if ((string) ($fields['error'] ?? '0') === '1') {
            $text = trim((string) ($fields['text'] ?? 'Request refused.'));
            $det  = trim(strip_tags((string) ($fields['details'] ?? '')));
            return ['ok' => false, 'fields' => $fields,
                    'message' => $det !== '' ? $text . ' — ' . $det : $text];
        }
        return ['ok' => true, 'message' => '', 'fields' => $fields];
    }

    /** @return array{body:string,status:int,error:string} */
    private function http(string $url, ?array $post): array
    {
        if (!function_exists('curl_init')) {
            return ['body' => '', 'status' => 0, 'error' => 'PHP cURL is required to talk to DirectAdmin.'];
        }
        // Same reasoning as the cPanel driver: a panel certificate is issued for
        // the hostname, so it can never match loopback or a bare IP.
        $byIp = self::isLoopbackOrIp($this->host);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERPWD        => $this->account . ':' . $this->loginKey,
            CURLOPT_HTTPAUTH       => CURLAUTH_BASIC,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_USERAGENT      => 'PPPHP-Provisioner/1',
            CURLOPT_SSL_VERIFYPEER => !$byIp,
            CURLOPT_SSL_VERIFYHOST => $byIp ? 0 : 2,
        ]);
        if ($post !== null) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
        }
        $body = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        unset($ch);

        if ($body === false || $status === 0) {
            return ['body' => '', 'status' => $status,
                    'error' => 'Could not reach DirectAdmin at ' . $this->host . ':' . $this->port
                             . ($err !== '' ? ' (' . $err . ')' : '.')
                             . ' Check the host and port (DirectAdmin usually listens on 2222).'];
        }
        return ['body' => (string) $body, 'status' => $status, 'error' => ''];
    }

    /** Loopback or a bare IP — the two cases a panel certificate can never match. */
    private static function isLoopbackOrIp(string $host): bool
    {
        $h = strtolower(trim($host));
        if (in_array($h, ['localhost', '127.0.0.1', '::1', '[::1]'], true)) return true;
        return (bool) filter_var($h, FILTER_VALIDATE_IP);
    }
}
