/**
 * Zoff Wallet — page-world injected provider.
 *
 * Runs in the page's main world (not the content script's isolated world)
 * so it can attach `window.cantonWallet` for dApps to discover and call.
 *
 * Surface: CASM v3 §S23 — `connect/disconnect/isConnected/getPartyId/
 * getBalances/signAndSubmit/on/off`. **Not** the CIP-103 `canton_*`
 * JSON-RPC envelope the original Session 3 prompt guessed: the
 * `demo-dapp/app.js` and `canton-wallet-CASM-v3.md` both confirm this is
 * the real surface, and the lessons file's #1 rule is "trust code, not
 * the prompt".
 *
 * Transport: every method call posts a `{target:'zoff-content', reqId,
 * method, params}` envelope to `window`. The content script (`content.js`,
 * isolated world) listens, forwards to the background SW via
 * `chrome.runtime.sendMessage`, awaits the SW's reply, and posts a
 * `{target:'zoff-injected', reqId, result | error}` back. We correlate
 * by `reqId` so concurrent requests don't collide.
 *
 * Push events (`accountChanged`, `disconnect`) flow the other way:
 * SW → tab → page world via `{target:'zoff-injected-event', event,
 * payload}`.
 *
 * Plain JS (page-world script can't import). The TS-side mirrors of
 * these envelope shapes live in `frontend/lib/extension-rpc.ts`.
 */

;(function () {
  if (window.__zoffWalletAnnounced) return
  window.__zoffWalletAnnounced = true

  const VERSION = '0.3.0'

  // ── reqId generator ─────────────────────────────────────────────────────
  function makeReqId() {
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
      return crypto.randomUUID()
    }
    return (
      'zoff-' +
      Math.random().toString(36).slice(2) +
      '-' +
      Date.now().toString(36)
    )
  }

  // ── pending request map ─────────────────────────────────────────────────
  // reqId → { resolve, reject }. Cleaned up when the matching response
  // arrives, or after a 5-minute timeout (anything longer means the
  // approval popup was abandoned and the SW should have already cleaned
  // up the pending request on its side).
  const pending = new Map()
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000

  function postRequest(method, params) {
    return new Promise((resolve, reject) => {
      const reqId = makeReqId()
      const timeout = setTimeout(() => {
        if (pending.has(reqId)) {
          pending.delete(reqId)
          reject(
            Object.assign(new Error('Zoff request timed out'), {
              code: -32603,
            }),
          )
        }
      }, REQUEST_TIMEOUT_MS)

      pending.set(reqId, {
        resolve: (value) => {
          clearTimeout(timeout)
          resolve(value)
        },
        reject: (err) => {
          clearTimeout(timeout)
          reject(err)
        },
      })

      window.postMessage(
        { target: 'zoff-content', reqId, method, params },
        location.origin,
      )
    })
  }

  // ── response listener ───────────────────────────────────────────────────
  window.addEventListener('message', (event) => {
    if (event.source !== window) return
    const data = event.data
    if (!data || typeof data !== 'object') return

    if (data.target === 'zoff-injected') {
      const entry = pending.get(data.reqId)
      if (!entry) return
      pending.delete(data.reqId)
      if (data.error) {
        const err = new Error(data.error.message || 'Zoff error')
        err.code = data.error.code
        if (data.error.data !== undefined) err.data = data.error.data
        entry.reject(err)
      } else {
        entry.resolve(data.result)
      }
      return
    }

    if (data.target === 'zoff-injected-event') {
      emit(data.event, data.payload)
      return
    }
  })

  // ── tiny EventEmitter for `on/off` ─────────────────────────────────────
  const listeners = new Map()
  function on(event, handler) {
    if (typeof handler !== 'function') return
    if (!listeners.has(event)) listeners.set(event, new Set())
    listeners.get(event).add(handler)
  }
  function off(event, handler) {
    const set = listeners.get(event)
    if (!set) return
    set.delete(handler)
  }
  function emit(event, payload) {
    const set = listeners.get(event)
    if (!set) return
    for (const handler of Array.from(set)) {
      try {
        handler(payload)
      } catch (err) {
        console.error('[Zoff] Listener for', event, 'threw:', err)
      }
    }
  }

  // ── public provider surface ────────────────────────────────────────────
  // Cached connection state. The SW is the source of truth, but caching
  // here avoids round-trips for the synchronous `isConnected()` check
  // that some dApps poll.
  let cachedPartyId = null
  let cachedConnected = false

  const provider = {
    isCantonWallet: true,
    isZoffWallet: true,
    version: VERSION,

    /** CASM v3 §S23 connect — opens approval popup on first call per origin. */
    async connect(opts) {
      const result = await postRequest('connect', opts || {})
      if (result && typeof result === 'object' && result.partyId) {
        cachedPartyId = result.partyId
        cachedConnected = true
      }
      return result
    },

    disconnect() {
      cachedPartyId = null
      cachedConnected = false
      // Fire-and-forget — disconnect should never block the dApp.
      postRequest('disconnect', {}).catch(() => {})
      emit('disconnect', undefined)
    },

    isConnected() {
      return cachedConnected
    },

    async getPartyId() {
      if (cachedPartyId) return cachedPartyId
      const result = await postRequest('getPartyId', {})
      if (typeof result === 'string') {
        cachedPartyId = result
        cachedConnected = true
      }
      return result
    },

    async getBalances() {
      return postRequest('getBalances', {})
    },

    async signAndSubmit(transaction) {
      return postRequest('signAndSubmit', { transaction })
    },

    on,
    off,
    // Aliases for dApps that use the EventEmitter-style add/remove names.
    addListener: on,
    removeListener: off,

    // Provider info for EIP-6963-style multi-wallet discoverability.
    info: {
      uuid: 'zoff-wallet-v1',
      name: 'Zoff Wallet',
      icon: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="%23F4E4C1"/><text x="32" y="42" text-anchor="middle" font-family="serif" font-size="36" fill="%23553311">Z</text></svg>',
    },
  }

  // ── attach to window ────────────────────────────────────────────────────
  // CASM v3 names this `cantonWallet`. We also keep `canton` as a soft
  // alias for the Session 1 stub (some early experiments looked there)
  // but only if nothing else has claimed it.
  if (!window.cantonWallet) {
    Object.defineProperty(window, 'cantonWallet', {
      value: provider,
      writable: false,
      configurable: false,
    })
  }
  if (!window.canton) {
    try {
      Object.defineProperty(window, 'canton', {
        value: provider,
        writable: false,
        configurable: false,
      })
    } catch {
      // If a previous Session 1 stub already locked down `window.canton`
      // we silently skip — `cantonWallet` is the canonical name anyway.
    }
  }

  // EIP-6963-style announcement so dApps can listen instead of polling.
  window.dispatchEvent(
    new CustomEvent('canton:announceProvider', {
      detail: { info: provider.info, provider },
    }),
  )
  window.addEventListener('canton:requestProvider', () => {
    window.dispatchEvent(
      new CustomEvent('canton:announceProvider', {
        detail: { info: provider.info, provider },
      }),
    )
  })

  console.log('[Zoff] window.cantonWallet ready (v' + VERSION + ')')
})()
