/* OnMic licensing / sign-in.
 *
 * Gates the panel behind a WhereToPodcast (Clerk) login with a 14-day free
 * trial, then the site's existing paid tier. Flow (loopback redirect — the user
 * just logs in, nothing to type):
 *   1. Panel starts a tiny HTTP server on 127.0.0.1:<port> and a random state.
 *   2. Panel opens https://wheretopodcast.com/activate?port&state in the browser.
 *   3. User signs in with Clerk; the site 302s a signed token back to the
 *      loopback /callback. We check the state, store the token, and verify.
 *   4. /api/onmic/verify returns the live entitlement (trial | paid | expired).
 *
 * All network calls use Node's https (not window.fetch) to avoid CORS — the
 * panel's origin can't read a cross-origin response, but Node can.
 */
var OnMicAuth = (function () {
  "use strict";

  var SITE_HOST = "wheretopodcast.com";
  var SITE = "https://" + SITE_HOST;
  var PORTS = (function(){ var a=[]; for(var p=47813;p<=47832;p++) a.push(p); return a; })();
  var GRACE_MS = 72 * 60 * 60 * 1000; // allow up to 72h offline once verified

  var http, fsx, osx, cryptox, https, childx;
  try {
    http = CEP.require("http"); fsx = CEP.require("fs"); osx = CEP.require("os");
    cryptox = CEP.require("crypto"); https = CEP.require("https"); childx = CEP.require("child_process");
  } catch (e) {}

  // Stable, privacy-preserving machine fingerprint: SHA-256 of the Mac's
  // hardware UUID. Lets the server grant ONE trial per machine (so making a new
  // free account doesn't reset the trial). Returns "" if it can't be read, in
  // which case the server falls back to a per-account trial. Computed once.
  var _devHash;
  function deviceHash() {
    if (_devHash !== undefined) return _devHash;
    _devHash = "";
    try {
      var uuid = "";
      if (IS_WIN) {
        // PowerShell is the most reliable; fall back to wmic on older Windows.
        try { uuid = childx.execSync('powershell -NoProfile -Command "(Get-CimInstance -ClassName Win32_ComputerSystemProduct).UUID"', { timeout: 6000 }).toString().replace(/\s+/g, ""); } catch (e1) {}
        if (!uuid || uuid.length < 8) {
          try {
            var w = childx.execSync("wmic csproduct get uuid", { timeout: 6000 }).toString().split(/\r?\n/);
            for (var i = 0; i < w.length; i++) { var t = w[i].trim(); if (t && t.toUpperCase() !== "UUID") { uuid = t; break; } }
          } catch (e2) {}
        }
      } else {
        var out = childx.execSync("/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice", { timeout: 4000 }).toString();
        var m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
        if (m && m[1]) uuid = m[1];
      }
      if (uuid && uuid.length >= 8) _devHash = cryptox.createHash("sha256").update("onmic-device|" + uuid).digest("hex");
    } catch (e) { _devHash = ""; }
    return _devHash;
  }
  function deviceStatus() {
    var h = deviceHash();
    return h ? ("ok " + h.slice(0, 12)) : "unavailable";
  }

  var IS_WIN = (typeof process !== "undefined" && process.platform === "win32");
  function deviceLabel() {
    try {
      var host = osx && osx.hostname ? String(osx.hostname()).replace(/[^A-Za-z0-9._ -]/g, "").slice(0, 60) : "";
      var os = IS_WIN ? "Windows PC" : "Mac";
      return host ? (os + " - " + host) : os;
    } catch (e) {
      return IS_WIN ? "Windows PC" : "Mac";
    }
  }
  function dir() {
    var d = "";
    try {
      if (IS_WIN) {
        var base = ((typeof process !== "undefined" && process.env.APPDATA) || (osx.homedir() + "/AppData/Roaming")).replace(/\\/g, "/");
        d = base + "/OnMic";
      } else {
        d = osx.homedir() + "/Library/Application Support/OnMic";
      }
      if (fsx && !fsx.existsSync(d)) fsx.mkdirSync(d, { recursive: true });
    } catch (e) {}
    return d;
  }
  function licPath() { return dir() + "/license.json"; }
  var KEYCHAIN_SERVICE = "OnMic";
  var KEYCHAIN_ACCOUNT = "onmic-license";
  function readSecureToken(){
    if (!childx || !childx.execFileSync) return "";
    if (IS_WIN) return "";
    try {
      return childx.execFileSync("/usr/bin/security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], { timeout: 3000 }).toString().replace(/\s+$/,"");
    } catch(e){ return ""; }
  }
  function readProtectedWindowsToken(blob){
    if (!IS_WIN || !childx || !childx.execFileSync || !blob) return "";
    try {
      var script = "$raw=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($raw);$u=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($u)";
      return childx.execFileSync("powershell", ["-NoProfile", "-Command", script], { input: blob, timeout: 4000 }).toString().replace(/\s+$/,"");
    } catch(e){ return ""; }
  }
  function protectWindowsToken(token){
    if (!IS_WIN || !childx || !childx.execFileSync || !token) return "";
    try {
      var script = "$b=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd());$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($p)";
      return childx.execFileSync("powershell", ["-NoProfile", "-Command", script], { input: token, timeout: 4000 }).toString().replace(/\s+$/,"");
    } catch(e){ return ""; }
  }
  function writeSecureToken(token){
    if (IS_WIN || !childx || !childx.execFileSync || !token) return false;
    try {
      childx.execFileSync("/usr/bin/security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w", token], { timeout: 3000 });
      return true;
    } catch(e){ return false; }
  }
  function deleteSecureToken(){
    if (IS_WIN || !childx || !childx.execFileSync) return;
    try { childx.execFileSync("/usr/bin/security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT], { timeout: 3000 }); } catch(e){}
  }
  function load() {
    var o = null;
    try { o = JSON.parse(fsx.readFileSync(licPath(), "utf8")); } catch (e) { o = null; }
    if (o && !o.token && o.secureToken) {
      var t = readSecureToken();
      if (t) o.token = t;
    }
    if (o && !o.token && o.dpapiToken) {
      var wt = readProtectedWindowsToken(o.dpapiToken);
      if (wt) o.token = wt;
    }
    return o;
  }
  function save(o) {
    try {
      var copy = {};
      for (var k in o) { if (o.hasOwnProperty(k)) copy[k] = o[k]; }
      if (copy.token && IS_WIN) {
        var protectedToken = protectWindowsToken(copy.token);
        if (protectedToken) {
          copy.dpapiToken = protectedToken;
          delete copy.token;
        }
      } else if (copy.token && writeSecureToken(copy.token)) {
        delete copy.token;
        copy.secureToken = true;
      }
      // v1.03: make a plaintext fallback VISIBLE (Keychain/DPAPI write failed) so
      // the support bundle can show it instead of it happening silently.
      copy.plaintextToken = !!copy.token;
      fsx.writeFileSync(licPath(), JSON.stringify(copy, null, 2));
      try { fsx.chmodSync(licPath(), 384); } catch(eCh) {}
      return true;
    } catch (e) { return false; }
  }
  function clearLicense() { try { fsx.unlinkSync(licPath()); } catch (e) {} deleteSecureToken(); }
  function isSignedIn() { var l = load(); return !!(l && l.token); }

  function nonce() {
    try { return cryptox.randomBytes(18).toString("hex"); }
    catch (e) { return (String(Date.now()) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)); }
  }

  // Node https POST of a JSON body. cb(err, statusCode, parsedJsonOrNull).
  function postJSON(path, body, cb) {
    try {
      var settled = false;
      function done(err, statusCode, parsed) {
        if (settled) return;
        settled = true;
        cb(err, statusCode, parsed);
      }
      var payload = JSON.stringify(body);
      var req = https.request({
        hostname: SITE_HOST, port: 443, path: path, method: "POST",
        headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
      }, function (res) {
        var buf = ""; res.setEncoding("utf8");
        res.on("data", function (c) { buf += c; });
        res.on("end", function () { var d = null; try { d = JSON.parse(buf); } catch (e) {} done(null, res.statusCode, d); });
      });
      req.on("error", function (e) { done(e); });
      req.setTimeout(12000, function () { done(new Error("timeout")); try { req.destroy(); } catch (_) {} });
      req.write(payload); req.end();
    } catch (e) { cb(e); }
  }

  // Verify the stored token against the live entitlement.
  // cb(result):
  //   { signedOut:true }                      -> no token stored
  //   { invalid:true }                        -> token rejected (sign in again)
  //   { offline:true, allowed:Bool }          -> couldn't reach server (grace)
  //   { ok:true, allowed, state, daysLeft, trialEndsAt, userId }
  // Offline grace: only honour a SERVER-ISSUED, signed entitlement (entSig +
  // entExpiresAt). A hand-edited license.json without a real signature is not
  // trusted, and we never grant access if the clock has been rolled back below
  // the highest time we've ever seen (defeats set-the-clock-back tricks).
  function offlineOk(lic){
    var last = lic && lic.lastStatus;
    if(!last || !last.allowed || !last.entSig || !last.entExpiresAt) return false;
    var now = Date.now();
    // v1.03: every field this check leans on must be PRESENT, not just
    // consistent when present. The old falsy-guards meant deleting lic.hw or
    // lic.deviceHash from a hand-edited license.json simply SKIPPED those
    // checks. Both are stamped on every successful online verify, so a real
    // license always has them.
    if(typeof lic.hw !== "number" || now < lic.hw - 60000) return false; // clock rolled back (or hw removed)
    var dh = deviceHash();
    if(dh){ if(!lic.deviceHash || lic.deviceHash !== dh) return false; } // copied to another computer (or hash removed)
    // Forgery cap: the panel can't hold CRON_SECRET, so it can't actually
    // verify the entSig HMAC — a hand-edited license.json with a far-future
    // entExpiresAt and any non-empty entSig would otherwise grant access
    // forever while offline. Bound offline access to GRACE_MS past the last
    // SUCCESSFUL ONLINE verify (last.at, stamped in verify() on every 2xx ok).
    // v1.03: last.at must also not be in the FUTURE — a forged far-future
    // last.at made (now - last.at) negative, passing the grace window forever.
    if(typeof last.at !== "number" || last.at > now + 300000 || now - last.at >= GRACE_MS) return false;
    var t = Date.parse(last.entExpiresAt);
    return (t===t) && now < t;
  }
  function verify(cb) {
    var lic = load();
    if (!lic || !lic.token) { cb({ signedOut: true }); return; }
    var dh = deviceHash();
    // v1.09: report the running panel version so the admin dashboard can show it.
    var ver = "";
    try { ver = (typeof window !== "undefined" && window.__ONMIC_PANEL_VERSION) ? String(window.__ONMIC_PANEL_VERSION) : ""; } catch (eV) {}
    postJSON("/api/onmic/verify", { token: lic.token, device: dh, deviceLabel: deviceLabel(), version: ver }, function (err, status, data) {
      if (err) {
        // Offline grace: trust the last good result for a bounded window.
        var last = lic.lastStatus;
        cb({ offline: true, allowed: offlineOk(lic), state: last ? last.state : null });
        return;
      }
      if (status === 401) { cb({ invalid: true }); return; }
      if (status === 429) {
        // v1.05 (audit #7): a rate-limited verify used to hard-gate the panel
        // even though a plain NETWORK error gets 72h offline grace. Lean on the
        // same signed last-good entitlement first; only show the try-again gate
        // when there is no valid grace to fall back on.
        if (offlineOk(lic)) { var lastRL = lic.lastStatus; cb({ offline: true, allowed: true, state: lastRL ? lastRL.state : null }); return; }
        cb({ rateLimited: true }); return;
      }
      if (status >= 200 && status < 300 && data && data.ok) {
        lic.lastStatus = { allowed: data.allowed, state: data.state, at: Date.now(), entExpiresAt: data.entExpiresAt || null, entSig: data.entSig || null };
        lic.hw = Math.max(lic.hw || 0, Date.now());   // monotonic clock high-water mark
        lic.userId = data.userId || lic.userId || null;
        lic.deviceHash = dh || lic.deviceHash || null;
        save(lic);
        if (!dh && !data.allowed && data.state === "expired") { cb({ noDevice: true }); return; }
        cb(data);
        return;
      }
      // Unexpected response — treat as offline-ish (don't lock a paying user out on a blip).
      var last2 = lic.lastStatus;
      cb({ offline: true, allowed: offlineOk(lic), state: last2 ? last2.state : null });
    });
  }

  // v1.04: exchange a one-time sign-in code for the real token (audit item 18).
  // cb(err, token). Uses the same Node https POST as verify(), so no token ever
  // rides in a URL. A non-2xx / missing-token response is a hard failure.
  function redeemCode(code, cb) {
    postJSON("/api/onmic/redeem", { code: code }, function (err, status, data) {
      if (err) { cb(err); return; }
      if (status >= 200 && status < 300 && data && data.ok && data.token) { cb(null, data.token); return; }
      cb(new Error("redeem failed (" + status + ")"));
    });
  }

  // Begin the loopback sign-in. cb receives the same shapes as verify() once
  // complete, or { error } / { manualURL } if it couldn't open the browser.
  function signIn(cb) {
    if (!http) { cb({ error: "This panel needs Node.js enabled to sign in." }); return null; }
    var state = nonce();
    var finished = false;
    var manualHanded = false;   // v1.03: the manual-URL flow gets extra time (see armTimeout)
    var idx = 0;
    var server = http.createServer(function (rq, rs) {
      try {
        var u = new URL(rq.url, "http://127.0.0.1");
        if (u.pathname === "/callback") {
          // v1.04: the site hands us a short-lived, single-use CODE (audit item
          // 18) so the long-lived token never appears in the browser URL/history.
          // We exchange it for the token via POST /api/onmic/redeem. Older
          // servers still send ?token= directly — accepted as a fallback so a
          // panel update never has to wait on a server deploy.
          var code = u.searchParams.get("code");
          var token = u.searchParams.get("token");
          var st = u.searchParams.get("state");
          if (finished || st !== state || (!code && !token)) {
            rs.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
            rs.end("<h2>OnMic: that sign-in response didn't match. Please try again from the panel.</h2>");
            return;
          }
          finished = true;
          if (code) {
            // Exchange the code for the real token, then verify as usual.
            // v1.05 (audit #7): respond to the browser AFTER the redeem settles —
            // the success page used to be sent BEFORE redeemCode ran, telling the
            // user "you're signed in" even when the exchange then failed. The
            // pending loopback response (and the server) stay open until we know;
            // server.close() only blocks new connections, so the in-flight socket
            // is unaffected and the browser happily waits the few seconds a
            // redeem can take (postJSON hard-caps it at 12s).
            redeemCode(code, function (err, tok) {
              var redeemed = !(err || !tok);
              try {
                rs.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
                rs.end(redeemed ? successHTML() : failureHTML());
              } catch (_) {}
              try { server.close(); } catch (_) {}
              if (!redeemed) { cb({ error: "Sign-in couldn't be completed (code exchange failed). Please try again." }); return; }
              save({ token: tok, ts: Date.now() });
              verify(cb);
            });
          } else {
            rs.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
            rs.end(successHTML());
            try { server.close(); } catch (_) {}
            save({ token: token, ts: Date.now() });   // legacy token-in-URL flow
            verify(cb);
          }
        } else { rs.writeHead(404); rs.end(); }
      } catch (e) { try { rs.writeHead(500); rs.end(); } catch (_) {} }
    });
    server.on("error", function () { if (!finished) tryNext(); });
    function tryNext() {
      // v1.04: mark finished + close on port exhaustion so the 5-minute timer
      // can't later fire cb() a SECOND time with a "timed out" error (which would
      // clobber a fresh sign-in attempt's UI).
      if (idx >= PORTS.length) { finished = true; try { server.close(); } catch (_) {} cb({ error: "Couldn't open a local sign-in listener. Another app may be using OnMic's local sign-in ports; close browser helpers/VPN tools if needed and try again." }); return; }
      var port = PORTS[idx++];
      try {
        server.listen(port, "127.0.0.1", function () {
          // flow=code asks the site for the one-time-code sign-in (audit item
          // 18); servers that don't understand it fall back to the token flow,
          // which this panel still accepts.
          var url = SITE + "/activate?port=" + port + "&state=" + encodeURIComponent(state) + "&flow=code";
          var opened = CEP.openURL(url);
          if (!opened) { manualHanded = true; cb({ manualURL: url }); }
        });
      } catch (e) { tryNext(); }
    }
    tryNext();
    // v1.03: the old flat 5-minute timer killed the manual-URL flow mid-typing
    // (closing the loopback server while the user copied the URL into a browser)
    // and then fired cb a SECOND time with an error. Manual flow now gets one
    // 10-minute extension, and a timed-out sign-in can never double-fire because
    // the /callback path checks `finished`.
    function armTimeout(ms) {
      setTimeout(function () {
        if (finished) return;
        if (manualHanded) { manualHanded = false; armTimeout(10 * 60 * 1000); return; }
        finished = true;
        try { server.close(); } catch (_) {}
        cb({ error: "Sign-in timed out. Please try again." });
      }, ms);
    }
    armTimeout(5 * 60 * 1000);
    // v1.07 (Adobe review issue 2): the caller gets a handle so the gate can
    // offer a working Cancel while we wait on the browser — closing the browser
    // window leaves no signal here, so without this the UI sat on "Opening your
    // browser…" with a disabled button until the 5-minute timeout.
    return {
      cancel: function () {
        if (finished) return;
        finished = true;
        try { server.close(); } catch (_) {}
        cb({ cancelled: true });
      }
    };
  }

  function signOut() { clearLicense(); }

  // Tokenless, best-effort "what's the latest shipped version" probe so the panel
  // can show an update notice on the sign-in / expired-trial screens even when no
  // token exists (verify() is never reached). The verify endpoint rejects the
  // empty token, but its JSON response still carries latestVersion/downloadUrl,
  // which is all we need here. Never throws; cb({}) on any failure.
  function latestVersion(cb) {
    if (typeof cb !== "function") return;
    if (!https) { cb({}); return; }
    try {
      postJSON("/api/onmic/verify", { token: "", probe: "version" }, function (err, status, data) {
        if (err || !data) { cb({}); return; }
        cb({
          latestVersion: data.latestVersion || null,
          downloadUrl: data.downloadUrl || null,
          downloadUrlWin: data.downloadUrlWin || null
        });
      });
    } catch (e) { cb({}); }
  }

  function successHTML() {
    return '<!doctype html><meta charset="utf-8"><title>OnMic</title>' +
      '<div style="font-family:-apple-system,Segoe UI,sans-serif;max-width:460px;margin:14vh auto;text-align:center;color:#1a1a1a">' +
      '<div style="font-size:42px">🎙️</div>' +
      '<h1 style="font-size:22px;margin:10px 0 6px">You\'re signed in to OnMic</h1>' +
      '<p style="color:#666;font-size:15px">You can close this tab and return to Premiere&nbsp;Pro.</p></div>';
  }

  // v1.05 (audit #7): shown when the one-time code couldn't be redeemed — the
  // old flow showed the success page unconditionally, before the redeem ran.
  function failureHTML() {
    return '<!doctype html><meta charset="utf-8"><title>OnMic</title>' +
      '<div style="font-family:-apple-system,Segoe UI,sans-serif;max-width:460px;margin:14vh auto;text-align:center;color:#1a1a1a">' +
      '<div style="font-size:42px">🎙️</div>' +
      '<h1 style="font-size:22px;margin:10px 0 6px">Sign-in couldn\'t be completed</h1>' +
      '<p style="color:#666;font-size:15px">The sign-in code couldn\'t be redeemed. Go back to Premiere&nbsp;Pro and try signing in again from the OnMic panel.</p></div>';
  }

  return {
    verify: verify, signIn: signIn, signOut: signOut, isSignedIn: isSignedIn,
    latestVersion: latestVersion,
    deviceStatus: deviceStatus,
    siteURL: SITE,
    // exposed for unit tests; pure function of the license object
    _offlineOk: offlineOk, _GRACE_MS: GRACE_MS
  };
})();
if (typeof window !== "undefined") { window.OnMicAuth = OnMicAuth; }
