// analytics.js — v0.15.0 / roadmap item #13.
//
// Wrapper around `@amplitude/unified` (Amplitude Analytics +
// Session Replay) that gates SDK initialization on the user's
// cookie/privacy consent (v0.13.0, `frontend/src/lib/consent.js`,
// SPEC §14.5). The wrapper presents a stable surface to the rest
// of the app:
//
//     import { track, identify, anonymize } from './lib/analytics'
//
//     track('RFC Viewed', { rfc_slug: 'open-human-model' })
//     identify({ user_id: 'u_123' })
//     anonymize()  // call on sign-out
//
// At first import the wrapper:
//   1. Calls `bootstrap()` once, which reads `getConsent()` and
//      subscribes to `onConsentChange()`. If consent.analytics is
//      true, it lazily imports the Amplitude SDK and calls
//      `amplitude.initAll(API_KEY, { analytics: { autocapture: true },
//                                     sessionReplay: { sampleRate: 1 } })`.
//      If consent.analytics is false (or undecided), the SDK is
//      not loaded — no network request, no cookies, no session
//      replay recording. A later consent change to `true` triggers
//      init at that moment.
//   2. The wrapper queues `track()` and `identify()` calls made
//      before init finishes (lazy import + consent grant), and
//      drains the queue when init completes.
//   3. If the user later flips consent from granted → denied, the
//      wrapper calls `amplitude.setOptOut(true)` so subsequent
//      events are dropped client-side and session replay stops
//      recording (the SDK is still loaded — we cannot unload a
//      script — but it stops firing).
//
// Consent precedence ladder:
//
//     consent.analytics === true   → init + track
//     consent.analytics === false  → no init; or if already init,
//                                    setOptOut(true)
//     consent.recorded_at === null → treat as denied (banner is up;
//                                    the user has not yet chosen)
//
// Session replay scope: this release ships session replay at
// `sampleRate: 1` (100% of sessions are recorded for full-DOM
// playback). That is the vendor-recommended default for new
// Amplitude deployments. The v0.13.0 consent banner's single
// "analytics" toggle gates both events and session replay together —
// a separate consent category for session-replay specifically is a
// §19.2 follow-up.
//
// API key resolution:
//
//     The build-time env var `VITE_AMPLITUDE_API_KEY` carries the
//     Amplitude project's API key. When it is unset/empty, the
//     wrapper logs one console warning and no-ops — every public
//     function becomes a deterministic no-op so dev environments
//     (and deployments that intentionally don't ship analytics)
//     keep working. The deploy gesture wires the key via flotilla's
//     `overlay set` verb (see CHANGELOG for the operator gesture):
//     Amplitude browser keys are bundle-embedded by design (visible
//     to anyone with dev tools, same nature as the v0.12.0
//     `VITE_TURNSTILE_SITE_KEY`), so the binding is overlay, not
//     secret.
//
// PII discipline:
//
//     `identify({ user_id })` SHOULD pass only the opaque server-
//     side user id (the `viewer.id` integer or string). DO NOT pass
//     email, display name, IP, or any other PII through the SDK.
//     Event properties SHOULD likewise stay limited to ids and
//     enums; free-text fields (titles, comment bodies) MUST NOT be
//     sent.
//
// Event taxonomy: defined in `EVENTS` below. Callers SHOULD use
// one of these names rather than firing arbitrary strings — that
// keeps the Amplitude dashboard coherent over time.

import { getConsent, onConsentChange } from './consent.js'

const API_KEY = import.meta.env.VITE_AMPLITUDE_API_KEY || ''

