'use strict';

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { readConfig, activeConnection, configDir } = require('./config');

const LICENSE_PATH = path.join(configDir(), 'license.lic');
const LOCAL_JWKS_CACHE_PATH = path.join(configDir(), 'license-jwks.json');

const EXPECTED_AUDIENCE = 'cielara-code';
const EXPECTED_PRODUCT = 'cielara-code';
const CLOCK_SKEW_SECS = 300;

// keygen entitlement code that grants Cielara Code on an enterprise instance's
// Cielara Enterprise license. The offline license file must carry it.
const KEYGEN_CIELARA_CODE_ENTITLEMENT = 'CIELARA_CODE';
const KEYGEN_LICENSE_FILE_BEGIN = '-----BEGIN LICENSE FILE-----';
const KEYGEN_LICENSE_FILE_END = '-----END LICENSE FILE-----';

// Set to false by the release pipeline for production npm artifacts
// (.github/workflows/release.yml rewrites this exact line). When false, the
// dev signing key, the cielara-dev.net / localhost issuer trust, and the
// runtime key/issuer env overrides below are all disabled. This is what stops
// a *production* CLI from being pointed at the dev portal (or a self-hosted
// signer) to mint a free license. The gate must be build-time, not a runtime
// env var: a customer controls their own env, but not the shipped source.
const DEV_BUILD = false;

// Entries starting with "*." match any subdomain of that domain.
//
// Enterprise *.cielara.ai instances are intentionally ABSENT: they no longer
// sign Cielara Code licenses with a per-tenant KMS key (the key lives in the
// customer's own cloud account, so we could never revoke it). Instead they
// issue keygen OFFLINE license files signed by xfabric's keygen account key,
// verified via verifyKeygenLicenseFile against KEYGEN_PUBLIC_KEYS_HEX below.
const TRUSTED_ISSUERS = [
  'https://cielaracode.com',
];

if (DEV_BUILD) {
  // Dev/staging portal. Stripped from prod builds (DEV_BUILD=false) so a
  // released CLI won't accept licenses issued by portal.cielara-dev.net.
  TRUSTED_ISSUERS.push('*.cielara-dev.net');
}

