<?php
namespace GlpiPlugin\Wbstore;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

class StoreClient {

   private string $apiBase;
   private string $licenseKey;
   private string $instanceId;
   private string $domain;
   private string $token;
   private string $portalToken;
   private bool $sslVerify;

   private int $lastCode = 0;
   private string $lastError = '';
   private string $lastUrl = '';

   public function __construct() {
      $this->apiBase    = rtrim((string)Config::get('api_base', ''), '/');
      $this->licenseKey = (string)Config::get('license_key', '');
      $this->instanceId = (string)Config::get('instance_id', '');
      // Domain used by the Store validation (can differ from GLPI host)
      $this->domain     = trim((string)Config::get('domain', ''));
      $this->token      = trim((string)Config::get('token', ''));
      $this->portalToken = trim((string)Config::get('portal_token', ''));
      if ($this->portalToken === '') {
         // Always have a portal_token to bind requests/iframe sessions.
         $this->portalToken = Utils::ensurePortalToken();
      }
      $this->sslVerify  = ((int)Config::get('ssl_verify', 1)) === 1;

      // Ensure we always have an instance id (per GLPI installation)
      if ($this->instanceId === '') {
         $this->instanceId = Utils::uuidv4();
         Config::set(['instance_id' => $this->instanceId]);
      }
   }

   public function getLastCode(): int { return $this->lastCode; }
   public function getLastError(): string { return $this->lastError; }
   public function getLastUrl(): string { return $this->lastUrl; }

   
   /**
    * Low-level helper used internally by WBStore (license/webhook).
    * Returns: ['ok'=>bool,'data'=>array,'code'=>int,'error'=>string]
    */
   public function postRaw(string $path, array $payload = [], string $profile = 'api'): array {
      $data = $this->post($path, $payload, $profile);
      $code = $this->getLastCode();
      $err  = $this->getLastError();
      // When HTTP fails, $data may be [].
      $ok = ($code >= 200 && $code < 300);
      // Some endpoints return {ok:false,...} with 200; treat as not ok.
      if (is_array($data) && isset($data['ok']) && !$data['ok']) {
         $ok = false;
      }
      return ['ok'=>$ok,'data'=>$data,'code'=>$code,'error'=>$err];
   }

public function ping(): array {
      return $this->get('/ping');
   }

   public function listPackages(): array {
      return $this->get('/packages', $this->authParams());
   }

   /**
    * Consulta se já existe um acesso liberado para o e-mail/telefone informado.
    * Espera retorno: { ok: true, license_key: "..." }.
    */
   public function lookupRegistration(array $payload): array {
      $path = (string)Config::get('lookup_path', '/register-lookup');
      return $this->post($path, $this->withInstance($payload));
   }

   /**
    * Faz o cadastro/liberação inicial de acesso.
    * Espera retorno: { ok: true, license_key: "..." }.
    */
   public function registerAccess(array $payload): array {
      $path = (string)Config::get('register_path', '/register-free');
      return $this->post($path, $this->withInstance($payload));
   }

   /**
    * Valida um código de doação (Pix) e libera o acesso.
    * Espera retorno: { ok: true, license_key: "..." }.
    */
   public function verifyDonation(array $payload): array {
      $path = (string)Config::get('donation_path', '/donation-verify');
      return $this->post($path, $this->withInstance($payload));
   }

   public function getPackage(string $slug): array {
      return $this->get('/packages/' . rawurlencode($slug), $this->authParams());
   }

   public function downloadPackage(string $slug): array {
      // Returns ['tmp' => '/path/file.zip', 'sha256' => '...', 'meta' => []]
      // Some stores don't have /packages/{slug}; so we fallback to /packages list.

      $meta = $this->getPackage($slug);
      if (empty($meta)) {
         $list = $this->listPackages();
         $pkgs = $list['packages'] ?? $list;
         if (is_array($pkgs)) {
            foreach ($pkgs as $p) {
               if (!is_array($p)) {
                  continue;
               }
               if (($p['slug'] ?? '') === $slug) {
                  $meta = $p;
                  break;
               }
            }
         }
      }
      if (empty($meta)) {
         return ['error' => 'Pacote não encontrado na Store (slug: ' . $slug . ').'];
      }

      // Prefer server-provided download_url, but if missing build a download-redirect.
      $zipUrl = $meta['download_url'] ?? '';
      $expected = $meta['sha256'] ?? '';

      if ($zipUrl === '') {
         // Try by package_id if exists; else by slug.
         $params = $this->authParams();
         $pkgId = $meta['package_id'] ?? ($meta['id'] ?? null);
         if (!empty($pkgId)) {
            $params['package_id'] = $pkgId;
         } else {
            $params['slug'] = $slug;
         }
         $zipUrl = $this->apiBase . '/download-redirect';
         $zipUrl = $this->appendQuery($zipUrl, $params);
      } else {
         // Ensure auth params are appended (many endpoints expect them)
         $zipUrl = $this->appendQuery($zipUrl, $this->authParams());
      }

      $dl = $zipUrl;

      $tmp = Utils::tmpFile('wbstore_' . preg_replace('/[^a-z0-9_\-]/i', '_', $slug), 'zip');
      $ok = $this->downloadTo($dl, $tmp);
      if (!$ok) {
         $code = $this->getLastCode();
         $err  = $this->getLastError();
         $msg  = 'Falha ao baixar o ZIP do pacote.';
         if ($code > 0) {
            $msg .= ' (HTTP ' . $code . ')';
         }
         if ($err !== '') {
            $msg .= ' - ' . $err;
         }
         return ['error' => $msg];
      }

      $real = Utils::sha256File($tmp);
      if ($expected !== '' && strtolower($real) !== strtolower($expected)) {
         @unlink($tmp);
         return ['error' => 'SHA256 não confere. Esperado ' . $expected . ' / Obtido ' . $real];
      }

      return ['tmp' => $tmp, 'sha256' => $real, 'meta' => $meta];
   }