// Public taxonomy. Keep this short and stable — new entries should
// land via a release, not ad-hoc. The strings match the Amplitude
// dashboard names exactly (Title Case, spaces, no punctuation).
export const EVENTS = Object.freeze({
  PAGE_VIEWED: 'Page Viewed',
  RFC_VIEWED: 'RFC Viewed',
  USER_SIGNED_IN: 'User Signed In',
  USER_SIGNED_OUT: 'User Signed Out',
  RFC_PROPOSED: 'RFC Proposed',
  PR_OPENED: 'PR Opened',
  COMMENT_POSTED: 'Comment Posted',
  BETA_ACCESS_REQUESTED: 'Beta Access Requested',
  ADMIN_PERMISSION_DECISION: 'Admin Permission Decision',
  // v0.16.0 / item #12 — per-RFC owner invites.
  INVITATION_SENT: 'Invitation Sent',
  INVITATION_ACCEPTED: 'Invitation Accepted',
  // v0.17.0 / item #16 — admin-create user + invite email.
  USER_INVITED: 'User Invited',
  INVITE_CLAIMED: 'Invite Claimed',
  // v0.19.0 / item #30 — `/docs/*` flyout + sessions browser. Carries
  // `section`: 'user-guide' | 'sessions/about' | 'sessions/<NNNN>' |
  // 'sessions/<NNNN>/<filename>' so the dashboard can answer which
  // docs surfaces get read most. The transcript-section value includes
  // the filename so an aggregator can group by `sessions/<NNNN>` or by
  // exact transcript.
  DOC_VIEWED: 'Doc Viewed',
})

// Internal state.
let _bootstrapped = false
let _amplitude = null       // The dynamically imported SDK module.
let _initPromise = null     // Pending init (lazy import + sdk.init).
let _initialized = false    // True after sdk.init has resolved.
let _warnedNoKey = false
let _pendingUserId = null   // identify() called before init resolves.
let _pendingProperties = null  // identify({ properties }) or
                               // setUserProperties() before init.
const _queue = []           // {kind: 'track'|'identify'|'anonymize'|
                            //  'setUserProperties', ...}

function warnNoKey() {
  if (_warnedNoKey) return
  _warnedNoKey = true
  // eslint-disable-next-line no-console
  console.warn(
    '[analytics] VITE_AMPLITUDE_API_KEY is unset; analytics events ' +
    'and session replay will not be sent. This is expected in dev; ' +
    'in production it means the operator has not yet run ' +
    '`flotilla overlay set <deployment> VITE_AMPLITUDE_API_KEY=<key>`.',
  )
}

function consentGranted() {
  const c = getConsent()
  return !!(c && c.recorded_at && c.analytics)
}

// Apply a {key: value} property bag as an Amplitude Identify event.
// Used by both `identify({ properties })` and `setUserProperties`.
function applyProperties(props) {
  if (!_initialized || !_amplitude || !props) return
  try {
    const id = new _amplitude.Identify()
    for (const [k, v] of Object.entries(props)) {
      if (v === undefined || v === null) continue
      if (Array.isArray(v) && v.length === 2 && v[0] === '__setOnce__') {
        id.setOnce(k, v[1])
      } else {
        id.set(k, v)
      }
    }
    _amplitude.identify(id)
  } catch (_) {
    // SDK errors are non-fatal; analytics is best-effort.
  }
}

// Drain the queue. Called once init resolves.
function drainQueue() {
  if (!_initialized || !_amplitude) return
  if (_pendingUserId != null) {
    try { _amplitude.setUserId(_pendingUserId) } catch (_) {}
    _pendingUserId = null
  }
  if (_pendingProperties != null) {
    applyProperties(_pendingProperties)
    _pendingProperties = null
  }
  while (_queue.length > 0) {
    const item = _queue.shift()
    try {
      if (item.kind === 'track') {
        _amplitude.track(item.name, item.props || {})
      } else if (item.kind === 'identify') {
        if (item.user_id != null) _amplitude.setUserId(item.user_id)
        if (item.properties != null) applyProperties(item.properties)
      } else if (item.kind === 'setUserProperties') {
        applyProperties(item.properties)
      } else if (item.kind === 'anonymize') {
        _amplitude.reset()
      }
    } catch (_) {
      // SDK errors are non-fatal; analytics is best-effort.
    }
  }
}

