/**
 * ZeqEqPassword — equation-style password generator for the vault.
 *
 * Generates passwords that VISUALLY look like mathematical equations but
 * use ONLY characters every website's password validator accepts:
 *
 *   • Letters: A–Z a–z (with ambiguous letters I,l,O,o omitted to avoid
 *     read-back errors when the user types into another device)
 *   • Digits:  0–9
 *   • Operators that universally pass HTML form validators:
 *       +  -  *  /  =  ^  !  .  _
 *   • Parentheses ( ) are included sparingly for visual equation feel
 *
 * Every output starts with the literal string  "Zeq"  per the project
 * convention so the user can recognise their generated passwords at a
 * glance and they all sort together in any password-manager search.
 *
 * Two operating modes:
 *
 *   ZeqEqPassword.derive(masterKey, site, length, rev)
 *       Deterministic. Re-running with the same {key, site, length, rev}
 *       always returns the same password. The user does NOT have to
 *       store the password — they re-derive it on demand by re-typing
 *       the site name in the vault. To rotate (because a site forced a
 *       password change), bump `rev` from 1 → 2 → 3 …
 *
 *   ZeqEqPassword.random(length)
 *       Fully random equation-style password seeded from
 *       window.crypto.getRandomValues. Use when the user wants a
 *       traditional store-the-password flow (encrypted in vault).
 *
 * Both modes guarantee:
 *   - First three characters are exactly "Zeq"
 *   - Total length matches the `length` argument (clamped to 12-128)
 *   - At least one digit and one operator appear after the prefix
 *   - The string never ends on a trailing operator
 *
 * The module is pure browser JS; no network call. The user's master key
 * never leaves the client.
 */