   private function authParams(): array {
      $params = [
         'license_key' => $this->licenseKey,
         'instance_id' => $this->instanceId,
         // If domain was set in config, use it; otherwise use current host
         'domain'      => ($this->domain !== '' ? $this->domain : Utils::currentDomain()),
      ];
      // Compat: alguns endpoints antigos usam apenas "token".
      // Se token não foi configurado, reutiliza portal_token (estável) para não quebrar download/avisos.
      if ($this->token !== '') {
         $params['token'] = $this->token;
      } elseif ($this->portalToken !== '') {
         $params['token'] = $this->portalToken;
      }
      if ($this->portalToken !== '') {
         $params['portal_token'] = $this->portalToken;
      }
      return $params;
   }

   private function withInstance(array $payload): array {
      // Normalize and always send instance/domain
      $payload['instance_id'] = $this->instanceId;
      $payload['domain']      = ($this->domain !== '' ? $this->domain : Utils::currentDomain());
      // Also accept legacy param name
      $payload['glpi_domain'] = $payload['domain'];
      return $payload;
   }

   private function get(string $path, array $params = [], string $profile = 'api'): array {
      if (empty($this->apiBase)) {
         $this->lastUrl = '';
         $this->lastCode = 0;
         $this->lastError = 'API Base não configurada.';
         return [];
      }
      $url = $this->apiBase . $path;
      if (!empty($params)) {
         $url = $this->appendQuery($url, $params);
      }

      $this->lastUrl = $url;

      $this->lastUrl = $url;
      $resp = $this->http('GET', $url, null, false, $profile);
      $this->lastCode = (int)($resp['code'] ?? 0);
      $this->lastError = (string)($resp['error'] ?? '');
      if ($resp['code'] < 200 || $resp['code'] >= 300) {
         return [];
      }
      return Utils::jsonDecode($resp['body']);
   }

   private function post(string $path, array $payload = [], string $profile = 'api'): array {
      if (empty($this->apiBase)) {
         $this->lastUrl = '';
         $this->lastCode = 0;
         $this->lastError = 'Endereço do servidor não configurado.';
         return [];
      }

      // Allow absolute URLs as override
      $url = (preg_match('#^https?://#i', $path)) ? $path : ($this->apiBase . $path);
      $this->lastUrl = $url;

      $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
      $resp = $this->http('POST', $url, $body, false, $profile);
      $this->lastCode = (int)($resp['code'] ?? 0);
      $this->lastError = (string)($resp['error'] ?? '');
      if ($resp['code'] < 200 || $resp['code'] >= 300) {
         // Try parse server message if any
         $data = Utils::jsonDecode((string)($resp['body'] ?? ''));
         if (!empty($data['message']) && is_string($data['message'])) {
            $this->lastError = $data['message'];
         }
         return $data ?: [];
      }
      return Utils::jsonDecode((string)($resp['body'] ?? ''));
   }

   private function appendQuery(string $url, array $params): string {
      $sep = (strpos($url, '?') === false) ? '?' : '&';
      return $url . $sep . http_build_query($params);
   }

   private function downloadTo(string $url, string $dest): bool {
      $this->lastUrl = $url;
      $resp = $this->http('GET', $url, null, true, 'download');
      $this->lastCode = (int)($resp['code'] ?? 0);
      $this->lastError = (string)($resp['error'] ?? '');
      if ($resp['code'] < 200 || $resp['code'] >= 300) {
         return false;
      }
      return (file_put_contents($dest, $resp['body']) !== false);
   }

