const { build } = require("esbuild");
const path = require("path");
const cfDir = __dirname;
const s = (f) => path.resolve(cfDir, f);

// Resolve the absolute path of the original queue module so we can intercept it
const queueModulePath = path.resolve(cfDir, "../api/src/libs/queue/index.ts");
const queueShimPath = s("shims/queue.ts");

// Resolve the absolute path of the original lock module so we can intercept it
const lockModulePath = path.resolve(cfDir, "../api/src/libs/lock.ts");
const lockShimPath = s("shims/lock.ts");

// Plugin to redirect libs/queue to our CF Workers shim
const queueNormalizedTarget = queueModulePath.replace(/\/index\.ts$/, '').replace(/\/index$/, '');
const queueShimPlugin = {
  name: 'queue-shim',
  setup(build) {
    // Intercept any import that resolves to the original queue module
    build.onResolve({ filter: /libs\/queue/ }, (args) => {
      const resolveDir = args.resolveDir;
      if (!resolveDir) return undefined;

      // Resolve the relative import to an absolute path
      const resolved = path.resolve(resolveDir, args.path).replace(/\/index(\.ts)?$/, '');
      const match = resolved === queueNormalizedTarget;
      if (args.path.includes('queue')) {
        console.log(`[queue-shim] ${match ? 'MATCH' : 'skip'}: ${args.path} -> ${resolved} (target: ${queueNormalizedTarget})`);
      }
      if (match) {
        return { path: queueShimPath };
      }
      return undefined;
    });
  },
};

// Plugin to redirect libs/lock to our CF Workers D1-based lock shim
const lockNormalizedTarget = lockModulePath.replace(/\.ts$/, '');
const lockShimPlugin = {
  name: 'lock-shim',
  setup(build) {
    build.onResolve({ filter: /libs\/lock/ }, (args) => {
      const resolveDir = args.resolveDir;
      if (!resolveDir) return undefined;

      const resolved = path.resolve(resolveDir, args.path).replace(/\.ts$/, '');
      const match = resolved === lockNormalizedTarget;
      if (args.path.includes('lock')) {
        console.log(`[lock-shim] ${match ? 'MATCH' : 'skip'}: ${args.path} -> ${resolved} (target: ${lockNormalizedTarget})`);
      }
      if (match) {
        return { path: lockShimPath };
      }
      return undefined;
    });
  },
};

// Plugin to neutralize rolldown-generated `__require(import.meta.url)` helpers
// that ship inside npm packages built with rolldown (e.g. @ocap/message,
// @arcblock/did-connect-js). These helpers use `createRequire(import.meta.url)`
// at module init to pull in CJS deps like `google-protobuf`, `debug`, etc. —
// which neither resolve nor work in the CF Workers runtime.
//
// The downstream patches/shims that use these require calls are all either:
//   - monkey-patches for edge cases Payment Kit doesn't exercise, or
//   - debug()/logger() factories that Payment Kit doesn't need on CF.
//
// Replace every `_virtual/rolldown_runtime.mjs` with a stub whose `__require`
// returns a Proxy that swallows further property accesses + function calls,
// and whose `__commonJSMin` returns an empty-module factory. This lets the
// importing modules finish loading without crashing the worker at startup.
const rolldownRuntimeStub = `
// After the Phase 2 CBOR switch (payment-method.ts + did-connect-auth.ts
// moved off @ocap/client/legacy), no codepath in the worker calls
// __require('google-protobuf') at runtime. The CBOR-only @ocap/client
// default entry + @ocap/client/encode use plain ESM imports for their
// deps. Any remaining __require calls in Rolldown-compiled packages
// (debug, @arcblock/event-hub, etc.) are handled by the __noop Proxy
// below — they don't need real modules for payment-kit's flows.
const __requireMap = {};
const __noopTarget = function(){};
const __noopHandler = {
  get(t, p) {
    if (p === Symbol.toPrimitive || p === 'toString') return () => '';
    if (p === 'default') return new Proxy(__noopTarget, __noopHandler);
    return new Proxy(__noopTarget, __noopHandler);
  },
  apply() { return new Proxy(__noopTarget, __noopHandler); },
  construct() { return new Proxy(__noopTarget, __noopHandler); },
};
const __noop = new Proxy(__noopTarget, __noopHandler);
export const __commonJSMin = function(cb){
  return function(){
    var m = { exports: {} };
    try { cb(m.exports, m); } catch (_) {}
    return m.exports;
  };
};
export const __require = function(id){
  if (__requireMap[id]) return __requireMap[id];
  return __noop;
};
export const __exportAll = function(target, source){
  if (source) {
    try {
      for (var key in source) {
        if (key !== 'default' && !Object.prototype.hasOwnProperty.call(target, key)) {
          Object.defineProperty(target, key, {
            get: function() { return source[key]; },
            enumerable: true,
            configurable: true,
          });
        }
      }
    } catch (_) {}
  }
  return target;
};
export const __toESM = function(mod){ return mod && mod.__esModule ? mod : { default: mod }; };
export const __toCommonJS = function(mod){ return mod; };
export const __copyProps = function(target){ return target; };
export default { __commonJSMin, __require, __exportAll, __toESM, __toCommonJS, __copyProps };
`;
const rolldownRuntimeNoopPlugin = {
  name: 'rolldown-runtime-noop',
  setup(build) {
    // Catch any import that resolves to a `_virtual/rolldown_runtime.mjs`
    // file, regardless of which package it comes from.
    build.onResolve({ filter: /_virtual\/rolldown_runtime\.mjs$/ }, (args) => {
      return { path: args.path, namespace: 'rolldown-runtime-noop' };
    });
    build.onLoad({ filter: /.*/, namespace: 'rolldown-runtime-noop' }, () => ({
      contents: rolldownRuntimeStub,
      loader: 'js',
      // Need a resolveDir so the virtual stub's `import "google-protobuf"` resolves
      // against the workspace's node_modules. Pointing at cfDir is sufficient since
      // pnpm hoists google-protobuf up to the repo root node_modules.
      resolveDir: cfDir,
    }));
  },
};

