/** FLOWhatsapp v2.0 — secure server-backed license service (MV3 service worker). */
(() => {
  'use strict';

  const CONFIG = Object.assign({
    appUrl: 'https://virality.com.br/FLOWhatsapp/painel-admin',
    resellerId: 'admin',
    validationIntervalHours: 1,
    offlineGraceHours: 1
  }, self.FLOWHATSAPP_LICENSE_CONFIG || {});

  const SESSION_KEY = 'flowhatsapp_license_session';
  const DEVICE_KEY  = 'flowhatsapp_device_id';
  const STATUS_KEY  = 'flowhatsapp_license_status';
  const API_PATH    = '/api/extension-license.php';

  function storageGet(keys)    { return chrome.storage.local.get(keys); }
  function storageSet(values)  { return chrome.storage.local.set(values); }
  function storageRemove(keys) { return chrome.storage.local.remove(keys); }

  async function sha256(value) {
    const bytes  = new TextEncoder().encode(String(value || ''));
    const digest = await crypto.subtle.digest('SHA-256', bytes);
    return Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('');
  }

  async function getMachineId() {
    const stored = await storageGet([DEVICE_KEY]);
    if (stored[DEVICE_KEY]) return stored[DEVICE_KEY];
    const seed = `${crypto.randomUUID()}|${navigator.userAgent || ''}|${chrome.runtime.id}`;
    const id   = (await sha256(seed)).slice(0, 32);
    await storageSet({ [DEVICE_KEY]: id });
    return id;
  }

  function endpoint() {
    const base = String(CONFIG.appUrl || '').replace(/\/$/, '');
    let url;
    try { url = new URL(base); } catch (_) { throw new Error('URL do servidor de licença inválida.'); }
    const local = ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
    if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
      throw new Error('O servidor de licença deve usar HTTPS.');
    }
    return url.href.replace(/\/$/, '') + API_PATH;
  }

  async function verifySignature(data) {
    if (!CONFIG.rsaPublicKey) {
      // Tests must opt in explicitly; every distributed/development package
      // contains the public key and therefore rejects unsigned responses.
      return CONFIG.allowUnsignedResponsesForTesting === true;
    }

    const signature = data.signature;
    if (!signature) {
      console.error("Signature missing from server response.");
      return false;
    }

    const serverTime = Number(data.serverTime || 0);
    const now = Date.now();
    if (serverTime === 0 || Math.abs(now - serverTime) > 900000) {
      console.error("Replay attack detected or clock drift too high. Server time:", serverTime, "Local time:", now);
      return false;
    }

    try {
      const { signature: _, ...unsignedData } = data;
      const dataStr = JSON.stringify(unsignedData);

      const cleanPem = CONFIG.rsaPublicKey.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\r|\n|\s/g, "");
      const binaryDerString = atob(cleanPem);
      const binaryDer = new Uint8Array(binaryDerString.length);
      for (let i = 0; i < binaryDerString.length; i++) {
        binaryDer[i] = binaryDerString.charCodeAt(i);
      }

      const cryptoKey = await crypto.subtle.importKey(
        "spki",
        binaryDer.buffer,
        {
          name: "RSASSA-PKCS1-v1_5",
          hash: { name: "SHA-256" }
        },
        false,
        ["verify"]
      );

      const binarySigString = atob(signature);
      const sigBytes = new Uint8Array(binarySigString.length);
      for (let i = 0; i < binarySigString.length; i++) {
        sigBytes[i] = binarySigString.charCodeAt(i);
      }

      const encoder = new TextEncoder();
      const dataBytes = encoder.encode(dataStr);

      return await crypto.subtle.verify(
        "RSASSA-PKCS1-v1_5",
        cryptoKey,
        sigBytes,
        dataBytes
      );
    } catch (err) {
      console.error("Error during signature verification:", err);
      return false;
    }
  }

  async function api(action, payload = {}) {
    const controller = new AbortController();
    const timeout    = setTimeout(() => controller.abort(), 15000);
    try {
      const response = await fetch(endpoint(), {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify({ action, resellerId: CONFIG.resellerId || 'admin', ...payload }),
        signal:  controller.signal,
        cache:   'no-store',
        credentials: 'omit'
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok) {
        const fallbackMessages = {
          401: 'A conexão com o servidor de licenças não foi autorizada. Atualize a extensão e tente novamente.',
          403: 'Esta instalação não está autorizada pelo servidor de licenças. Instale o pacote oficial mais recente.',
          404: 'O endereço do servidor de licenças não foi encontrado.',
          429: 'Muitas tentativas em pouco tempo. Aguarde um minuto e tente novamente.',
        };
        throw new Error(String(data.message || fallbackMessages[response.status] || `Servidor de licença indisponível (${response.status}).`));
      }

      if (['activate', 'check', 'heartbeat', 'bootstrap'].includes(action)) {
        const isValid = await verifySignature(data);
        if (!isValid) {
          throw new Error('Assinatura de resposta inválida ou expirada. Acesso bloqueado.');
        }
      }

      return data;
    } catch (error) {
      if (error?.name === 'AbortError') throw new Error('A conexão com o servidor de licenças excedeu o tempo limite. Tente novamente.');
      throw error;
    } finally {
      clearTimeout(timeout);
    }
  }

  /**
   * Normalize expiresAt from the server response.
   * The server now returns expiresAt as milliseconds (JS-compatible).
   * We keep it as-is if it's already a number, or parse ISO string.
   */
  function normalizeExpiresAt(rawExpiresAt) {
    if (!rawExpiresAt) return null;
    // Server returns milliseconds as number
    if (typeof rawExpiresAt === 'number') return rawExpiresAt;
    // ISO string fallback
    if (typeof rawExpiresAt === 'string') {
      const parsed = Date.parse(rawExpiresAt);
      return isNaN(parsed) ? null : parsed;
    }
    // Firestore timestamp object { seconds, nanoseconds } fallback
    if (typeof rawExpiresAt === 'object' && rawExpiresAt.seconds) {
      return rawExpiresAt.seconds * 1000;
    }
    return null;
  }

  function normalizeState(result, licenseKey, machineId) {
    const now       = Date.now();
    const expiresAt = normalizeExpiresAt(result.expiresAt);
    
    // Prefer server-authoritative entitlements and expiry. The fallback keeps
    // already installed clients compatible during a rolling API deployment.
    let expiryDays = result.expiryDays ?? null;
    if (expiryDays === null && expiresAt) {
      const diff = expiresAt - now;
      expiryDays = Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
    }
    const fallbackFeatures = ["basic","tabs","templates","quickReply","kanban","chatbotFlow","followUp","broadcast","schedule","calendar","status","validator","webhook","api","aiBot","gmapsExtractor","ead","reseller","branding","premium","advanced"];
    const featuresEnabled = Array.isArray(result.featuresEnabled)
      ? result.featuresEnabled.filter(feature => fallbackFeatures.includes(feature))
      : (result.valid === true ? fallbackFeatures : []);

    const authoritativeKey = String(result.licenseKey || licenseKey || '').trim().toUpperCase();
    return {
      key:         authoritativeKey,
      licenseKey:  authoritativeKey,
      machineId,
      configured:  configured(),
      hasLicense:  result.valid === true,
      valid:       result.valid === true,
      isValid:     result.valid === true,
      success:     result.success === true,
      plan:        result.plan || 'inactive',
      status:      result.status || (result.valid ? 'active' : 'invalid'),
      expiresAt,   // always milliseconds or null
      expiryDays,  // days remaining
      featuresEnabled, // enabled features list
      phoneNumber: result.clientPhone || result.phoneNumber || '',
      maxMachines: Number.isFinite(result.maxMachines) ? result.maxMachines : 1,
      resellerId:  result.resellerId || CONFIG.resellerId || 'admin',
      accessToken: typeof result.accessToken === 'string' ? result.accessToken : '',
      accessTokenExpiresAt: Number(result.accessTokenExpiresAt || 0),
      message:     result.message || '',
      error:       result.error || null,
      lastVerified: now,
      graceUntil:  now + Math.max(1, CONFIG.offlineGraceHours || 6) * 3600000,
      announcements: Array.isArray(result.announcements) ? result.announcements : []
    };
  }

  async function saveState(state) {
    const status = {
      configured: configured(),
      hasLicense: Boolean(state?.licenseKey),
      isValid:    state?.valid === true,
      plan:       state?.plan || 'inactive',
      expiresAt:  state?.expiresAt || null,
      message:    state?.message || ''
    };
    await storageSet({ [SESSION_KEY]: state, [STATUS_KEY]: status });
    return state;
  }

  async function getSession() {
    const stored = await storageGet([SESSION_KEY]);
    return stored[SESSION_KEY] || null;
  }

  function configured() {
    try {
      const url = new URL(String(CONFIG.appUrl || ''));
      return url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(url.hostname));
    } catch (_) { return false; }
  }

  async function activate(licenseKeyInput, _phoneNumber, explicitMachineId) {
    const licenseKey = String(licenseKeyInput || '').trim().toUpperCase();
    const machineId  = explicitMachineId || await getMachineId();
    // Authenticity is decided by the server's exact database lookup. Keeping
    // only a strict syntax check supports both legacy random keys and new keys.
    if (!/^FLOWHATSAPP-[A-Z0-9]{4}(?:-[A-Z0-9]{4}){3}$/.test(licenseKey)) {
      return { success: false, valid: false, isValid: false, error: 'invalid_key', message: 'Formato de chave inválido.' };
    }
    try {
      const result = await api('activate', { licenseKey, machineId, os: navigator.userAgent || '' });
      const state  = normalizeState(result, licenseKey, machineId);
      await saveState(state);
      return state;
    } catch (error) {
      return { success: false, valid: false, isValid: false, error: 'network', message: error.message || 'Falha de conexão.' };
    }
  }

  async function requestTrial(_phoneNumber, explicitMachineId, lead = {}) {
    const machineId = explicitMachineId || await getMachineId();
    try {
      // Bootstrap is the only first-install path. The server resolves an
      // existing paid/trial entitlement first and only then issues one trial
      // key for a never-seen device. Offline requests never unlock a trial.
      const result = await api('bootstrap', {
        machineId,
        leadName: String(lead.name || '').slice(0, 80),
        leadContact: String(lead.contact || '').slice(0, 100),
      });
      const state  = normalizeState(result, result.licenseKey || '', machineId);
      await saveState(state);
      return state;
    } catch (error) {
      return { success: false, valid: false, isValid: false, error: 'network', message: error.message || 'Falha ao iniciar teste.' };
    }
  }

  async function ensureTrial(phoneNumber, explicitMachineId) {
    const session = await getSession();
    if (session?.valid && (!session.expiresAt || session.expiresAt > Date.now())) return session;
    return requestTrial(phoneNumber, explicitMachineId);
  }

  async function check(explicitMachineId) {
    const session   = await getSession();
    if (!session?.licenseKey) return session;
    const machineId = explicitMachineId || session.machineId || await getMachineId();
    try {
      const result = await api('check', { licenseKey: session.licenseKey, machineId });
      return saveState(normalizeState(result, session.licenseKey, machineId));
    } catch (error) {
      if (error.message && error.message.includes('Assinatura')) {
        const invalid = { ...session, valid: false, isValid: false, success: false, error: 'compromised_signature', message: error.message };
        await saveState(invalid);
        return invalid;
      }
      if (session.valid && Date.now() <= Number(session.graceUntil || 0)) {
        return { ...session, offline: true, message: 'Validação offline temporária.' };
      }
      const invalid = { ...session, valid: false, isValid: false, success: false, error: 'offline_grace_expired', message: 'Não foi possível validar a licença.' };
      await saveState(invalid);
      return invalid;
    }
  }

  /**
   * getStatus — returns the current license state.
   * If no session exists, automatically starts the 7-day trial
   * so the extension can work immediately on first install.
   */
  async function getStatus(explicitMachineId) {
    const session = await getSession();
    const machineId = explicitMachineId || session?.machineId || await getMachineId();

    // No local session → server bootstrap is mandatory. It may restore an
    // entitlement after a reinstall, create the first trial, or block access.
    if (!session) {
      // A trial may only be created after WhatsApp's content bridge supplies
      // the canonical device fingerprint. Opening the popup/service worker by
      // itself must never mint a trial with the random fallback identifier.
      if (!explicitMachineId) {
        return {
          configured: configured(), hasLicense: false, valid: false, isValid: false,
          success: false, plan: 'inactive', status: 'not_initialized', machineId: '',
          message: 'Abra o WhatsApp Web para identificar este computador.'
        };
      }
      return requestTrial(null, machineId);
    }

    // Session exists but expiresAt has passed
    if (session.expiresAt && session.expiresAt <= Date.now()) {
      if (session.licenseKey) return check(machineId);
      // Trial expired: try to extend or re-request
      return requestTrial(null, machineId);
    }

    // Valid session within time window — check if needs re-validation
    const interval = Math.max(1, CONFIG.validationIntervalHours || 1) * 3600000;
    if (session.licenseKey && Date.now() - Number(session.lastVerified || 0) > interval) {
      return check(machineId);
    }

    // If the explicit machineId is provided and differs from session, re-verify
    if (explicitMachineId && session.machineId !== explicitMachineId) {
      if (session.licenseKey) return check(explicitMachineId);
      return requestTrial(null, explicitMachineId);
    }

    return session;
  }

  async function quickValidate(force = false) {
    const session = await getSession();
    const state   = force && session?.licenseKey ? await check(session.machineId) : await getStatus();
    return state?.valid === true && (!state.expiresAt || state.expiresAt > Date.now());
  }

  async function getInstallationCode() {
    const session = await getSession();
    // Once bootstrap/activation occurs, session.machineId is the exact
    // fingerprint registered by the server. Never expose a different code.
    return String(session?.machineId || '');
  }

  async function getApiCredentials() {
    return { success: false, error: 'credentials_not_exposed' };
  }

  /** Returns a newly checked, short-lived credential for extension feature APIs. */
  async function getAccessToken() {
    const session = await getSession();
    if (!session?.licenseKey) return { success: false, error: 'license_required' };
    const state = await check(session.machineId || await getMachineId());
    if (!state?.valid || !state.accessToken) return { success: false, error: 'license_invalid' };
    return {
      success: true,
      accessToken: state.accessToken,
      expiresAt: state.accessTokenExpiresAt || 0,
      licenseKey: state.licenseKey,
    };
  }

  async function reportHeartbeat({ machineId, whatsappState } = {}) {
    const session = await getSession();
    if (!session?.licenseKey) return { success: false };
    try {
      const result = await api('heartbeat', {
        licenseKey:    session.licenseKey,
        machineId:     machineId || session.machineId || await getMachineId(),
        whatsappState: whatsappState || 'unknown'
      });
      if (result?.valid === false) await saveState(normalizeState(result, session.licenseKey, session.machineId));
      return result;
    } catch (error) {
      if (error.message && error.message.includes('Assinatura')) {
        await saveState({ ...session, valid: false, isValid: false, success: false, error: 'compromised_signature', message: error.message });
      }
      return { success: false, error: error.message };
    }
  }

  async function publicConfig() {
    return api('public_config');
  }

  async function deactivate() {
    await storageRemove([SESSION_KEY, STATUS_KEY]);
    return { success: true };
  }

  self.FLOWhatsappLicenseService = {
    configured, getMachineId, getSession, getStatus, activate, requestTrial,
    ensureTrial, check, quickValidate, getInstallationCode, getApiCredentials,
    getAccessToken, reportHeartbeat, publicConfig, deactivate
  };
})();
