<?php
/**
 * CpanelApi.php
 * ---------------------------------------------------------------------------
 * A small, dependency-free wrapper around cPanel's UAPI, authenticated via
 * an API Token (Security -> Manage API Tokens, inside cPanel).
 *
 * This works on ORDINARY shared cPanel hosting — no WHM, no reseller
 * account, no SSH. An API Token only ever has the same permissions as the
 * cPanel user that created it, and it can be revoked instantly from the
 * cPanel UI if it's ever compromised, without changing your main password.
 *
 * WHERE TO GET YOUR CREDENTIALS
 *   1. Log into cPanel (https://yourdomain.com:2083 or via your host's
 *      "cPanel Login" link).
 *   2. Security -> Manage API Tokens -> Create.
 *   3. Give it a name (e.g. "quickecommerce-provisioner"), no expiry (or a
 *      long one — you can always revoke it), and copy the token shown ONCE.
 *   4. You'll also need: your cPanel username, and the hostname cPanel
 *      itself runs on (often the same as your main domain, but check the
 *      URL bar while logged into cPanel — it's usually something like
 *      yourdomain.com:2083 or a shared box hostname your host gave you).
 *
 * DOCS: https://api.docs.cpanel.net/openapi/cpanel/operation/create_database/
 * ---------------------------------------------------------------------------
 */

class CpanelApi
{
    private string $host;      // e.g. "yourdomain.com" (no https://, no port)
    private int    $port;      // usually 2083 for cPanel over SSL
    private string $username;  // your cPanel account username
    private string $token;     // the API token you generated

    public function __construct(string $host, string $username, string $token, int $port = 2083)
    {
        $this->host     = $host;
        $this->port     = $port;
        $this->username = $username;
        $this->token    = $token;
    }

    /**
     * Low-level UAPI call. $module e.g. "Mysql", $function e.g. "create_database".
     * Returns the decoded JSON response as an associative array, or throws
     * CpanelApiException on a transport-level or cPanel-level failure.
     */
    public function call(string $module, string $function, array $params = []): array
    {
        $url = sprintf(
            'https://%s:%d/execute/%s/%s',
            $this->host,
            $this->port,
            rawurlencode($module),
            rawurlencode($function)
        );

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query($params),
            CURLOPT_HTTPHEADER     => [
                'Authorization: cpanel ' . $this->username . ':' . $this->token,
            ],
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        $raw = curl_exec($ch);
        if ($raw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            throw new CpanelApiException("Connection to cPanel failed: $err");
        }
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $decoded = json_decode($raw, true);
        if ($decoded === null) {
            throw new CpanelApiException(
                "cPanel returned a non-JSON response (HTTP $httpCode). " .
                "Check host/port/username are correct. Raw: " . substr($raw, 0, 300)
            );
        }

        // UAPI's own success/failure flag — separate from the HTTP status.
        $status = $decoded['status'] ?? ($decoded['result']['status'] ?? null);
        if ($status !== null && (int)$status !== 1) {
            $errors = $decoded['errors'] ?? ($decoded['result']['errors'] ?? ['Unknown cPanel API error']);
            throw new CpanelApiException('cPanel API error: ' . implode('; ', (array)$errors));
        }

        return $decoded;
    }

    /**
     * Creates a MySQL database. cPanel automatically prefixes the name
     * with your cPanel username + underscore (e.g. "inwaycom_storename"),
     * and returns the FULL prefixed name — always use the returned name,
     * never assume it matches what you passed in.
     */
    public function createDatabase(string $shortName): string
    {
        $shortName = $this->sanitizeDbIdentifier($shortName);
        $this->call('Mysql', 'create_database', ['name' => $shortName]);
        return $this->username . '_' . $shortName;
    }

    /**
     * Creates a MySQL user with a given password. Returns the full
     * prefixed username (same cPanel auto-prefix behaviour as databases).
     */
    public function createDbUser(string $shortName, string $password): string
    {
        $shortName = $this->sanitizeDbIdentifier($shortName);
        $this->call('Mysql', 'create_user', [
            'name'     => $shortName,
            'password' => $password,
        ]);
        return $this->username . '_' . $shortName;
    }

    /** Grants ALL PRIVILEGES for a user on a database (both full/prefixed names). */
    public function grantAllPrivileges(string $fullDbUser, string $fullDbName): void
    {
        $this->call('Mysql', 'set_privileges_on_database', [
            'user'       => $fullDbUser,
            'database'   => $fullDbName,
            'privileges' => 'ALL PRIVILEGES',
        ]);
    }

    /**
     * Creates a subdomain (e.g. "storename" under "quickecommerce.co"),
     * which also creates its document root folder in one step — exactly
     * what's needed before copying the store's codebase into place.
     * Returns the document root path cPanel created.
     */
    public function createSubdomain(string $subdomain, string $rootDomain, string $docRootRelative): string
    {
        $subdomain = $this->sanitizeSubdomain($subdomain);
        $result = $this->call('SubDomain', 'addsubdomain', [
            'domain'    => $subdomain,
            'rootdomain'=> $rootDomain,
            'dir'       => $docRootRelative, // relative to home, e.g. "public_html/stores/storename"
        ]);
        return $result['result']['data']['dir'] ?? $docRootRelative;
    }

    /** Checks how many MySQL databases exist vs. the account's plan limit. */
    public function getDatabaseQuota(): array
    {
        $result = $this->call('Mysql', 'list_databases');
        $count  = count($result['result']['data'] ?? []);
        // Note: UAPI doesn't directly expose the MAX allowed via this call on
        // all cPanel versions — if your host enforces one, check the
        // "MySQL Databases" page in cPanel for "You have used X of Y".
        return ['current_count' => $count];
    }

    /** cPanel db/user identifiers: lowercase letters, numbers, underscore only, reasonably short. */
    private function sanitizeDbIdentifier(string $name): string
    {
        $name = strtolower(preg_replace('/[^a-zA-Z0-9_]/', '', $name));
        // cPanel prefixes with "username_", and MySQL's own identifier limit
        // is 64 chars total — leave generous room for the prefix.
        return substr($name, 0, 32);
    }

    private function sanitizeSubdomain(string $name): string
    {
        $name = strtolower(preg_replace('/[^a-z0-9\-]/', '', strtolower($name)));
        return substr(trim($name, '-'), 0, 50);
    }
}

class CpanelApiException extends \Exception {}