// Plugin: fix lodash sub-path CJS/ESM interop
// The "lodash" → "lodash-es" alias also redirects require('lodash/camelCase')
// to lodash-es/camelCase (ESM). CJS consumers get { __esModule, default: fn }
// instead of the function itself, causing "_m is not a function" at runtime.
// Fix: intercept lodash-es sub-path imports from CJS contexts and generate a
// virtual wrapper that re-exports the default as module.exports.
const lodashSubpathPlugin = {
  name: 'lodash-subpath-interop',
  setup(build) {
    // Intercept resolved lodash-es sub-path imports that come from require() calls
    // Intercept CJS require('lodash/xxx') — the alias hasn't applied yet at this stage
    build.onResolve({ filter: /^lodash\/.+/ }, (args) => {
      if (args.kind === 'require-call') {
        // Convert lodash/xxx → lodash-es/xxx and wrap for CJS compat
        const esPath = args.path.replace(/^lodash\//, 'lodash-es/');
        return {
          path: esPath,
          namespace: 'lodash-cjs-compat',
        };
      }
    });
    build.onLoad({ filter: /.*/, namespace: 'lodash-cjs-compat' }, (args) => {
      // Resolve the actual file path, then read and re-export as CJS.
      // By loading the original ESM source as 'js' with CJS-style wrapping,
      // esbuild treats the result as CJS, avoiding the __toModule interop.
      const subPath = args.path.replace('lodash-es/', '');
      const filePath = require.resolve(`lodash-es/${subPath}`);
      const source = require('fs').readFileSync(filePath, 'utf8');
      // Transform: replace ESM export default with module.exports
      // lodash-es modules all follow: import deps... ; function fn(...){...}; export default fn;
      const transformed = source
        .replace(/^export default /m, 'module.exports = ')
        .replace(/^export\s*\{[^}]*\}\s*;?\s*$/m, '');
      return {
        contents: transformed,
        loader: 'js',
        resolveDir: require('path').dirname(filePath),
      };
    });
  },
};

