'use strict';

/*
 * Builds and serves the BrightSign player package (autorun.zip) for self-update.
 *
 * THE INVARIANT THIS FILE EXISTS TO HOLD: the checksum in the manifest and the bytes on the
 * download route come from the SAME in-memory buffer, built once. Advertising a version whose
 * checksum does not match the bytes actually served is the classic OTA-loop condition — the player
 * downloads, fails verification, retries, forever — and it is the easiest mistake to make when the
 * manifest is computed from one source and the file from another (a file on disk that a deploy
 * replaced, say). Here it is impossible by construction: there is one buffer and both routes read
 * it.
 *
 * The zip is built deterministically from brightsign/, not read from a prebuilt artifact, because a
 * prebuilt autorun.zip is a CI output that is not present in a git-checkout deployment. Building it
 * means the manifest is always available and always describes files that actually exist.
 */

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const archiver = require('archiver');

// The payload, mirroring scripts/build-autorun-zip.sh. autozip.brs must be present or nothing
// unpacks the archive on the player; autorun.brs must be INSIDE it and never beside it on the
// storage root, or the player refuses to process the zip at all.
const PACKAGE_FILES = ['autozip.brs', 'autorun.brs', 'offline.html', 'screentinker.json'];

// sha256 rather than sha1 because that is the algorithm BrightScript's roMessageDigest is
// documented against — the player has to be able to verify what we advertise, and an algorithm it
// cannot compute is an unverifiable package, which this whole design exists to refuse.
let cached = null;   // { version, sha256, size, buffer }

function brightsignDir() {
  return path.join(__dirname, '..', '..', 'brightsign');
}

function readVersion() {
  try {
    return fs.readFileSync(path.join(__dirname, '..', '..', 'VERSION'), 'utf8').trim();
  } catch (e) {
    return null;
  }
}

/*
 * Rewrite the stamped version line in autorun.brs.
 *
 * Anchored on the ST_PACKAGE_VERSION marker rather than on the literal, so a hand-edited default
 * cannot cause a silent miss. If the marker is ever removed the stamp is skipped and the package
 * ships reporting "0.0.0-dev", which reads as permanently out of date — noisy, but noisy in the
 * direction of "someone look at this" rather than a silent update loop.
 */
function stampVersion(source, version) {
  return source.replace(
    /return "[^"]*"(\s*'\s*ST_PACKAGE_VERSION)/,
    `return "${version}"$1`
  );
}

/*
 * Build the archive in memory. Entries are added in a fixed order with a fixed timestamp so the
 * bytes are reproducible: a checksum that changed on every server restart would make every player
 * re-download the same package after every deploy.
 */
function buildZip() {
  return new Promise((resolve, reject) => {
    const dir = brightsignDir();
    const chunks = [];
    // STORED, no compression — not a size/speed choice. A player could not open our first
    // deflated archive: BrightSign's automated deployment copied it across and then reported it
    // invalid. The bootstrap extracts autozip.brs before any script runs, and roBrightPackage
    // supports a specific set of methods, of which "no compression" is the universally safe one.
    // A compressed package deploys perfectly and then fails to open, which reads as a broken
    // deployment rather than a broken zip.
    const archive = archiver('zip', { store: true });

    archive.on('data', (c) => chunks.push(c));
    archive.on('error', reject);
    archive.on('end', () => resolve(Buffer.concat(chunks)));

    const version = readVersion();
    for (const name of PACKAGE_FILES) {
      const p = path.join(dir, name);
      if (!fs.existsSync(p)) return reject(new Error(`package file missing: ${name}`));
      let body = fs.readFileSync(p);
      // Stamp the version into the host so the script REPORTS the version it actually is. Ship it
      // unstamped and the player applies the update, still reports the old version, and is offered
      // the same package forever — the OTA loop, arriving by the back door.
      if (name === 'autorun.brs') body = Buffer.from(stampVersion(body.toString('utf8'), version), 'utf8');
      // date fixed for reproducibility; the player never reads it.
      archive.append(body, { name, date: new Date(0) });
    }
    archive.finalize();
  });
}

/*
 * Get the package, building once and caching. Returns null when the package cannot be built (a
 * deployment without the brightsign/ directory, for instance) — callers must treat that as "no
 * manifest", which the update decision reads as "keep running", never as "wipe yourself".
 */
async function getPackage() {
  if (cached) return cached;
  const version = readVersion();
  if (!version) return null;
  try {
    const buffer = await buildZip();
    cached = {
      version,
      sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
      size: buffer.length,
      buffer
    };
    return cached;
  } catch (e) {
    return null;
  }
}

/* Test seam: drop the cache so a changed file is picked up without a restart. */
function _reset() { cached = null; }

module.exports = { getPackage, _reset, PACKAGE_FILES };