function extractHost(url) {
  const m = url.match(/^https?:\/\/([^/?:#]+)/);
  return m ? m[1] : null;
}

function isTrustedIssuer(iss) {
  const host = extractHost(iss);
  if (!host) return false;
  for (const pattern of TRUSTED_ISSUERS) {
    if (pattern.startsWith('*.')) {
      const suffix = pattern.slice(2);
      if (host === suffix || host.endsWith(`.${suffix}`)) return true;
    } else {
      const trustedHost = extractHost(pattern);
      if (trustedHost && host === trustedHost) return true;
    }
  }
  if (DEV_BUILD) {
    // Local dev signers. Disabled in prod builds so a customer can't point a
    // released CLI at their own localhost signer (with a matching env key) to
    // self-mint a license.
    if (iss.startsWith('http://localhost:') || iss.startsWith('http://127.0.0.1:')) {
      return true;
    }
    const custom = process.env.CIELARA_ISSUER;
    if (custom && custom === iss) return true;
  }
  return false;
}

function isLoopbackIssuer(iss) {
  if (!DEV_BUILD || !iss) return false;
  try {
    const url = new URL(iss);
    return url.protocol === 'http:' && (
      url.hostname === 'localhost' ||
      url.hostname === '127.0.0.1' ||
      url.hostname === '::1' ||
      url.hostname === '[::1]'
    );
  } catch {
    return false;
  }
}

// Issuers whose JWT signing keys come from the fetched-and-cached JWKS rather
// than the bundled PORTAL_PUBLIC_KEYS: loopback dev signers only (DEV_BUILD-
// gated inside isLoopbackIssuer). cielaracode.com uses bundled keys; enterprise
// *.cielara.ai no longer uses JWTs at all (keygen offline files instead), so it
// is intentionally absent here.
function isCacheableIssuer(iss) {
  return isLoopbackIssuer(iss);
}

// Portal Ed25519 public keys, keyed by JWT `kid`.
//
// The `v1` entry is the production signing key, injected at release time from
// the PORTAL_PUBLIC_KEY_V1 GitHub secret (the release pipeline rewrites this
// exact array literal — keep it as the regex target, do not add other kids
// inside it).
const PORTAL_PUBLIC_KEYS = [
  { kid: 'v1', publicKeyBase64url: 'J3bjwEO1iQqVTbcRCRYlwnRKHy7FDbQf2E3pH1UhYpE' },
];

// `dev-v1` is the portal.cielara-dev.net signing key. It is NOT secret (it
// signs dev/free-trial licenses only) so it is bundled directly, letting
// dev-portal logins work in dev builds. Pushed *after* the array literal above
// so the release-time regex
// injection of the prod key doesn't drop it; gated on DEV_BUILD so prod
// artifacts (DEV_BUILD=false) won't accept dev-signed licenses.
if (DEV_BUILD) {
  PORTAL_PUBLIC_KEYS.push({
    kid: 'dev-v1',
    publicKeyBase64url: '0zLIb9LDucQOGvfclb5sYFM7vcKaiTV2tNs2kM_4Ekw',
  });
}

// Env override: PORTAL_PUBLIC_KEY_V1 (raw 32-byte Ed25519 key, base64url —
// same as JWKS "x") *replaces* any bundled key with the same kid rather than
// shadowing it, so local/dev runs can point at a custom signer. DEV_BUILD-gated
// so a released CLI ignores this env var — otherwise a customer could set it to
// their own key and forge a license. The prod v1 key is baked at build time by
// the release regex, not via this env var.
// Generate from PEM: openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64url
if (DEV_BUILD && process.env.PORTAL_PUBLIC_KEY_V1) {
  const entry = { kid: 'v1', publicKeyBase64url: process.env.PORTAL_PUBLIC_KEY_V1 };
  const i = PORTAL_PUBLIC_KEYS.findIndex((k) => k.kid === 'v1');
  if (i >= 0) PORTAL_PUBLIC_KEYS[i] = entry;
  else PORTAL_PUBLIC_KEYS.push(entry);
}

// keygen ACCOUNT Ed25519 public key(s), HEX-encoded (64 chars → 32 bytes). This
// verifies enterprise (*.cielara.ai) offline license files. The key belongs to
// xfabric's keygen account, NOT the customer — so a customer who owns their
// cloud KMS keys still cannot forge a Cielara Code license, and xfabric retains
// the kill switch via the Enterprise license / CIELARA_CODE entitlement.
//
// The production key is injected at release time from the KEYGEN_PUBLIC_KEYS_HEX
// GitHub secret (the pipeline rewrites this exact array literal — keep it as the
// regex target). Rotation = ship a build with both old and new keys for the
// overlap window. Empty in a prod build means enterprise verification is
// unavailable and fails closed.
const KEYGEN_PUBLIC_KEYS_HEX = [
  'a211da2e02cc5c9d3b3dfe8ef7d49763db2c61c6a29b59fa46c4e8a26baaceb8',
];

// Env override (DEV_BUILD-gated, same rationale as PORTAL_PUBLIC_KEY_V1): point
// a dev build at a dev keygen account's public key. Ignored in prod builds.
// Derive from a keygen-go account: it is the account's `verify-key` (hex).
if (DEV_BUILD && process.env.KEYGEN_PUBLIC_KEY_HEX) {
  KEYGEN_PUBLIC_KEYS_HEX.unshift(process.env.KEYGEN_PUBLIC_KEY_HEX);
}

class LicenseError extends Error {
  constructor(message, code = 'INVALID') {
    super(message);
    this.name = 'LicenseError';
    this.code = code;
  }
}

function base64urlDecode(str) {
  return Buffer.from(str, 'base64url');
}

function readLocalJWKSCache() {
  // The JWKS cache now only serves loopback dev signers, so gate the read on
  // DEV_BUILD: a released CLI never reads cached JWTs keys at all. The issuer
  // gate in localCachedKeyFor (isCacheableIssuer → isLoopbackIssuer) bounds it
  // further. Enterprise verification does not use this path — see
  // verifyKeygenLicenseFile.
  if (!DEV_BUILD) return [];
  try {
    const parsed = JSON.parse(fs.readFileSync(LOCAL_JWKS_CACHE_PATH, 'utf8'));
    return Array.isArray(parsed.keys) ? parsed.keys : [];
  } catch {
    return [];
  }
}

function localCachedKeyFor(kid, issuer) {
  if (!isCacheableIssuer(issuer)) return null;
  return readLocalJWKSCache().find((k) =>
    k &&
    k.kid === kid &&
    k.issuer === issuer &&
    typeof k.publicKeyBase64url === 'string' &&
    k.publicKeyBase64url
  ) || null;
}

function verifyEd25519SignatureWithKey(keyEntry, headerB64, payloadB64, sigB64) {
  const pubKeyRaw = base64urlDecode(keyEntry.publicKeyBase64url);
  if (pubKeyRaw.length !== 32) {
    throw new LicenseError('Portal public key must be 32 bytes.');
  }

  const publicKey = crypto.createPublicKey({
    key: Buffer.concat([
      // Ed25519 DER prefix (RFC 8410): 30 2a 30 05 06 03 2b 65 70 03 21 00
      Buffer.from('302a300506032b6570032100', 'hex'),
      pubKeyRaw,
    ]),
    format: 'der',
    type: 'spki',
  });

  const signedInput = Buffer.from(`${headerB64}.${payloadB64}`);
  const signature = base64urlDecode(sigB64);

  return crypto.verify(null, signedInput, publicKey, signature);
}

function verifyEd25519Signature(headerB64, payloadB64, sigB64, header, payload) {
  const kid = header.kid;
  if (!kid) {
    throw new LicenseError('JWT header missing kid claim.');
  }

  const issuer = payload && payload.iss;
  const candidates = [];
  const cachedLocal = localCachedKeyFor(kid, issuer);
  if (cachedLocal) candidates.push(cachedLocal);

  const bundled = PORTAL_PUBLIC_KEYS.find((k) => k.kid === kid);
  if (bundled) candidates.push(bundled);

  if (candidates.length === 0) {
    if (PORTAL_PUBLIC_KEYS.length === 0) {
      return;
    }
    throw new LicenseError(`Unknown JWT key ID: ${kid}`);
  }

  let lastError = null;
  let sawInvalidSignature = false;
  for (const candidate of candidates) {
    try {
      if (verifyEd25519SignatureWithKey(candidate, headerB64, payloadB64, sigB64)) {
        return;
      }
      sawInvalidSignature = true;
    } catch (error) {
      lastError = error;
    }
  }

  if (lastError && !sawInvalidSignature) throw lastError;
  throw new LicenseError(
    'JWT signature verification failed. License may be forged.',
  );
}

function decodeJWTPayload(jwt) {
  const parts = jwt.split('.');
  if (parts.length !== 3) return null;
  try {
    return {
      header: JSON.parse(base64urlDecode(parts[0]).toString('utf8')),
      payload: JSON.parse(base64urlDecode(parts[1]).toString('utf8')),
    };
  } catch {
    return null;
  }
}

async function cacheIssuerJWKSForLicense(licenseText) {
  if (typeof fetch !== 'function') return;
  const decoded = decodeJWTPayload(licenseText);
  const issuer = decoded?.payload?.iss;
  // Only fetch+cache for issuers whose keys aren't bundled: enterprise
  // *.cielara.ai (prod/dev) and loopback dev signers. cielaracode.com and
  // untrusted hosts are skipped — their keys are bundled or rejected.
  if (!isCacheableIssuer(issuer)) return;

  const jwksUrl = `${issuer.replace(/\/$/, '')}/.well-known/jwks.json`;
  const response = await fetch(jwksUrl, {
    method: 'GET',
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) return;

  const jwks = await response.json().catch(() => null);
  const fetchedKeys = Array.isArray(jwks?.keys) ? jwks.keys : [];
  const nextEntries = fetchedKeys
    .filter((jwk) =>
      jwk &&
      jwk.kty === 'OKP' &&
      jwk.crv === 'Ed25519' &&
      typeof jwk.kid === 'string' &&
      typeof jwk.x === 'string' &&
      jwk.kid &&
      jwk.x
    )
    .map((jwk) => ({
      issuer,
      kid: jwk.kid,
      publicKeyBase64url: jwk.x,
      fetchedAt: new Date().toISOString(),
    }));
  if (nextEntries.length === 0) return;

  const existing = readLocalJWKSCache();
  const kept = existing.filter((entry) =>
    !nextEntries.some((next) => next.issuer === entry.issuer && next.kid === entry.kid)
  );
  fs.mkdirSync(path.dirname(LOCAL_JWKS_CACHE_PATH), { recursive: true, mode: 0o700 });
  const tmpPath = LOCAL_JWKS_CACHE_PATH + '.tmp';
  fs.writeFileSync(
    tmpPath,
    `${JSON.stringify({ version: 1, keys: [...kept, ...nextEntries] }, null, 2)}\n`,
    { mode: 0o600 },
  );
  fs.renameSync(tmpPath, LOCAL_JWKS_CACHE_PATH);
}

function verifyPortalJWT(jwt) {
  const parts = jwt.split('.');
  if (parts.length !== 3) {
    throw new LicenseError('Invalid JWT format.');
  }

  let header;
  try {
    header = JSON.parse(base64urlDecode(parts[0]).toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode JWT header.');
  }

  // alg: REQUIRED, must be EdDSA
  if (!header.alg) {
    throw new LicenseError('JWT header missing alg claim.');
  }
  if (header.alg !== 'EdDSA') {
    throw new LicenseError(
      `Unsupported JWT algorithm: expected EdDSA, got ${header.alg}`,
    );
  }

  let payload;
  try {
    payload = JSON.parse(base64urlDecode(parts[1]).toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode JWT payload.');
  }

  // Ed25519 signature verification. For DEV_BUILD loopback issuers, the
  // verifier may use a cached JWKS fetched from that local issuer; the issuer
  // claim is decoded before validation only to select that key source.
  verifyEd25519Signature(parts[0], parts[1], parts[2], header, payload);

  const now = Math.floor(Date.now() / 1000);

  // exp: REQUIRED
  if (!payload.exp) {
    throw new LicenseError('JWT missing required exp claim.', 'INVALID');
  }
  if (payload.exp < now) {
    throw new LicenseError('Portal license expired.', 'EXPIRED');
  }

  // aud: REQUIRED
  if (!payload.aud) {
    throw new LicenseError('JWT missing required aud claim.');
  }
  if (payload.aud !== EXPECTED_AUDIENCE) {
    throw new LicenseError(
      `JWT audience mismatch: expected ${EXPECTED_AUDIENCE}, got ${payload.aud}`,
    );
  }

  // iss: REQUIRED, must be in trusted issuer allowlist
  if (!payload.iss) {
    throw new LicenseError('JWT missing required iss claim.');
  }
  if (!isTrustedIssuer(payload.iss)) {
    throw new LicenseError(
      `JWT issuer not trusted: ${payload.iss}`,
    );
  }

  // product: REQUIRED
  if (!payload.product) {
    throw new LicenseError('JWT missing required product claim.');
  }
  if (payload.product !== EXPECTED_PRODUCT) {
    throw new LicenseError(
      `JWT product mismatch: expected ${EXPECTED_PRODUCT}, got ${payload.product}`,
    );
  }

  // nbf: optional, reject if in the future beyond clock skew
  if (payload.nbf && payload.nbf > now + CLOCK_SKEW_SECS) {
    throw new LicenseError('JWT not yet valid (nbf is in the future).');
  }

  // Blocking statuses
  const status = (payload.status || '').toLowerCase();
  if (status === 'trial_expired') {
    throw new LicenseError(
      'Your 60-day trial has ended. Upgrade your plan at https://cielaracode.com/plan',
      'EXPIRED',
    );
  }
  if (status === 'suspended') {
    throw new LicenseError(
      'Your license has been suspended. Contact support@cielara.ai',
      'REVOKED',
    );
  }
  if (status === 'revoked') {
    throw new LicenseError(
      'Your license has been revoked. Contact support@cielara.ai',
      'REVOKED',
    );
  }
  if (status === 'past_due') {
    throw new LicenseError(
      'Your payment is past due. Update billing at https://cielaracode.com/billing',
      'PAST_DUE',
    );
  }

  return {
    valid: true,
    licenseId: payload.sub || payload.user_id,
    expiry: payload.exp ? new Date(payload.exp * 1000).toISOString() : null,
    tier: payload.tier,
    status: payload.status,
  };
}

function isJWT(text) {
  return !text.includes('-----BEGIN') && text.split('.').length === 3;
}

function isKeygenLicenseFile(text) {
  return typeof text === 'string' && text.includes(KEYGEN_LICENSE_FILE_BEGIN);
}

function keygenPublicKeys() {
  return KEYGEN_PUBLIC_KEYS_HEX.filter((k) => typeof k === 'string' && k);
}

function verifyKeygenEd25519WithKey(pubKeyHex, msg, signature) {
  const pubKeyRaw = Buffer.from(pubKeyHex, 'hex');
  if (pubKeyRaw.length !== 32) {
    throw new LicenseError('Keygen public key must be 32 bytes (64 hex chars).');
  }
  const publicKey = crypto.createPublicKey({
    key: Buffer.concat([
      // Ed25519 DER prefix (RFC 8410), same as the portal path.
      Buffer.from('302a300506032b6570032100', 'hex'),
      pubKeyRaw,
    ]),
    format: 'der',
    type: 'spki',
  });
  return crypto.verify(null, msg, publicKey, signature);
}

// verifyKeygenLicenseFile verifies a keygen OFFLINE license file (the artifact
// an enterprise *.cielara.ai instance issues for Cielara Code). Mirrors keygen's
// own verification: the cert is a PEM-wrapped base64 of {enc, sig, alg}; the
// signature is Ed25519 over the bytes "license/<enc>" against the keygen account
// key; the dataset is base64(JSON:API) carrying the license + its entitlements.
function verifyKeygenLicenseFile(certText) {
  const payload = certText
    .trim()
    .replace(KEYGEN_LICENSE_FILE_BEGIN, '')
    .replace(KEYGEN_LICENSE_FILE_END, '')
    .replace(/\s+/g, '');

  let cert;
  try {
    cert = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode Keygen license file.', 'PARSE_ERROR');
  }
  const { enc, sig, alg } = cert || {};
  if (!enc || !sig) {
    throw new LicenseError('Keygen license file missing enc/sig.', 'PARSE_ERROR');
  }
  // We only issue base64+ed25519 (unencrypted so the connector can verify
  // offline with only the public key; ed25519 matches the account key). An
  // aes-256-gcm file would need a shared decryption secret we deliberately
  // don't ship.
  if (alg && alg !== 'base64+ed25519') {
    throw new LicenseError(`Unsupported Keygen license file algorithm: ${alg}`);
  }

  // Signature: ed25519 over "license/" + enc; sig is standard base64.
  const keys = keygenPublicKeys();
  if (keys.length === 0) {
    // Fail closed: an unverifiable enterprise license must never be trusted,
    // in prod OR dev. Dev builds can supply KEYGEN_PUBLIC_KEY_HEX.
    throw new LicenseError(
      DEV_BUILD
        ? 'No Keygen public key configured. Set KEYGEN_PUBLIC_KEY_HEX for dev.'
        : 'Keygen verification key unavailable (build misconfigured).',
    );
  }
  const msg = Buffer.from(`license/${enc}`);
  const signature = Buffer.from(sig, 'base64');
  let verified = false;
  for (const k of keys) {
    try {
      if (verifyKeygenEd25519WithKey(k, msg, signature)) {
        verified = true;
        break;
      }
    } catch {
      // try the next key (e.g. during rotation overlap)
    }
  }
  if (!verified) {
    throw new LicenseError('Keygen license file signature invalid. License may be forged.');
  }

  let doc;
  try {
    doc = JSON.parse(Buffer.from(enc, 'base64').toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode Keygen license dataset.', 'PARSE_ERROR');
  }

  const lic = doc.data || {};
  const attrs = lic.attributes || {};
  const meta = doc.meta || {};
  const included = Array.isArray(doc.included) ? doc.included : [];

  const now = Date.now();
  const skewMs = CLOCK_SKEW_SECS * 1000;

  // License-file TTL window — the kill-switch bound. ttl===0/absent means the
  // file itself doesn't expire (we always issue a TTL, so this normally fires).
  if (meta.ttl && meta.expiry && now > new Date(meta.expiry).getTime() + skewMs) {
    throw new LicenseError('Enterprise license expired; refresh required.', 'EXPIRED');
  }

  // Inner license status (keygen statuses are uppercase).
  const status = (attrs.status || '').toUpperCase();
  if (status === 'EXPIRED') {
    throw new LicenseError('Your Cielara Enterprise license has expired. Contact support@cielara.ai', 'EXPIRED');
  }
  if (status === 'SUSPENDED') {
    throw new LicenseError('Your Cielara Enterprise license is suspended. Contact support@cielara.ai', 'REVOKED');
  }
  if (status === 'BANNED') {
    throw new LicenseError('Your Cielara Enterprise license has been revoked. Contact support@cielara.ai', 'REVOKED');
  }

  // Inner license hard expiry, independent of the file TTL.
  if (attrs.expiry && now > new Date(attrs.expiry).getTime() + skewMs) {
    throw new LicenseError('Your Cielara Enterprise license has expired. Contact support@cielara.ai', 'EXPIRED');
  }

  // Must carry the CIELARA_CODE entitlement, embedded via checkout include.
  const entitled = included.some((e) =>
    e &&
    e.type === 'entitlements' &&
    ((e.attributes && e.attributes.code) || '').toUpperCase() === KEYGEN_CIELARA_CODE_ENTITLEMENT
  );
  if (!entitled) {
    throw new LicenseError('This Cielara Enterprise license does not include Cielara Code.', 'INVALID');
  }

  // Refresh at the file-TTL expiry (short), so revocation is re-checked within
  // one window. Fall back to the license expiry if no TTL was set.
  const refreshExpiry = meta.expiry
    ? new Date(meta.expiry).toISOString()
    : (attrs.expiry ? new Date(attrs.expiry).toISOString() : null);

  return {
    valid: true,
    licenseId: lic.id || (attrs.metadata && attrs.metadata.client_id) || null,
    expiry: refreshExpiry,
    tier: 'enterprise',
    status: 'active',
  };
}

// decodeKeygenLicenseFile parses a keygen offline license file for DISPLAY
// (e.g. `cielara license`) WITHOUT requiring the signing key — it does not
// verify the signature, only decodes the dataset so the CLI can show tier /
// status / expiry / entitlements. Callers that need trust must use
// verifyKeygenLicenseFile. Throws LicenseError(PARSE_ERROR) on malformed input.
function decodeKeygenLicenseFile(certText) {
  const payload = certText
    .trim()
    .replace(KEYGEN_LICENSE_FILE_BEGIN, '')
    .replace(KEYGEN_LICENSE_FILE_END, '')
    .replace(/\s+/g, '');

  let cert;
  try {
    cert = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode Keygen license file.', 'PARSE_ERROR');
  }
  const { enc } = cert || {};
  if (!enc) {
    throw new LicenseError('Keygen license file missing enc.', 'PARSE_ERROR');
  }

  let doc;
  try {
    doc = JSON.parse(Buffer.from(enc, 'base64').toString('utf8'));
  } catch {
    throw new LicenseError('Cannot decode Keygen license dataset.', 'PARSE_ERROR');
  }

  const lic = doc.data || {};
  const attrs = lic.attributes || {};
  const meta = doc.meta || {};
  const included = Array.isArray(doc.included) ? doc.included : [];
  const entitlements = included
    .filter((e) => e && e.type === 'entitlements')
    .map((e) => ((e.attributes && e.attributes.code) || '').trim())
    .filter(Boolean);

  return { cert, doc, lic, attrs, meta, included, entitlements };
}

// verifyLicense routes by artifact shape: a keygen license file is PEM-wrapped
// (enterprise *.cielara.ai), a portal license is a JWT (cielaracode.com).
function verifyLicense(text) {
  if (isKeygenLicenseFile(text)) return verifyKeygenLicenseFile(text);
  if (isJWT(text)) return verifyPortalJWT(text);
  throw new LicenseError('Unsupported license format.', 'FORMAT_ERROR');
}

async function fetchLicenseFromBackend() {
  const { retrieveCredentials } = require('./credential-store');

  let accessToken, licenseUrl, portalUrl;
  try {
    const { portalEnsureFresh } = require('./portal-auth');
    const fresh = await portalEnsureFresh('default');
    accessToken = fresh.credentials.accessToken;
    portalUrl = fresh.credentials.portalUrl;
    licenseUrl = `${portalUrl.replace(/\/$/, '')}/v1/products/cielara-code/license`;
  } catch {
    // No portal credentials, try legacy
  }

  if (!accessToken) {
    try {
      const portalCreds = await retrieveCredentials('default');
      if (portalCreds?.accessToken && portalCreds?.portalUrl) {
        accessToken = portalCreds.accessToken;
        portalUrl = portalCreds.portalUrl;
        licenseUrl = `${portalUrl.replace(/\/$/, '')}/v1/products/cielara-code/license`;
      }
    } catch {
      // No portal credentials
    }
  }

  if (!accessToken) {
    const config = readConfig();
    const active = activeConnection(config);
    if (!active || !active.connection?.baseUrl) {
      throw new LicenseError(
        'No active connection settings found. Run `cielara login` to connect to your server.',
        'NO_CONNECTION',
      );
    }
    accessToken = active.connection.accessToken;
    const baseUrl = active.connection.backendUrl || active.connection.baseUrl;
    licenseUrl = `${baseUrl}/api/license/checkout`;
  }

  if (!accessToken) {
    throw new LicenseError(
      'No access token found. Please log in: `cielara login`',
      'NO_TOKEN',
    );
  }

  if (typeof fetch !== 'function') {
    throw new LicenseError(
      'This Node.js runtime does not provide fetch. Cielara requires Node.js 18 or newer.',
      'RUNTIME',
    );
  }

  process.stderr.write('Synchronizing license...\n');

  const response = await fetch(licenseUrl, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      Accept: 'application/json, text/plain',
    },
    signal: AbortSignal.timeout(15_000),
  });

  if (!response.ok) {
    const errorText = await response.text().catch(() => '');
    throw new LicenseError(
      `Server returned error ${response.status}: ${errorText || response.statusText}`,
      'SERVER_ERROR',
    );
  }

  const contentType = response.headers.get('content-type') || '';
  const body = await response.text();
  let licenseText;

  if (contentType.includes('application/json')) {
    // Portal returns JSON: { "license_file": "<jwt>", "expires_at": "..." }
    try {
      const parsed = JSON.parse(body);
      licenseText = (parsed.license_file || '').trim();
    } catch {
      throw new LicenseError(
        'Could not parse license response as JSON.',
        'PARSE_ERROR',
      );
    }
  } else {
    // Legacy: raw JWT text
    licenseText = body.trim();
  }

  if (!licenseText || (!isJWT(licenseText) && !isKeygenLicenseFile(licenseText))) {
    throw new LicenseError(
      'Expected a JWT or keygen license from the server but received an unsupported format.',
      'FORMAT_ERROR',
    );
  }

  if (isJWT(licenseText)) {
    try {
      // Loopback-dev JWKS prefetch only; enterprise keygen files need no cache.
      await cacheIssuerJWKSForLicense(licenseText);
    } catch {
      // Best-effort only. If the key cannot be cached, the verification step
      // below will fail with the normal signature/key error.
    }
  }

  fs.mkdirSync(path.dirname(LICENSE_PATH), { recursive: true, mode: 0o700 });
  // Atomic write: temp file then rename
  const tmpPath = LICENSE_PATH + '.tmp';
  fs.writeFileSync(tmpPath, licenseText, { mode: 0o600 });
  fs.renameSync(tmpPath, LICENSE_PATH);

  process.stderr.write('License synchronized and saved successfully.\n');
  return licenseText;
}

async function assertLicense() {
  if (fs.existsSync(LICENSE_PATH)) {
    try {
      const content = fs.readFileSync(LICENSE_PATH, 'utf8').trim();
      if (isKeygenLicenseFile(content) || isJWT(content)) {
        return verifyLicense(content);
      }
      process.stderr.write(
        'Legacy license format detected. Fetching new license...\n',
      );
    } catch (error) {
      // The cached license failed verification — expired, stale issuer (e.g.
      // a license from a different portal), or bad signature. Don't hard-fail:
      // a fresh license from the backend may well be valid. Fall through to
      // re-fetch; only the fetch+verify result below is authoritative.
      if (!(error instanceof LicenseError)) throw error;
      process.stderr.write(
        `Cached license invalid (${error.code || 'INVALID'}); refreshing from server...\n`,
      );
    }
  }

  const licenseText = await fetchLicenseFromBackend();
  return verifyLicense(licenseText);
}

let _lastWarnTs = 0;
const WARN_INTERVAL_MS = 300_000; // 5 minutes between warnings

async function softAssertLicense() {
  try {
    return await assertLicense();
  } catch (error) {
    if (!(error instanceof LicenseError)) {
      return null;
    }
    // Auto-refresh: if expired or no file, try fetching silently
    if (error.code === 'EXPIRED' || error.code === 'NO_CONNECTION' || error.code === 'NO_TOKEN') {
      try {
        const licenseText = await fetchLicenseFromBackend();
        return verifyLicense(licenseText);
      } catch {
        // refresh failed — degrade gracefully
      }
    }
    // Not signed in is the expected fresh-install state — degrade silently.
    // The sign-in instruction is shown once at install (postinstall), so we
    // don't nag on every hook fire.
    if (error.code === 'NO_CONNECTION' || error.code === 'NO_TOKEN') {
      return null;
    }
    const now = Date.now();
    if (now - _lastWarnTs > WARN_INTERVAL_MS) {
      _lastWarnTs = now;
      process.stderr.write(`cielara: license issue (${error.code || 'INVALID'}): ${error.message}\n`);
      process.stderr.write('cielara: running in degraded mode. Run `cielara login` to fix.\n');
    }
    return null;
  }
}

module.exports = {
  assertLicense,
  softAssertLicense,
  verifyLicense,
  verifyPortalJWT,
  verifyKeygenLicenseFile,
  decodeKeygenLicenseFile,
  isJWT,
  isKeygenLicenseFile,
  isTrustedIssuer,
  fetchLicenseFromBackend,
  LicenseError,
  LICENSE_PATH,
  LOCAL_JWKS_CACHE_PATH,
  PORTAL_PUBLIC_KEYS,
  KEYGEN_PUBLIC_KEYS_HEX,
  KEYGEN_CIELARA_CODE_ENTITLEMENT,
  DEV_BUILD,
};
