<?php

namespace Sitecake;

/**
 * License check and activation for the SiteCake CMS.
 *
 * Model:
 *   - localhost / *.local / *.test / private IPs → always free, no license needed
 *   - Real domain → requires activation against admin.sitecake.com on first use
 *   - After activation, verification is purely local (RSA signature check, zero server contact)
 *   - Domain change detected → triggers automatic re-activation against the server
 */
class License
{
    /**
     * RSA-2048 public key used to verify license tokens.
     * The corresponding private key lives only on admin.sitecake.com.
     */
    const PUBLIC_KEY = <<<EOT
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl/Fo04vmrZgJODxfZNFA
ym/2a0Filh7CuCAkdVNTqqvCvM4Iz3rSrzy/fiPocTLNOnYOoQqjifrjMKnCRoIa
yT5QW+PKPiBXOERm4gtb/fZJFxImGFtIdr1A1unyc1b9czKyJS41B+JBuJ8ksJLY
T4hCvM7fgfMZhUrqMGvqJkQxKeq/19HgBRxcxf6kTCcvxSbwrNasG7HV5aLXJFvt
rhOQcIZ66HNSM+5W66UJh/QMX6ubXtvI9d8CbLxGrRK1sz/bzqv4/zpfJs2PAwvh
+uzoKGOuPNVdhMjp+viMVjU1sF5S3NpTaCFaS8cLwfh+fKE9wNyVI794AUmAjNda
OwIDAQAB
-----END PUBLIC KEY-----
EOT;

    const ADMIN_ACTIVATE_URL = 'https://admin.sitecake.com/api/license/activate';

    /**
     * Check whether the current domain has a valid license.
     *
     * Returns true immediately for local/dev environments.
     * For real domains: reads license.php, verifies RSA signature, handles domain changes.
     */
    public static function check(string $licensePath): bool
    {
        $domain = self::currentDomain();

        if (self::isLocal($domain)) {
            return true;
        }

        $license = self::read($licensePath);
        if ($license === null) {
            return false;
        }

        // Domain has changed since activation — license is not valid for this domain.
        // Domain transfers are handled via support, not self-service.
        if (($license['domain'] ?? '') !== $domain) {
            return false;
        }

        return self::verifySignature($license);
    }

    /**
     * Activate a license key for the current domain.
     * Called when the user submits their license key in the login dialog.
     *
     * @return array { status: 0 } on success, { status: 1, message: string } on failure
     */
    public static function activate(string $key, string $licensePath): array
    {
        $domain = self::currentDomain();

        $result = self::post(self::ADMIN_ACTIVATE_URL, [
            'key'    => $key,
            'domain' => $domain,
        ]);

        if ($result === null) {
            return ['status' => 1, 'message' => 'Could not reach license server. Check your internet connection.'];
        }

        if (($result['status'] ?? 1) !== 0) {
            return ['status' => 1, 'message' => $result['message'] ?? 'Invalid license key'];
        }

        $saved = self::write($licensePath, [
            'key'    => $key,
            'domain' => $domain,
            'token'  => $result['token'],
            'issued' => $result['issued'],
        ]);

        if (!$saved) {
            return ['status' => 1, 'message' => 'Could not write license file. Check write permissions on the sitecake directory.'];
        }

        return ['status' => 0];
    }

    /**
     * Returns true if the given domain is a local/dev environment that
     * does not require a license.
     */
    public static function isLocal(string $domain): bool
    {
        // Localhost variants
        if (in_array($domain, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true)) {
            return true;
        }

        // Local TLDs
        if (preg_match('/\.(local|localhost|test|dev|example|invalid)$/i', $domain)) {
            return true;
        }

        // Private IP ranges (IPv4)
        if (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            if (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
                return true; // private or reserved
            }
        }

        return false;
    }

    /**
     * Return the current domain, normalized (lowercase, no www., no port).
     */
    public static function currentDomain(): string
    {
        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
        $host = strtolower($host);
        $host = preg_replace('/:\d+$/', '', $host);   // strip port
        $host = preg_replace('/^www\./', '', $host);  // strip www.
        return $host;
    }

    // ── Private helpers ────────────────────────────────────────────────────────

    /**
     * Verify the RSA-SHA256 signature in the license file.
     * Signed data format: "{key}:{domain}:{issued}"
     */
    private static function verifySignature(array $license): bool
    {
        $token  = $license['token']  ?? '';
        $key    = $license['key']    ?? '';
        $domain = $license['domain'] ?? '';
        $issued = $license['issued'] ?? '';

        if (!$token || !$key || !$domain || !$issued) {
            return false;
        }

        $pubKey = openssl_pkey_get_public(self::PUBLIC_KEY);
        if (!$pubKey) {
            return false;
        }

        $data      = "$key:$domain:$issued";
        $signature = base64_decode($token);

        return openssl_verify($data, $signature, $pubKey, OPENSSL_ALGO_SHA256) === 1;
    }

    /**
     * Read the license.php file and return its data array, or null if not found / invalid.
     */
    private static function read(string $path): ?array
    {
        if (!file_exists($path)) {
            return null;
        }

        // license.php format: <?php return ['key'=>..., 'domain'=>..., 'token'=>..., 'issued'=>...];
        $data = @include $path;
        if (!is_array($data)) {
            return null;
        }

        return $data;
    }

    /**
     * Write license data to license.php as a PHP return array.
     */
    private static function write(string $path, array $data): bool
    {
        $php = "<?php\n// SiteCake license — do not edit manually\nreturn " . var_export($data, true) . ";\n";
        return file_put_contents($path, $php, LOCK_EX) !== false;
    }

    /**
     * POST JSON to a URL and return decoded response, or null on failure.
     */
    private static function post(string $url, array $data): ?array
    {
        $ctx = stream_context_create([
            'http' => [
                'method'  => 'POST',
                'header'  => "Content-Type: application/json\r\n",
                'content' => json_encode($data),
                'timeout' => 10,
                'ignore_errors' => true,
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ]);

        $raw = @file_get_contents($url, false, $ctx);
        if ($raw === false) {
            return null;
        }

        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }
}
