const fs = require('node:fs/promises');
const { constants } = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const { createHash, generateKeyPairSync, createPrivateKey, createPublicKey, sign, randomUUID } = require('node:crypto');

const SERVICE = 'https://t.mailtingpot.com';
const SERVICE_ORIGINS = Object.freeze([SERVICE, 'https://transferwing.com', 'https://relyo.online']);
const runFile = promisify(execFile);
const sha256 = text => createHash('sha256').update(text, 'utf8').digest('hex');
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

async function machineFingerprint(platform = process.platform) {
  let identifier;
  if (platform === 'darwin') {
    const { stdout } = await runFile('/usr/sbin/ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { timeout: 5000, maxBuffer: 256 * 1024 });
    identifier = stdout.match(/"IOPlatformUUID"\s*=\s*"([0-9a-f-]+)"/i)?.[1];
  } else if (platform === 'win32') {
    const root = /^[a-z]:\\Windows$/i.test(process.env.SystemRoot || '') ? process.env.SystemRoot : 'C:\\Windows';
    const executable = path.win32.join(root, 'System32', 'reg.exe');
    const { stdout } = await runFile(executable, ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid', '/reg:64'],
      { timeout: 5000, maxBuffer: 64 * 1024, windowsHide: true });
    identifier = stdout.match(/MachineGuid\s+REG_SZ\s+([0-9a-f-]+)/i)?.[1];
  } else throw new Error('Activation disponible uniquement sur macOS et Windows.');
  if (!identifier || !GUID.test(identifier)) throw new Error('Impossible de reconnaître cet ordinateur.');
  // The raw OS identifier never leaves this function or gets persisted.
  return sha256(`relio-machine-v1:${platform}:${identifier.toLowerCase()}`);
}

function activationToken(link) {
  if (typeof link !== 'string' || link.length > 4096) throw new Error('Lien d’activation invalide.');
  let url; try { url = new URL(link.trim()); } catch { throw new Error('Lien d’activation invalide.'); }
  if (!SERVICE_ORIGINS.includes(url.origin) || url.pathname !== '/activate' || url.search || url.username || url.password ||
      !/^#[A-Za-z0-9_-]{43}$/.test(url.hash)) throw new Error('Utilisez le lien d’activation reçu.');
  return url.hash.slice(1);
}

function transcript(purpose, challenge, identity, machineName, fingerprint) {
  return JSON.stringify(['relio-device-v1', purpose, challenge.challengeId, challenge.challenge,
    identity, purpose === 'activate' ? machineName : '', fingerprint]);
}

class DesktopLicense {
  constructor({ directory, safeStorage, platform = process.platform, fingerprint = () => machineFingerprint(platform),
    machineName = os.hostname(), serviceOrigin = SERVICE, fetch: fetcher = globalThis.fetch, now = () => Date.now() }) {
    if (!SERVICE_ORIGINS.includes(serviceOrigin)) throw new Error('Service de licences refusé.');
    this.serviceOrigin = serviceOrigin;
    this.file = path.join(directory, 'relio-device-license.v1');
    this.safeStorage = safeStorage; this.platform = platform; this.getFingerprint = fingerprint;
    this.machineName = String(machineName).replace(/[\x00-\x1f\x7f]/g, '').slice(0, 100) || 'Mon ordinateur';
    this.fetch = fetcher; this.now = now; this.queue = Promise.resolve(); this.publisher = null;
  }
  exclusive(callback) {
    const task = this.queue.then(callback); this.queue = task.catch(() => {}); return task;
  }
  requireStorage() {
    if (!['darwin', 'win32'].includes(this.platform)) throw new Error('Activation disponible uniquement sur macOS et Windows.');
    if (!this.safeStorage.isEncryptionAvailable()) throw new Error('Le stockage sécurisé du système est indisponible. Aucun secret ne sera enregistré en clair.');
  }
  async load() {
    this.requireStorage();
    let handle;
    try {
      handle = await fs.open(this.file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
      const stat = await handle.stat();
      if (!stat.isFile() || stat.size > 64 * 1024) throw new Error('Licence locale invalide.');
      const envelope = JSON.parse(await handle.readFile('utf8'));
      if (envelope.version !== 1 || typeof envelope.encrypted !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(envelope.encrypted))
        throw new Error('Licence locale invalide.');
      const record = JSON.parse(this.safeStorage.decryptString(Buffer.from(envelope.encrypted, 'base64')));
      if (record.version !== 1 || !/^[a-f0-9]{64}$/.test(record.fingerprint) || typeof record.privateKey !== 'string' ||
          typeof record.publicKey !== 'string' || typeof record.machineName !== 'string' ||
          !['pending', 'active', 'deactivated'].includes(record.state)) throw new Error('Licence locale invalide.');
      const key = createPrivateKey({ key: Buffer.from(record.privateKey, 'base64'), type: 'pkcs8', format: 'der' });
      if (key.asymmetricKeyType !== 'ed25519' || createPublicKey(key).export({ type: 'spki', format: 'der' }).toString('base64') !== record.publicKey)
        throw new Error('Licence locale invalide.');
      if (record.state === 'active' && (typeof record.deviceId !== 'string' || typeof record.licenseId !== 'string'))
        throw new Error('Licence locale invalide.');
      return record;
    } catch (error) {
      if (error.code === 'ENOENT') return null;
      throw new Error('Impossible de lire la licence sécurisée de cet ordinateur.');
    } finally { await handle?.close(); }
  }
  async save(record) {
    this.requireStorage();
    const envelope = JSON.stringify({ version: 1, encrypted: this.safeStorage.encryptString(JSON.stringify(record)).toString('base64') });
    await fs.mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 });
    const temporary = `${this.file}.${randomUUID()}.tmp`;
    let handle;
    try {
      handle = await fs.open(temporary, 'wx', 0o600);
      await handle.writeFile(envelope, 'utf8'); await handle.sync(); await handle.close(); handle = null;
      await fs.rename(temporary, this.file);
    } finally { await handle?.close(); await fs.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
  }
  async currentFingerprint(record) {
    const fingerprint = await this.getFingerprint();
    if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw new Error('Empreinte de cet ordinateur indisponible.');
    if (record && record.fingerprint !== fingerprint) throw new Error('Cette licence appartient à un autre ordinateur. Activez une licence pour cet appareil.');
    return fingerprint;
  }
  status() { return this.exclusive(async () => {
    const base = { supported: ['darwin', 'win32'].includes(this.platform), secureStorage: false, activated: false, machineName: this.machineName };
    try {
      this.requireStorage(); base.secureStorage = true;
      const record = await this.load();
      if (!record || record.state !== 'active') return base;
      await this.currentFingerprint(record);
      return { ...base, activated: true, deviceId: record.deviceId, licenseId: record.licenseId };
    } catch (error) { return { ...base, error: error.message }; }
  }); }
  async request(endpoint, body) {
    let response;
    try { response = await this.fetch(`${this.serviceOrigin}/api/device/${endpoint}`, { method: 'POST', redirect: 'error',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) }); }
    catch { throw new Error('Le service d’activation est inaccessible. Réessayez lorsque la connexion est rétablie.'); }
    if (!response.ok) {
      if ([401, 403, 404, 409, 410].includes(response.status)) this.publisher = null;
      const messages = { 401: 'Activation ou licence refusée.', 403: 'Cette licence ne permet pas cette opération.',
        404: 'Licence ou appareil introuvable.', 409: 'Cette activation a déjà été utilisée ou la limite d’appareils est atteinte.',
        410: 'Ce lien d’activation a expiré.', 429: 'Trop de tentatives. Réessayez plus tard.' };
      throw new Error(messages[response.status] || 'Le service de licences est temporairement indisponible.');
    }
    try { return await response.json(); } catch { throw new Error('Réponse du service de licences invalide.'); }
  }
  async challenge(purpose, record, fingerprint, token) {
    const challenge = await this.request('challenge', { purpose, publicKey: record.publicKey });
    if (typeof challenge.challengeId !== 'string' || challenge.challengeId.length > 256 || typeof challenge.challenge !== 'string' ||
        challenge.challenge.length > 2048 || !Number.isSafeInteger(challenge.expiresAt) || challenge.expiresAt <= this.now())
      throw new Error('Défi d’activation invalide.');
    const identity = purpose === 'activate' ? sha256(token) : record.deviceId;
    const key = createPrivateKey({ key: Buffer.from(record.privateKey, 'base64'), type: 'pkcs8', format: 'der' });
    const signature = sign(null, Buffer.from(transcript(purpose, challenge, identity, record.machineName, fingerprint)), key).toString('base64');
    return { challengeId: challenge.challengeId, signature };
  }
  acceptPublisher(value, record) {
    if (!value || typeof value.publisherToken !== 'string' || value.publisherToken.length < 32 || value.publisherToken.length > 4096 ||
        !Number.isSafeInteger(value.expiresAt) || value.expiresAt <= this.now() || value.expiresAt > this.now() + 11 * 60_000 ||
        typeof value.deviceId !== 'string' || typeof value.licenseId !== 'string' ||
        (record?.deviceId && record.deviceId !== value.deviceId)) throw new Error('Réponse de licence invalide.');
    this.publisher = { publisherToken: value.publisherToken, expiresAt: value.expiresAt, deviceId: value.deviceId, licenseId: value.licenseId };
  }
  activate(link) { return this.exclusive(async () => {
    const token = activationToken(link); this.requireStorage();
    let record = await this.load();
    if (record?.state === 'active') throw new Error('Cet ordinateur est déjà activé. Désactivez-le avant d’utiliser un autre lien.');
    const fingerprint = await this.currentFingerprint(record);
    if (!record || record.state === 'deactivated') {
      const { privateKey, publicKey } = generateKeyPairSync('ed25519');
      record = { version: 1, state: 'pending', machineName: this.machineName, fingerprint,
        privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),
        publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64') };
      await this.save(record);
    }
    const proof = await this.challenge('activate', record, fingerprint, token);
    const result = await this.request('activate', { token, publicKey: record.publicKey, machineName: record.machineName, fingerprint, ...proof });
    this.acceptPublisher(result, record);
    record.state = 'active'; record.deviceId = result.deviceId; record.licenseId = result.licenseId;
    try { await this.save(record); } catch { this.publisher = null; throw new Error('Activation reçue mais sauvegarde locale impossible. Contactez l’administrateur pour réinitialiser cet appareil.'); }
    return { supported: true, secureStorage: true, activated: true, machineName: this.machineName, deviceId: record.deviceId, licenseId: record.licenseId };
  }); }
  publisherToken() { return this.exclusive(async () => {
    const record = await this.load();
    if (!record || record.state !== 'active') throw new Error('Activez une licence pour partager depuis cet ordinateur.');
    const fingerprint = await this.currentFingerprint(record);
    if (this.publisher && this.publisher.deviceId === record.deviceId && this.publisher.expiresAt > this.now() + 60_000) return { ...this.publisher };
    const proof = await this.challenge('publish', record, fingerprint);
    const result = await this.request('publish', { deviceId: record.deviceId, fingerprint, ...proof });
    this.acceptPublisher(result, record); return { ...this.publisher };
  }); }
  deactivate() { return this.exclusive(async () => {
    const record = await this.load();
    if (!record || record.state !== 'active') return { deactivated: true };
    const fingerprint = await this.currentFingerprint(record);
    const proof = await this.challenge('deactivate', record, fingerprint);
    const result = await this.request('deactivate', { deviceId: record.deviceId, fingerprint, ...proof });
    if (result?.deactivated !== true) throw new Error('Désactivation non confirmée.');
    this.publisher = null;
    // Persist a tombstone before deleting so a failed deletion cannot reuse an active record.
    record.state = 'deactivated'; await this.save(record); await fs.unlink(this.file);
    return { deactivated: true };
  }); }
}

module.exports = { DesktopLicense, activationToken, machineFingerprint, transcript };