// Lazy import + init. Resolves once the SDK is ready to take events.
// Idempotent: subsequent calls return the same promise.
async function initSdk() {
  if (_initPromise) return _initPromise
  if (!API_KEY) {
    warnNoKey()
    // Resolve immediately with a no-op shape; the wrapper's public
    // functions check API_KEY and short-circuit, so this never
    // actually runs SDK code.
    _initPromise = Promise.resolve(null)
    return _initPromise
  }
  _initPromise = (async () => {
    try {
      const mod = await import('@amplitude/unified')
      // The unified package exposes `initAll`, `track`,
      // `setUserId`, `reset`, `setOptOut` as named functions.
      // We hold the module so the queue drainer can call them
      // by name.
      _amplitude = mod
      // initAll wires up both Analytics and Session Replay in one
      // call. Vendor-recommended init shape from the Amplitude
      // installation wizard:
      //   - analytics.autocapture: true — auto-instruments page
      //     views, session start/end, clicks, and form interactions.
      //     Our explicit `track('Page Viewed', …)` etc. layer on top
      //     for app-specific names that survive renames.
      //   - sessionReplay.sampleRate: 1 — record 100% of sessions
      //     for full-DOM playback. Gated by the v0.13.0 consent
      //     banner just like the rest of the SDK; never starts
      //     recording without explicit analytics opt-in.
      const ret = mod.initAll(API_KEY, {
        analytics: { autocapture: true },
        sessionReplay: { sampleRate: 1 },
      })
      // initAll returns an AmplitudeReturn with a `.promise` accessor
      // (consistent with the legacy `init`). Some unified builds
      // resolve synchronously; await defensively.
      if (ret && ret.promise) await ret.promise
      _initialized = true
      drainQueue()
    } catch (err) {
      // Init failure is non-fatal; keep the wrapper alive so future
      // calls no-op. Log once for the operator.
      // eslint-disable-next-line no-console
      console.warn('[analytics] Amplitude init failed:', err)
      _initialized = false
    }
    return _amplitude
  })()
  return _initPromise
}

// Bootstrap is called lazily on first track/identify. It wires the
// consent subscription so a later flip from denied→granted triggers
// init at that moment, and granted→denied flips the opt-out.
function bootstrap() {
  if (_bootstrapped) return
  _bootstrapped = true
  if (consentGranted()) {
    // Fire-and-forget; the queue catches any events that arrive
    // before init resolves.
    initSdk()
  }
  onConsentChange(snapshot => {
    const allowed = !!(snapshot && snapshot.recorded_at && snapshot.analytics)
    if (allowed && !_initPromise) {
      initSdk()
    } else if (allowed && _initialized && _amplitude) {
      // Re-enable in case we previously opted out.
      try { _amplitude.setOptOut(false) } catch (_) {}
    } else if (!allowed && _initialized && _amplitude) {
      // Granted → denied. Stop firing. We cannot unload the script
      // tag; setOptOut is the SDK's contract for "drop subsequent
      // events client-side".
      try { _amplitude.setOptOut(true) } catch (_) {}
    }
  })
}

/** Fire a track event. Safe to call before consent / init resolve;
 *  the call is queued and drained once both are true. Drops the
 *  event silently if API_KEY is empty (with a one-shot warn) or
 *  consent.analytics is false. */
export function track(name, props) {
  if (!API_KEY) { warnNoKey(); return }
  bootstrap()
  if (!consentGranted()) return
  if (_initialized && _amplitude) {
    try { _amplitude.track(name, props || {}) } catch (_) {}
    return
  }
  _queue.push({ kind: 'track', name, props })
}

