<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class LicenseManager
{
    private $ci;
    private $cfg = array();
    private $runtimeStatus;

    public function __construct()
    {
        $this->ci =& get_instance();
        $this->ci->config->load('license', TRUE);
        $this->cfg = (array) $this->ci->config->item('license');
        $this->ci->load->helper('license');
    }

    public function enabled()
    {
        return !empty($this->cfg['license_enabled']);
    }

    public function fingerprint()
    {
        return license_server_fingerprint(array(
            'include_ip' => !empty($this->cfg['license_include_server_ip']),
            'include_mac' => !empty($this->cfg['license_include_mac']),
        ));
    }

    public function status($forceOnline = FALSE)
    {
        if ($this->runtimeStatus !== NULL && !$forceOnline) return $this->runtimeStatus;
        if (!$this->enabled()) return $this->runtimeStatus = $this->result(TRUE, 'disabled', 'Proteksi lisensi belum diaktifkan.');

        $cache = $this->readCache();
        $verified = $this->verifyEnvelope($cache);
        if (!$verified['valid']) {
            return $this->runtimeStatus = $this->result(FALSE, 'cache_invalid', $verified['message']);
        }
        $payload = $verified['payload'];
        $localMatch = isset($payload['fingerprint_hash']) && hash_equals((string) $payload['fingerprint_hash'], $this->fingerprint());
        if (!$localMatch) return $this->runtimeStatus = $this->result(FALSE, 'fingerprint_mismatch', 'Lisensi terikat pada server lain.');

        $now = time();
        if (!$forceOnline && $this->payloadUsable($payload) && $now <= (int) $payload['next_check_at']) {
            return $this->runtimeStatus = $this->result(TRUE, 'valid_cache', 'Lisensi valid.', $payload);
        }

        $online = $this->validateOnline(isset($payload['license_key']) ? $payload['license_key'] : '');
        if ($online['success']) return $this->runtimeStatus = $this->result(TRUE, 'valid_online', 'Lisensi berhasil divalidasi.', $online['payload']);
        if ($this->payloadUsable($payload) && $now <= (int) $payload['grace_until'] && !empty($online['network_error'])) {
            return $this->runtimeStatus = $this->result(TRUE, 'grace', 'Server lisensi tidak tersedia; masa tenggang aktif.', $payload);
        }
        return $this->runtimeStatus = $this->result(FALSE, isset($online['code']) ? $online['code'] : 'restricted', isset($online['message']) ? $online['message'] : 'Lisensi tidak dapat divalidasi.', $payload);
    }

    public function activate($licenseKey)
    {
        return $this->requestAndStore('activate', trim($licenseKey));
    }

    public function recheck()
    {
        $verified = $this->verifyEnvelope($this->readCache());
        if (!$verified['valid']) return $this->result(FALSE, 'cache_invalid', $verified['message']);
        return $this->requestAndStore('validate', $verified['payload']['license_key']);
    }

    private function validateOnline($licenseKey)
    {
        return $this->requestAndStore('validate', $licenseKey);
    }

    private function requestAndStore($action, $licenseKey)
    {
        if ($licenseKey === '') return $this->result(FALSE, 'missing_key', 'License key wajib diisi.');
        $response = $this->post($action, array(
            'license_key' => $licenseKey,
            'product_code' => $this->cfg['license_product_code'],
            'domain' => license_normalize_domain(),
            'fingerprint_hash' => $this->fingerprint(),
        ));
        if (!$response['success']) return $response;
        $verified = $this->verifyEnvelope($response['envelope']);
        if (!$verified['valid']) return $this->result(FALSE, 'bad_server_signature', $verified['message']);
        if (!hash_equals($this->fingerprint(), (string) $verified['payload']['fingerprint_hash'])) {
            return $this->result(FALSE, 'fingerprint_mismatch', 'Fingerprint pada respons tidak cocok.');
        }
        if (!$this->writeCache($response['envelope'])) return $this->result(FALSE, 'cache_write_failed', 'Cache lisensi tidak dapat ditulis.');
        $this->runtimeStatus = NULL;
        return array('success' => TRUE, 'code' => 'valid', 'message' => 'Lisensi valid.', 'payload' => $verified['payload']);
    }

    private function post($action, array $body)
    {
        if (!function_exists('curl_init')) return $this->networkError('Ekstensi cURL belum aktif.');
        $url = rtrim($this->cfg['license_server_url'], '/') . '/' . $action;
        $ch = curl_init($url);
        curl_setopt_array($ch, array(
            CURLOPT_POST => TRUE, CURLOPT_RETURNTRANSFER => TRUE,
            CURLOPT_HTTPHEADER => array('Accept: application/json', 'Content-Type: application/json'),
            CURLOPT_POSTFIELDS => json_encode($body),
            CURLOPT_CONNECTTIMEOUT => (int) $this->cfg['license_connect_timeout'],
            CURLOPT_TIMEOUT => (int) $this->cfg['license_timeout'],
            CURLOPT_SSL_VERIFYPEER => !empty($this->cfg['license_verify_tls']),
            CURLOPT_SSL_VERIFYHOST => !empty($this->cfg['license_verify_tls']) ? 2 : 0,
        ));
        $raw = curl_exec($ch); $error = curl_error($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
        if ($raw === FALSE || $status === 0 || $status >= 500) return $this->networkError($error ?: 'Server lisensi tidak tersedia.');
        $json = json_decode($raw, TRUE);
        if (!is_array($json)) return $this->result(FALSE, 'invalid_response', 'Respons server lisensi tidak valid.');
        if (empty($json['success'])) return $this->result(FALSE, isset($json['code']) ? $json['code'] : 'invalid', isset($json['message']) ? $json['message'] : 'Lisensi ditolak.');
        if (empty($json['data']['payload_b64']) || empty($json['data']['signature'])) return $this->result(FALSE, 'invalid_response', 'Envelope lisensi tidak lengkap.');
        return array('success' => TRUE, 'envelope' => $json['data']);
    }

    private function verifyEnvelope($envelope)
    {
        if (!is_array($envelope) || empty($envelope['payload_b64']) || empty($envelope['signature'])) return array('valid' => FALSE, 'message' => 'Cache lisensi belum tersedia.');
        $payloadJson = base64_decode(strtr($envelope['payload_b64'], '-_', '+/'), TRUE);
        $signature = base64_decode(strtr($envelope['signature'], '-_', '+/'), TRUE);
        $key = @openssl_pkey_get_public($this->cfg['license_public_key']);
        if ($payloadJson === FALSE || $signature === FALSE || !$key) return array('valid' => FALSE, 'message' => 'Public key atau envelope lisensi tidak valid.');
        $ok = openssl_verify($envelope['payload_b64'], $signature, $key, OPENSSL_ALGO_SHA256) === 1;
        if (is_resource($key)) openssl_free_key($key);
        $payload = json_decode($payloadJson, TRUE);
        if (!$ok || !is_array($payload)) return array('valid' => FALSE, 'message' => 'Signature cache lisensi tidak valid.');
        foreach (array('license_key','fingerprint_hash','status','checked_at','next_check_at','grace_until') as $field) {
            if (!array_key_exists($field, $payload)) return array('valid' => FALSE, 'message' => 'Payload lisensi tidak lengkap.');
        }
        return array('valid' => TRUE, 'payload' => $payload);
    }

    private function payloadUsable(array $p)
    {
        $now = time(); $skew = (int) $this->cfg['license_clock_skew_seconds'];
        if ($p['status'] !== 'active' || (int) $p['checked_at'] > $now + $skew) return FALSE;
        return empty($p['expires_at']) || strtotime($p['expires_at']) + $skew >= $now;
    }

    private function readCache()
    {
        $file = $this->cfg['license_cache_file'];
        if (!is_file($file) || !is_readable($file)) return NULL;
        $data = json_decode((string) @file_get_contents($file), TRUE);
        return is_array($data) ? $data : NULL;
    }

    private function writeCache(array $data)
    {
        $file = $this->cfg['license_cache_file']; $dir = dirname($file);
        if (!is_dir($dir) || !is_writable($dir)) return FALSE;
        $tmp = tempnam($dir, 'lic_');
        if ($tmp === FALSE) return FALSE;
        $ok = file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES), LOCK_EX) !== FALSE && @rename($tmp, $file);
        if (!$ok && is_file($tmp)) @unlink($tmp);
        return $ok;
    }

    private function networkError($message) { $r = $this->result(FALSE, 'network_error', $message); $r['network_error'] = TRUE; return $r; }
    private function result($success, $code, $message, $payload = array()) { return array('success'=>(bool)$success,'code'=>$code,'message'=>$message,'payload'=>$payload); }
}