// Plugin: redirect bare `@arcblock/did-util` and `@ocap/message` imports to their
// CBOR-only subpath entries, and noop `@arcblock/did-util/protobuf`. This strips
// the protobuf runtime (`@ocap/proto/runtime`, `google-protobuf`, `*_pb.js`) out
// of the CF Workers bundle. Only matches EXACT bare specifiers — subpath imports
// (e.g. `@arcblock/did-util/cbor`) pass through untouched.
const cborOnlyPlugin = {
  name: 'cbor-only-redirect',
  setup(build) {
    build.onResolve({ filter: /^@arcblock\/did-util$/ }, () => ({
      path: path.resolve(__dirname, '../../..', 'node_modules/@arcblock/did-util/esm/cbor.mjs'),
    }));
    build.onResolve({ filter: /^@ocap\/message$/ }, () => ({
      path: path.resolve(__dirname, '../../..', 'node_modules/@ocap/message/esm/cbor.mjs'),
    }));
    build.onResolve({ filter: /^@arcblock\/did-util\/protobuf$/ }, () => ({
      path: s('shims/noop.ts'),
    }));
  },
};

// Plugin: noop entire package trees that have sub-path imports (alias alone
// doesn't work because esbuild treats "axon" alias as prefix, breaking
// "axon/lib/plugins/queue"). This plugin intercepts bare + sub-path imports.
const noopPackagesPlugin = {
  name: 'noop-packages',
  setup(build) {
    const packages = ['axon', '@arcblock/ws', '@arcblock/event-hub', 'graphql', 'follow-redirects', 'proxy-from-env'];
    const filter = new RegExp('^(' + packages.join('|') + ')(\\/|$)');
    build.onResolve({ filter }, () => ({ path: s('shims/noop.ts') }));
  },
};

// Plugin: drop ethers' non-English BIP39 wordlists (~70K dead weight). The payment
// worker never uses Mnemonic/HD wallets (0 source refs to Mnemonic/HDNode/wordlist),
// so the 8 non-English word tables never execute. ethers' own /dist build strips
// these too (~80kb). Keep LangEn (Mnemonic default). Stub the rest with a static
// wordlist() returning null so wordlists.js's top-level LangXx.wordlist() calls
// (run at module init) don't crash.
const dropEthersWordlistsPlugin = {
  name: 'drop-ethers-wordlists',
  setup(build) {
    build.onLoad({ filter: /ethers\/lib\.esm\/wordlists\/lang-(cz|es|fr|ja|ko|it|pt|zh)\.js$/ }, (args) => {
      const lang = /lang-(\w+)\.js$/.exec(args.path)[1];
      const cls = 'Lang' + lang.charAt(0).toUpperCase() + lang.slice(1);
      return { contents: `export class ${cls} { static wordlist() { return null; } }`, loader: 'js' };
    });
  },
};