/** Attach an authenticated user id and optional durable properties.
 *  Pass `{ user_id: '<opaque-id>', properties?: { role, first_sign_in_at, … } }`.
 *  DO NOT pass email, display name, or other PII as user_id or in
 *  properties. Idempotent — subsequent calls with the same id are
 *  cheap; properties are merged into the Amplitude user record.
 *
 *  To mark a property as setOnce (immutable after first write),
 *  pass `properties: { first_sign_in_at: ['__setOnce__', '2026-05-28T…'] }`.
 *  Bare values use Amplitude's `.set()` (mutable).
 *
 *  Pattern (per #21 Part C):
 *    - On sign-in success in App.jsx: identify with viewer.id + the
 *      durable property bag (role, permission_state, first_sign_in_at
 *      setOnce, passcode_set, device_trusted_count, account_created_at
 *      setOnce).
 *    - On invite-claim success in InviteClaim.jsx / AcceptInvitation.jsx:
 *      identify with the new viewer.id + invitation-derived properties
 *      (invited_by_admin_id, invited_at setOnce, initial_role, claim_method)
 *      BEFORE firing any track() — so the Amplitude user record is
 *      created with the OHM user_id from the first event, not as an
 *      anonymous device that retroactively links. */
export function identify({ user_id, properties } = {}) {
  if (!API_KEY) { warnNoKey(); return }
  if (user_id == null && properties == null) return
  bootstrap()
  if (!consentGranted()) {
    // Hold for when consent lands; identify-on-sign-in is a common
    // race with the consent banner choice.
    if (user_id != null) _pendingUserId = user_id
    if (properties != null) {
      _pendingProperties = { ..._pendingProperties, ...properties }
    }
    return
  }
  if (_initialized && _amplitude) {
    try {
      if (user_id != null) _amplitude.setUserId(user_id)
      if (properties != null) applyProperties(properties)
    } catch (_) {}
    return
  }
  if (user_id != null) _pendingUserId = user_id
  if (properties != null) {
    _pendingProperties = { ..._pendingProperties, ...properties }
  }
  _queue.push({ kind: 'identify', user_id, properties })
}

/** Update durable user properties on the current Amplitude user
 *  record mid-session — for state changes that shouldn't wait for the
 *  next sign-in to surface (role grant/revoke, passcode set, device
 *  trusted, etc.). Same property shape as `identify({ properties })`.
 *  setOnce values use the `['__setOnce__', value]` sentinel pattern.
 *  Has no effect if no identify has happened yet — set the user_id
 *  via `identify()` first.
 *
 *  Per #21 Part C: call this from any surface where the user's
 *  Amplitude-relevant state changes mid-session, so the dashboard
 *  stays current. */
export function setUserProperties(properties) {
  if (!API_KEY) { warnNoKey(); return }
  if (properties == null) return
  bootstrap()
  if (!consentGranted()) {
    _pendingProperties = { ..._pendingProperties, ...properties }
    return
  }
  if (_initialized && _amplitude) {
    applyProperties(properties)
    return
  }
  _pendingProperties = { ..._pendingProperties, ...properties }
  _queue.push({ kind: 'setUserProperties', properties })
}

/** Reset the user binding. Call this on sign-out so the next page
 *  navigations are attributed to a fresh anonymous device id. Has
 *  no effect when analytics is disabled.
 *
 *  Per #21 Part C: clears both the user_id binding AND the pending
 *  property cache, so a subsequent sign-in as a different user
 *  starts with a fully fresh slate (no carry-over properties from
 *  the previous user). */
export function anonymize() {
  if (!API_KEY) { warnNoKey(); return }
  _pendingUserId = null
  _pendingProperties = null
  bootstrap()
  if (!consentGranted()) return
  if (_initialized && _amplitude) {
    try { _amplitude.reset() } catch (_) {}
    return
  }
  _queue.push({ kind: 'anonymize' })
}

/** Test helper — exposed for unit tests, not for app code.
 *  Resets module-level state so a fresh bootstrap cycle can be
 *  exercised. */
export function __resetForTests() {
  _bootstrapped = false
  _amplitude = null
  _initPromise = null
  _initialized = false
  _warnedNoKey = false
  _pendingUserId = null
  _pendingProperties = null
  _queue.length = 0
}