   private function http(string $method, string $url, ?string $body = null, bool $binary = false, string $profile = 'api'): array {
      // cURL preferred
      if (function_exists('curl_init')) {
         $ch = curl_init();
         curl_setopt($ch, CURLOPT_URL, $url);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
         curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
         // Prefer IPv4 to avoid container IPv6 routing issues (common cause of 0 bytes/timeouts).
         if (defined('CURL_IPRESOLVE_V4') && defined('CURLOPT_IPRESOLVE')) {
            curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
         }
         // Accept gzip/deflate automatically.
         if (defined('CURLOPT_ENCODING')) {
            curl_setopt($ch, CURLOPT_ENCODING, '');
         }
                  // Timeouts curtos para não travar o GLPI.
         // Para download (binary=true), usamos tempo maior.
         // Profiles:
         // - fast: quick reachability checks (no UI blocking)
         // - api: normal JSON calls (catalog, packages)
         // - license: license validation/activation (may take longer)
         // - download: binary ZIP download
         $profiles = [
            'fast'    => ['timeout'=>1000,  'connect'=>500],
            'api'     => ['timeout'=>4000,  'connect'=>500],
            'license' => ['timeout'=>8000,  'connect'=>800],
            'download'=> ['timeout'=>120000,'connect'=>10000],
         ];
         if (!isset($profiles[$profile])) {
            $profile = $binary ? 'download' : 'api';
         }
         $timeoutMs = $binary ? $profiles['download']['timeout'] : $profiles[$profile]['timeout'];
         $connectMs = $binary ? $profiles['download']['connect'] : $profiles[$profile]['connect'];
         curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
         if (defined('CURLOPT_TIMEOUT_MS')) {
            curl_setopt($ch, CURLOPT_TIMEOUT_MS, $timeoutMs);
         } else {
            curl_setopt($ch, CURLOPT_TIMEOUT, (int)ceil($timeoutMs/1000));
         }
         if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $connectMs);
         } else {
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int)ceil($connectMs/1000));
         }
         curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->sslVerify ? true : false);
         curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->sslVerify ? 2 : 0);

         $headers = [
            'User-Agent: WBStore/GLPI10',
            'Accept: application/json'
         ];

         // Send auth also via headers (some WP stacks validate headers)
         if ($this->licenseKey !== '') {
            $headers[] = 'license_key: ' . $this->licenseKey;
         }
         if ($this->instanceId !== '') {
            $headers[] = 'instance_id: ' . $this->instanceId;
         }
         $hDomain = ($this->domain !== '' ? $this->domain : Utils::currentDomain());
         if ($hDomain !== '') {
            $headers[] = 'domain: ' . $hDomain;
         }
         if ($this->token !== '') {
            $headers[] = 'token: ' . $this->token;
         } elseif ($this->portalToken !== '') {
            // Compat: quando não houver token configurado, envia portal_token também como token.
            $headers[] = 'token: ' . $this->portalToken;
         }
         if ($this->portalToken !== '') {
            $headers[] = 'portal_token: ' . $this->portalToken;
         }
         if ($binary) {
            $headers[1] = 'Accept: */*';
         }
         if ($method !== 'GET' && $body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
            $headers[] = 'Content-Type: application/json';
         }
         curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

         $out = curl_exec($ch);
         $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
         $err = '';
         if ($out === false) {
            $err = (string)curl_error($ch);
            $out = '';
            $code = 0;
         }
	         // PHP 8.5+: curl_close() is deprecated (no effect since PHP 8.0).
	         // Keep compatibility with older PHP versions without triggering warnings.
	         if (defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80500) {
	            $canClose = false;
	            if (is_resource($ch)) {
	               $canClose = true;
	            } elseif (class_exists('CurlHandle') && $ch instanceof \CurlHandle) {
	               $canClose = true;
	            }
	            if ($canClose) {
	               curl_close($ch);
	            }
	         }
	         unset($ch);
         $resp = ['code' => $code, 'body' => $out];
         if ($err !== '') {
            $resp['error'] = $err;
         }
         return $resp;
      }

      // Fallback
      $ctx = stream_context_create([
         'http' => [
            'method'  => $method,
            'timeout' => 3,
            'header'  => "User-Agent: WBStore/GLPI10\r\n"
         ]
      ]);
      $out = @file_get_contents($url, false, $ctx);
      $code = 0;
      if (isset($http_response_header) && is_array($http_response_header)) {
         foreach ($http_response_header as $h) {
            if (preg_match('#^HTTP/\\S+\\s+(\\d{3})#', $h, $m)) {
               $code = (int)$m[1];
               break;
            }
         }
      }
      return ['code' => $code, 'body' => $out !== false ? $out : ''];
   }
}