(function (root) {
  // ── Universally-allowed character buckets ────────────────────────
  // Letters skip ambiguous I/l/O/o so a copy-by-eye to a phone always
  // disambiguates; preserves visual equation-letter feel.
  var ALPHA = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz";
  var DIGIT = "0123456789";
  var OPS_INNER = "+-*/=^!._";   // safe in any password field
  var OPS_NOEND = "+-*/=^_";     // operators that should never be the LAST char

  function clampLen(n) {
    n = +n || 24;
    if (n < 12) n = 12;
    if (n > 128) n = 128;
    return n;
  }

  // FNV-1a + xorshift PRNG seeded from a string. Used as a fallback
  // pseudo-random source for the deterministic mode.
  function seedFromString(s) {
    var h = 0x811c9dc5 >>> 0;
    for (var i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = Math.imul(h, 0x01000193) >>> 0;
    }
    return h >>> 0;
  }

  function xorshift32(state) {
    return function () {
      state ^= state << 13;
      state ^= state >>> 17;
      state ^= state << 5;
      return (state >>> 0) / 0x100000000;
    };
  }

  // ── Async HMAC-SHA256 → 32 bytes (Uint8Array) ─────────────────────
  // Used by derive(). Falls back to FNV+xorshift if WebCrypto absent.
  async function hmacSha256(keyStr, msgStr) {
    if (root.crypto && root.crypto.subtle) {
      var enc = new TextEncoder();
      var key = await root.crypto.subtle.importKey(
        "raw", enc.encode(keyStr),
        { name: "HMAC", hash: "SHA-256" },
        false, ["sign"]
      );
      var sig = await root.crypto.subtle.sign("HMAC", key, enc.encode(msgStr));
      return new Uint8Array(sig);
    }
    // Soft fallback (synchronous, lower entropy — only kicks in on
    // extremely old browsers without WebCrypto).
    var seed = seedFromString(keyStr + "|" + msgStr);
    var rand = xorshift32(seed);
    var out = new Uint8Array(32);
    for (var i = 0; i < 32; i++) out[i] = Math.floor(rand() * 256);
    return out;
  }

  // ── Body builder — equation-style assembler from a byte array ─────
  // Pattern: [letter][digit?][op][letter][digit][op] … with parens
  // injected at deterministic positions. Always ends on a non-op char.
  function buildEquation(bytes, length) {
    var prefix = "Zeq";
    var bodyLen = length - prefix.length;
    if (bodyLen <= 0) return prefix;

    var out = "";
    var bi = 0;
    var openParen = false;
    var hasDigit = false;
    var hasOp = false;

    function nextByte() {
      var b = bytes[bi % bytes.length];
      bi++;
      return b;
    }

    while (out.length < bodyLen) {
      var slot = out.length;          // position within body
      var b = nextByte();

      // Decide character class for this slot. The pattern is:
      //   slot 0:    letter
      //   slot 1-2:  letter or digit
      //   slot 3:    operator (introduces equation feel)
      //   slot 4+:   alternate letter/digit/operator with parens
      var ch = "";
      var isLast = (out.length === bodyLen - 1);

      if (slot === 0) {
        ch = ALPHA[b % ALPHA.length];
      } else if (slot < 3) {
        ch = (b & 1) ? DIGIT[b % DIGIT.length] : ALPHA[b % ALPHA.length];
        if (DIGIT.indexOf(ch) !== -1) hasDigit = true;
      } else {
        var pick = b % 8;
        if (pick < 3) {
          ch = ALPHA[nextByte() % ALPHA.length];
        } else if (pick < 5) {
          ch = DIGIT[nextByte() % DIGIT.length];
          hasDigit = true;
        } else if (pick === 5 && !isLast) {
          ch = OPS_INNER[nextByte() % OPS_INNER.length];
          hasOp = true;
        } else if (pick === 6 && !openParen && bodyLen - out.length >= 4) {
          ch = "(";
          openParen = true;
        } else if (pick === 7 && openParen) {
          ch = ")";
          openParen = false;
        } else {
          ch = ALPHA[nextByte() % ALPHA.length];
        }
      }
      out += ch;
    }

    // Close any dangling open paren by replacing the last char.
    if (openParen) {
      out = out.substring(0, out.length - 1) + ")";
    }

    // Make sure last char is NOT a trailing-prone operator.
    while (out.length && OPS_NOEND.indexOf(out[out.length - 1]) !== -1) {
      out = out.substring(0, out.length - 1) + ALPHA[nextByte() % ALPHA.length];
    }

    // Inject a digit and an op if the equation has neither (rare with
    // length ≥ 12 but possible with adversarial seeds).
    if (!hasDigit && out.length > 4) {
      var injectAt = 4;
      out = out.substring(0, injectAt) + DIGIT[nextByte() % DIGIT.length] + out.substring(injectAt + 1);
    }
    if (!hasOp && out.length > 5) {
      var injectAt2 = 5;
      out = out.substring(0, injectAt2) + OPS_INNER[nextByte() % OPS_INNER.length] + out.substring(injectAt2 + 1);
    }

    return prefix + out;
  }

  // ── Public API ───────────────────────────────────────────────────
  async function derive(masterKey, site, length, rev) {
    var len = clampLen(length);
    var rotation = parseInt(rev || 1, 10) || 1;
    var msg = "Zeq.eqpw|" + String(site || "").toLowerCase() + "|" + len + "|" + rotation;
    var bytes = await hmacSha256(String(masterKey || ""), msg);
    return buildEquation(bytes, len);
  }

  function randomBytes(n) {
    var bytes = new Uint8Array(n);
    if (root.crypto && root.crypto.getRandomValues) {
      root.crypto.getRandomValues(bytes);
    } else {
      for (var i = 0; i < n; i++) bytes[i] = Math.floor(Math.random() * 256);
    }
    return bytes;
  }

  function randomEq(length) {
    var len = clampLen(length);
    return buildEquation(randomBytes(64), len);
  }

  root.ZeqEqPassword = {
    derive: derive,
    random: randomEq,
    LIMITS: { min: 12, max: 128 },
    PRESET_LENGTHS: [16, 24, 32, 48, 64, 96, 128],
  };
})(typeof window !== "undefined" ? window : globalThis);