build({
  entryPoints: [s("worker.ts")],
  bundle: true, format: "esm", platform: "node", target: "esnext",
  outdir: s("dist"), minify: true, sourcemap: true, metafile: true,
  mainFields: ["module", "main"],
  plugins: [
    noopPackagesPlugin, queueShimPlugin, lockShimPlugin, rolldownRuntimeNoopPlugin, lodashSubpathPlugin, dropEthersWordlistsPlugin,
  ],
  external: ["cloudflare:*", "__STATIC_CONTENT_MANIFEST"],
  // Give import.meta.url a stable fallback so bundled deps that call
  // createRequire(import.meta.url) at module init don't throw at worker
  // startup. The polyfill in banner handles any legitimate require() lookups.
  define: {
    "import.meta.url": '"file:///worker.js"',
  },
  banner: {
    js: [
      '// Polyfill require() for CJS modules that dynamically require Node builtins',
      'import __nodeCrypto from "node:crypto";',
      'import __nodeBuffer from "node:buffer";',
      'import __nodeEvents from "node:events";',
      'import __nodeStream from "node:stream";',
      'import __nodeUtil from "node:util";',
      'import __nodeAssert from "node:assert";',
      'import __nodePath from "node:path";',
      'import __nodeUrl from "node:url";',
      'import __nodeProcess from "node:process";',
      'const __qsShim = { stringify: function(o) { return new URLSearchParams(o).toString(); }, parse: function(s) { return Object.fromEntries(new URLSearchParams(s).entries()); } };',
      'const __nodeModules = { crypto: __nodeCrypto, buffer: __nodeBuffer, events: __nodeEvents, stream: __nodeStream, util: __nodeUtil, assert: __nodeAssert, path: __nodePath, url: __nodeUrl, process: __nodeProcess, querystring: __qsShim };',
      '// Accept both "xxx" and "node:xxx" specifier forms; return empty stub for unknown modules',
      '// (some deps tree-shake to nothing but still call require() at init — a hard throw crashes the worker).',
      'globalThis.require = globalThis.require || function(m) { var key = typeof m === "string" && m.indexOf("node:") === 0 ? m.slice(5) : m; if (__nodeModules[key]) return __nodeModules[key]; console.warn("[require shim] unknown module: " + m); return {}; };',
      '// Ensure process.stderr.fd exists for debug/supports-color',
      'if (typeof process !== "undefined") { if (!process.stderr) process.stderr = { fd: 2, write: function(){} }; else if (process.stderr.fd === undefined) process.stderr.fd = 2; if (!process.stdout) process.stdout = { fd: 1, write: function(){} }; else if (process.stdout.fd === undefined) process.stdout.fd = 1; }',
      '// Polyfill process.on/process.exit for modules that use them at init',
      'if (typeof process !== "undefined" && !process.on) { process.on = function(){}; process.once = function(){}; process.off = function(){}; process.removeListener = function(){}; process.emit = function(){}; process.exit = function(){}; }',
      '// CF Workers: defer timers called during global scope init until first request',
      'var __deferredTimers = []; var __timersReady = false;',
      'globalThis.__flushDeferredTimers = function() { if (__timersReady) return; __timersReady = true; var q = __deferredTimers; __deferredTimers = []; q.forEach(function(entry) { try { entry.fn.apply(null, entry.args); } catch(e) { console.error("deferred timer error:", e); } }); };',
      'var _origSetTimeout = globalThis.setTimeout; globalThis.setTimeout = function() { if (!__timersReady) { var args = arguments; var fn = args[0]; __deferredTimers.push({fn: function(){ _origSetTimeout.apply(globalThis, args); }, args: []}); return { unref: function(){}, ref: function(){}, [Symbol.toPrimitive]: function(){ return 0; } }; } var id = _origSetTimeout.apply(this, arguments); if (typeof id === "number") { return { unref: function(){}, ref: function(){}, [Symbol.toPrimitive]: function(){ return id; } }; } if (id && !id.unref) id.unref = function(){}; return id; };',
      'var _origSetInterval = globalThis.setInterval; globalThis.setInterval = function() { if (!__timersReady) { var args = arguments; __deferredTimers.push({fn: function(){ _origSetInterval.apply(globalThis, args); }, args: []}); return { unref: function(){}, ref: function(){}, [Symbol.toPrimitive]: function(){ return 0; } }; } var id = _origSetInterval.apply(this, arguments); if (typeof id === "number") { return { unref: function(){}, ref: function(){}, [Symbol.toPrimitive]: function(){ return id; } }; } if (id && !id.unref) id.unref = function(){}; return id; };',
      'var _origSetImmediate = globalThis.setImmediate; if (_origSetImmediate) { globalThis.setImmediate = function() { if (!__timersReady) { var args = arguments; __deferredTimers.push({fn: function(){ _origSetImmediate.apply(globalThis, args); }, args: []}); return { unref: function(){}, ref: function(){} }; } return _origSetImmediate.apply(this, arguments); }; } else { globalThis.setImmediate = function(fn) { if (!__timersReady) { __deferredTimers.push({fn: function(){ _origSetTimeout(fn, 0); }, args: []}); return { unref: function(){}, ref: function(){} }; } return _origSetTimeout(fn, 0); }; }',
      'if (typeof process !== "undefined") { var _origNextTick = process.nextTick; if (_origNextTick) { process.nextTick = function() { if (!__timersReady) { var args = Array.prototype.slice.call(arguments); var fn = args.shift(); __deferredTimers.push({fn: function(){ _origNextTick.apply(process, [fn].concat(args)); }, args: []}); return; } return _origNextTick.apply(process, arguments); }; } else { process.nextTick = function(fn) { if (!__timersReady) { __deferredTimers.push({fn: function(){ _origSetTimeout(fn, 0); }, args: []}); return; } _origSetTimeout(fn, 0); }; } }',
    ].join('\n'),
  },
  alias: {
    // axios → lightweight fetch-based shim (115KB → ~2KB)
    "axios": s("shims/axios-lite.ts"),

    // node-fetch → native fetch (drops encoding/tr46/whatwg-url polyfill ~754KB
    // pulled in by @apple/app-store-server-library; dead weight on CF Workers)
    "node-fetch": s("shims/node-fetch.ts"),

    // Stripe — wrap constructor to use fetch HTTP client in CF Workers
    "stripe": s("shims/stripe-cf.ts"),
    "__real_stripe__": require.resolve("stripe"),

    // @ocap/message ships a patch.mjs that monkey-patches google-protobuf at
    // module init. google-protobuf isn't available in CF Workers (no filesystem
    // for createRequire to resolve), and Payment Kit doesn't use the features
    // that patch touches. Replace with a noop so the import chain stays alive
    // but the patch is skipped. Same for the rolldown_runtime helper that
    // relies on createRequire(import.meta.url).
    "@ocap/message/esm/patch.mjs": s("shims/noop.ts"),
    "@ocap/message/esm/_virtual/rolldown_runtime.mjs": s("shims/noop.ts"),

    "sequelize": s("shims/sequelize-d1/index.ts"),
    "sqlite3": s("shims/noop.ts"),
    "cls-hooked": s("shims/noop.ts"),
    "express-async-errors": s("shims/noop.ts"),
    "express": s("shims/express-compat/index.ts"),
    "cors": s("shims/cors.ts"),
    "cookie-parser": s("shims/cookie-parser.ts"),
    "@blocklet/sdk/lib/middlewares/fallback": s("shims/blocklet-sdk/fallback.ts"),
    "@blocklet/sdk/lib/middlewares/cdn": s("shims/blocklet-sdk/cdn.ts"),
    "@blocklet/sdk/lib/middlewares/session": s("shims/blocklet-sdk/session.ts"),
    "@blocklet/sdk/lib/middlewares": s("shims/blocklet-sdk/middlewares.ts"),
    "@blocklet/sdk/lib/env": s("shims/blocklet-sdk/env.ts"),
    "@blocklet/sdk/lib/config": s("shims/blocklet-sdk/config.ts"),
    "@blocklet/sdk/lib/component": s("shims/blocklet-sdk/component.ts"),
    "@blocklet/sdk/lib/wallet": s("shims/blocklet-sdk/wallet.ts"),
    "@blocklet/sdk/lib/wallet-authenticator": s("shims/blocklet-sdk/wallet-authenticator.ts"),
    "@blocklet/sdk/lib/wallet-handler": s("shims/blocklet-sdk/wallet-handler.ts"),
    "@blocklet/sdk/lib/security": s("shims/blocklet-sdk/security.ts"),
    "@blocklet/sdk/lib/util/verify-sign": s("shims/blocklet-sdk/verify-sign.ts"),
    "@blocklet/sdk/lib/util/verify-session": s("shims/blocklet-sdk/verify-session.ts"),
    "@blocklet/sdk/lib/util/component-api": s("shims/blocklet-sdk/component-api.ts"),
    "@blocklet/sdk/lib/error-handler": s("shims/noop.ts"),
    "@blocklet/sdk/lib/did": s("shims/blocklet-sdk/did.ts"),
    "@blocklet/sdk/lib/types/notification": s("shims/noop.ts"),
    "@blocklet/sdk/service/notification": s("shims/blocklet-sdk/notification.ts"),
    "@blocklet/sdk/service/eventbus": s("shims/blocklet-sdk/eventbus.ts"),
    "@blocklet/sdk/service/auth": s("shims/blocklet-sdk/auth-service.ts"),
    "@blocklet/sdk/service/blocklet": s("shims/blocklet-sdk/auth-service.ts"),
    "@blocklet/sdk": s("shims/blocklet-sdk/index.ts"),
    "@blocklet/xss": s("shims/xss.ts"),
    "@blocklet/error": s("shims/error.ts"),
    "@blocklet/logger": s("shims/blocklet-sdk/logger.ts"),
    "@blocklet/did-space-js": s("shims/did-space.ts"),
    "@blocklet/payment-vendor": s("shims/payment-vendor.ts"),
    "@arcblock/did-connect-storage-nedb": s("shims/nedb-storage.ts"),
    "@abtnode/cron": s("shims/cron.ts"),
    "dotenv-flow": s("shims/noop.ts"),
    "fastq": s("shims/fastq.ts"),

    // Bundle size optimizations — lodash base import aliased to lodash-es for tree-shaking.
    // Sub-path imports (lodash/camelCase etc.) handled by lodashSubpathPlugin below.
    "lodash": "lodash-es",                   // 357KB CJS → tree-shakeable ESM
    "esprima": s("shims/noop.ts"),           // 215KB — only used by @ocap/contract, not called in payment-kit
    "mustache": s("shims/noop.ts"),          //  16KB — template engine, not used in payment-kit
    "mime-types": s("shims/mime-types.ts"),   // 150KB — lightweight shim with common MIME types
    "mime-db": s("shims/noop.ts"),
    "ws": s("shims/ws-lite.ts"),             //  66KB — native WebSocket wrapper for CF Workers
    "crypto-js": s("shims/crypto-js-warn.ts"),  // 115KB — AES legacy not used, warns if called
    "crypto-js/aes": s("shims/crypto-js-warn.ts"),
    "crypto-js/enc-base64": s("shims/noop.ts"),
    "crypto-js/enc-hex": s("shims/noop.ts"),
    "crypto-js/enc-latin1": s("shims/noop.ts"),
    "crypto-js/enc-utf8": s("shims/noop.ts"),
    "crypto-js/enc-utf16": s("shims/noop.ts"),

    // Force ESM-only to avoid CJS+ESM double-bundling
    "valibot": path.resolve(__dirname, "../../..", "node_modules/valibot/dist/index.mjs"), // 396KB → ~200KB
    // CF Workers: noop modules not useful in Workers environment
    "phoenix": s("shims/noop.ts"),                // 36KB — Phoenix channels, only used by @arcblock/ws
    "numbro": s("shims/noop.ts"),                 // 49KB — number formatting, replaced inline in util.ts

    // Node built-in shims (not fully supported in CF Workers nodejs_compat)
    "os": s("shims/node-os.ts"),
    "node:os": s("shims/node-os.ts"),
    "tty": s("shims/node-tty.ts"),
    "node:tty": s("shims/node-tty.ts"),
    "http": s("shims/node-http.ts"),
    "node:http": s("shims/node-http.ts"),
    "https": s("shims/node-https.ts"),
    "node:https": s("shims/node-https.ts"),
    "http2": s("shims/node-http.ts"),
    "node:http2": s("shims/node-http.ts"),
    "net": s("shims/node-net.ts"),
    "node:net": s("shims/node-net.ts"),
    "tls": s("shims/node-misc.ts"),
    "node:tls": s("shims/node-misc.ts"),
    "fs": s("shims/node-fs.ts"),
    "node:fs": s("shims/node-fs.ts"),
    "cluster": s("shims/node-misc.ts"),
    "node:cluster": s("shims/node-misc.ts"),
    "zlib": s("shims/node-zlib.ts"),
    "node:zlib": s("shims/node-zlib.ts"),
    "child_process": s("shims/node-child-process.ts"),
    "node:child_process": s("shims/node-child-process.ts"),
    "bufferutil": s("shims/noop.ts"),
    "utf-8-validate": s("shims/noop.ts"),
    "dns": s("shims/noop.ts"),
    "node:dns": s("shims/noop.ts"),
    "pg": s("shims/noop.ts"),
    "postgres": s("shims/noop.ts"),
  },
  logLevel: "warning",
}).then(result => {
  require('fs').writeFileSync(s('dist/meta.json'), JSON.stringify(result.metafile));
  Object.entries(result.metafile.outputs).forEach(function(entry) {
    var k = entry[0], v = entry[1];
    if (k.indexOf(".map") === -1) {
      console.log(k + ": " + (v.bytes / 1024).toFixed(1) + "KB");
    }
  });
  console.log("BUILD SUCCESS");
}).catch(err => {
  console.error("BUILD FAILED: " + (err.errors ? err.errors.length + " errors" : err.message));
  if (err.errors) {
    err.errors.slice(0, 20).forEach(function(e) {
      var loc = e.location ? " at " + e.location.file + ":" + e.location.line : "";
      console.error("  " + e.text + loc);
    });
    if (err.errors.length > 20) console.error("  ... and " + (err.errors.length - 20) + " more");
  }
});
