<?php
namespace GlpiPlugin\Wbstore;

use Config as GlpiConfig;

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

/**
 * License validator for WB Store.
 * - Validates against Portal-WB endpoint /wp-json/wbstore/v1/license/validate
 * - Caches result (default 6 hours)
 * - Offline grace period (default 7 days) if last validation was OK
 */
class License {

   private const CACHE_KEY_VALID_UNTIL = 'license_valid_until';
   private const CACHE_KEY_LAST_OK_AT  = 'license_last_ok_at';
   
   const CACHE_KEY_LAST_ERR_TYPE = 'license_last_err_type';
   const CACHE_KEY_LAST_ERR_CODE = 'license_last_err_code';
   const CACHE_KEY_LAST_DENIED_AT = 'license_last_denied_at';
private const CACHE_KEY_LAST_FAIL_TYPE = 'license_last_fail_type'; // denied|network
   private const CACHE_KEY_LAST_FAIL_AT   = 'license_last_fail_at';


   public static function isConfigured(): bool {
      $lk = trim((string)Config::get('license_key', ''));
      $api = trim((string)Config::get('api_base', ''));
      return ($lk !== '' && $api !== '');
   }

   public static function isValid(bool $force = false): bool {
      if (!self::isConfigured()) {
         return false;
      }

      $now = time();
      $cacheUntil  = (int)Config::get(self::CACHE_KEY_VALID_UNTIL, 0);
      $lastOk      = (int)Config::get(self::CACHE_KEY_LAST_OK_AT, 0);
      $lastErrType = (string)Config::get(self::CACHE_KEY_LAST_ERR_TYPE, '');
      $lastErrCode = (int)Config::get(self::CACHE_KEY_LAST_ERR_CODE, 0);
      $lastDenied  = (int)Config::get(self::CACHE_KEY_LAST_DENIED_AT, 0);

      // Uso normal do GLPI: consulta SOMENTE o cache local.
      // Validação remota fica restrita ao cron noturno ou à sincronização manual.
      if (!$force) {
         if ($lastErrType === 'deny' || ($lastDenied > 0 && $lastDenied >= $lastOk)
            || ($lastErrCode >= 400 && $lastErrCode < 500 && $lastErrCode != 429)) {
            return false;
         }

         if ($cacheUntil > $now) {
            return true;
         }

         $graceDays = (int)Config::get('offline_grace_days', 7);
         $graceDays = max(0, min(30, $graceDays));
         if ($graceDays > 0 && $lastOk > 0 && ($now - $lastOk) <= ($graceDays * 86400)) {
            return true;
         }

         return false;
      }

      $ok = self::validateRemote();
      if ($ok) {
         Config::set([
            self::CACHE_KEY_VALID_UNTIL     => self::nextNightlyDeadline($now),
            self::CACHE_KEY_LAST_OK_AT      => $now,
            self::CACHE_KEY_LAST_FAIL_TYPE  => '',
            self::CACHE_KEY_LAST_FAIL_AT    => 0,
            self::CACHE_KEY_LAST_DENIED_AT  => 0,
         ]);
         return true;
      }

      // Remote validation failed
      $lastErrType = (string)Config::get(self::CACHE_KEY_LAST_ERR_TYPE, '');
      $lastErrCode = (int)Config::get(self::CACHE_KEY_LAST_ERR_CODE, 0);

      // Se o portal respondeu NEGANDO (revogado / inválido / 4xx), NÃO aplica grace.
      if ($lastErrType === 'deny' || ($lastErrCode >= 400 && $lastErrCode < 500 && $lastErrCode != 429)) {
         Config::set([self::CACHE_KEY_VALID_UNTIL => 0]);
         return false;
      }

      // Caso contrário (timeout/5xx/rede), permite grace se houve OK recente.
      $graceDays = (int)Config::get('offline_grace_days', 7);
      $graceDays = max(0, min(30, $graceDays));
      $lastOk = (int)Config::get(self::CACHE_KEY_LAST_OK_AT, 0);

      if ($graceDays > 0 && $lastOk > 0) {
         if (($now - $lastOk) <= ($graceDays * 86400)) {
            return true;
         }
      }

      Config::set([self::CACHE_KEY_VALID_UNTIL => 0]);
      return false;
   }

   private static function nextNightlyDeadline(int $now): int {
      $tzName = @date_default_timezone_get();
      if (!is_string($tzName) || $tzName === '') {
         $tzName = 'UTC';
      }
      $tz = new \DateTimeZone($tzName);
      $dt = (new \DateTimeImmutable('@' . $now))->setTimezone($tz);
      return $dt->modify('tomorrow')->setTime(5, 0, 0)->getTimestamp();
   }

   private static function validateRemote(): bool {
      try {
         $client = new StoreClient();

         $payload = [
            'license_key' => (string)Config::get('license_key', ''),
            'instance_id' => (string)Config::get('instance_id', ''),
            'domain'      => (string)Config::get('domain', ''),
            'portal_token' => Utils::ensurePortalToken(),
         ];
         if (trim($payload['domain']) === '') {
            $payload['domain'] = Utils::currentDomain();
         }

         $resp = $client->postRaw('/license/validate', $payload, 'license');

         // Reset last error meta
         Config::set([
            self::CACHE_KEY_LAST_ERR_TYPE => '',
            self::CACHE_KEY_LAST_ERR_CODE => 0,
            self::CACHE_KEY_LAST_DENIED_AT => 0,
         ]);

         $code = (int)($resp['code'] ?? 0);
         $data = $resp['data'] ?? null;

         // 4xx (exceto 429) = credencial/licença inválida => negar sem grace
         if (!($resp['ok'] ?? false)) {
            $deny = ($code >= 400 && $code < 500 && $code != 429);
            Config::set([
               self::CACHE_KEY_LAST_ERR_CODE => $code,
               self::CACHE_KEY_LAST_ERR_TYPE => ($deny ? 'deny' : 'temp'),
            ]);
            if ($deny) {
               Config::set([self::CACHE_KEY_LAST_DENIED_AT => time()]);
            }
            return false;
         }

         if (!is_array($data)) {
            Config::set([
               self::CACHE_KEY_LAST_ERR_CODE => $code,
               self::CACHE_KEY_LAST_ERR_TYPE => 'temp',
            ]);
            return false;
         }

         $valid = null;
         if (array_key_exists('valid', $data)) {
            $valid = (bool)$data['valid'];
         } elseif (array_key_exists('ok', $data)) {
            $valid = (bool)$data['ok'];
         } elseif (array_key_exists('status', $data)) {
            $valid = ((string)$data['status'] === 'active');
         }

         if ($valid !== true) {
            Config::set([
               self::CACHE_KEY_LAST_ERR_CODE => $code,
               self::CACHE_KEY_LAST_ERR_TYPE => 'deny',
               self::CACHE_KEY_LAST_DENIED_AT => time(),
            ]);
            return false;
         }

         return true;
      } catch (\Throwable $e) {
         Config::set([
            self::CACHE_KEY_LAST_ERR_TYPE => 'temp',
            self::CACHE_KEY_LAST_ERR_CODE => 0,
         ]);
         return false;
      }
   }
}
