/* global React, sbx, chunk, money, useDemoMode, rngFor */
// The admin audit trail.
//
// Two people share full admin rights here, and the portal can set someone's
// password, delete an account outright, move money and hand out course keys.
// Until now only set-password left any record, so "who deleted that account
// on Tuesday" had no answer at all.
//
// The write is what matters and the write is what cannot be backfilled — an
// action that goes unrecorded today is gone for good, whether or not a screen
// exists to read it back. So the recording lands first; reading it is a
// separate job.
//
// ─── What this is, and is not ────────────────────────────────────────
// These rows are written from the browser, by the admin doing the thing. The
// `admin_audit_insert` policy is `CHECK (auth.uid() = actor_id)`, so nobody
// can forge a row as somebody else — but an admin who talked to the API
// directly could simply not write one. That makes this an honest record of
// what the portal did, not tamper-proof evidence against the person using it.
// For two trusted admins asking "what happened here", that is the useful
// thing. set-password's row is written server-side and is the stronger kind.
//
// Deliberately NOT logged: tee-slot edits, yardages, pin placements, course
// detail tweaks. They are high-volume and low-consequence, and a log that is
// mostly noise is one nobody reads.

// Whose action this is. Read from the session rather than passed in, so a
// caller cannot accidentally attribute it to the wrong person.
async function auditActor() {
  try {
    const { data } = await sbx.auth.getSession();
    return (data && data.session && data.session.user && data.session.user.id) || null;
  } catch (_) { return null; }
}

// Record one admin action.
//
// Never throws and never rejects. A failed audit write must not turn a
// successful deletion into what looks like a failed one — the worst outcome
// here is a missing row, and the worst outcome of the alternative is an admin
// retrying a destructive action that already worked.
async function audit(action, entity, entityId, before, after) {
  try {
    const actor_id = await auditActor();
    if (!actor_id) return;
    await sbx.from('admin_audit').insert({
      actor_id,
      action,
      entity,
      entity_id: entityId == null ? null : String(entityId),
      before: before === undefined ? null : before,
      after: after === undefined ? null : after,
    });
  } catch (e) {
    // Surfaced for a developer, invisible to the admin mid-task.
    // eslint-disable-next-line no-console
    console.warn('audit write failed', action, entity, entityId, e && e.message);
  }
}

// Strip a row down to the fields worth keeping a copy of. Storing whole rows
// would put avatars, bios and other personal data into a table that outlives
// the account it describes — the point is to identify what changed, not to
// hold a second copy of the record.
function auditSnapshot(row, keys) {
  if (!row) return null;
  const out = {};
  keys.forEach(k => { if (row[k] !== undefined) out[k] = row[k]; });
  return out;
}

Object.assign(window, { audit, auditSnapshot });

// ─── Reading it back ──────────────────────────────────────────────────
// The log is only worth writing if somebody looks at it, and nobody opens a
// SQL editor to check a hunch. So the rows come back resolved and phrased
// rather than as raw jsonb.

const AUDIT_PAGE = 60;

// Every action the portal records, in the order they appear in the filter.
// `destructive` drives the shape a row is drawn with — losing something is a
// different kind of event from changing it.
const AUDIT_ACTIONS = {
  account_delete:      { label: 'Account deleted',   destructive: true },
  admin_grant:         { label: 'Admin granted',     destructive: false },
  admin_revoke:        { label: 'Admin revoked',     destructive: true },
  set_password:        { label: 'Password set',      destructive: false },
  tier_change:         { label: 'Tier changed',      destructive: false },
  guest_pass_grant:    { label: 'Guest pass given',  destructive: false },
  guest_pass_revoke:   { label: 'Guest pass pulled', destructive: true },
  course_access_grant: { label: 'Course access',     destructive: false },
  course_access_revoke:{ label: 'Access removed',    destructive: true },
  payout_status:       { label: 'Payout status',     destructive: false },
  payout_ledger_build: { label: 'Ledger rebuilt',    destructive: false },
  match_edit:          { label: 'Match edited',      destructive: false },
  match_delete:        { label: 'Match deleted',     destructive: true },
  match_bulk_delete:   { label: 'Matches deleted',    destructive: true },
  booking_delete:      { label: 'Booking deleted',   destructive: true },
  event_delete:        { label: 'Event deleted',     destructive: true },
};

const auditName = (p) => {
  if (!p) return null;
  const full = [p.first_name, p.last_name].filter(Boolean).join(' ').trim();
  return full || (p.handle ? `@${String(p.handle).replace(/^@/, '')}` : null);
};

// One row as a sentence. The snapshots were written with exactly this in
// mind — they carry a course name, a handle, a margin — so most rows can
// describe themselves without a second lookup.
function auditSentence(row, nameFor) {
  const b = row.before || {};
  const a = row.after || {};
  const target = nameFor(row.entity_id)
    || (b.handle ? `@${String(b.handle).replace(/^@/, '')}` : null)
    || auditName(b);

  // Actor and subject are the same person. Since set_membership_tier went in
  // on the golfer-app side, a member joining or cancelling writes its own row
  // here — and the admin phrasing would render it as
  // "Dani Quiles moved Dani Quiles from free to plus", which reads like staff
  // acting on somebody else. Self-service gets its own voice.
  const self = !!(row.actor_id && row.entity_id && row.actor_id === row.entity_id);

  switch (row.action) {
    case 'account_delete':
      return `deleted the account ${target || 'of an unknown golfer'}${b.tier ? ` (${b.tier})` : ''}`;
    case 'admin_grant':   return `made ${target || 'someone'} an admin`;
    case 'admin_revoke':  return `removed admin from ${target || 'someone'}`;
    case 'set_password':  return `set the password for ${target || 'an account'}`;
    case 'tier_change':
      if (self) {
        if (a.tier === 'plus') return 'joined SBX+';
        if (b.tier === 'plus' && a.tier === 'free') return 'cancelled their SBX+ membership';
        return `changed their own membership to ${a.tier || '—'}`;
      }
      return `moved ${target || 'someone'} from ${b.tier || '—'} to ${a.tier || '—'}`;
    case 'guest_pass_grant':
      return `gave ${target || 'someone'} a guest pass${a.note ? ` — ${a.note}` : ''}`;
    case 'guest_pass_revoke': return `pulled a guest pass${target ? ` from ${target}` : ''}`;
    case 'course_access_grant': return `gave ${target || 'someone'} keys to a course`;
    case 'course_access_revoke': return 'removed someone’s course access';
    case 'payout_status':
      return `marked a payout ${a.status || '—'}${b.course_net_cents != null ? ` (${money(Math.round(b.course_net_cents / 100))})` : ''}`;
    case 'payout_ledger_build':
      return `rebuilt the payout ledger for ${a.period || row.entity_id} — ${a.rows || 0} course${a.rows === 1 ? '' : 's'}`;
    case 'match_edit':
      return `edited a match at ${b.course_name || 'a course'}`;
    case 'match_delete':
      return `deleted a ${b.status || ''} match at ${b.course_name || 'a course'}`.replace('  ', ' ');
    case 'match_bulk_delete': {
      const n = b.count || (b.matches || []).length || 0;
      const courses = [...new Set((b.matches || []).map(m => m.course_name).filter(Boolean))];
      const where = courses.length === 1 ? ` at ${courses[0]}`
        : courses.length > 1 ? ` across ${courses.length} courses` : '';
      return `deleted ${n} match${n === 1 ? '' : 'es'}${where}`;
    }
    case 'booking_delete':
      return `deleted a booking${b.price_charged != null ? ` worth ${money(b.price_charged)}` : ''}`;
    case 'event_delete':
      return `deleted the event ${b.course_short || ''}`.trim();
    default:
      return `${row.action} on ${row.entity}`;
  }
}

// ─── useAuditLog ──────────────────────────────────────────────────────
//   null while loading, then { rows, blocked, message, demo, hasMore }
function useAuditLog(pages = 1) {
  const demo = useDemoMode();
  const [state, setState] = React.useState(null);
  const [nonce, setNonce] = React.useState(0);
  const reload = React.useCallback(() => setNonce(n => n + 1), []);

  React.useEffect(() => {
    let live = true;
    setState(null);
    (async () => {
      if (demo) { if (live) setState({ ...demoAudit(), demo: true }); return; }

      const want = AUDIT_PAGE * pages;
      const { data, error } = await sbx.from('admin_audit')
        .select('*').order('created_at', { ascending: false }).limit(want + 1);

      if (error) {
        if (!live) return;
        // Most likely the migration that creates the table hasn't been run,
        // or the read policy is missing. Either way, say which.
        setState({ rows: [], blocked: true, message: error.message, demo: false, hasMore: false });
        return;
      }

      const all = data || [];
      const rows = all.slice(0, want);

      // One lookup for everyone involved — actors, and any entity_id that is
      // itself a person (profiles / auth.users rows share the id).
      const ids = [...new Set([
        ...rows.map(r => r.actor_id),
        ...rows.filter(r => r.entity === 'profiles' || r.entity === 'auth.users').map(r => r.entity_id),
      ].filter(Boolean))];

      const people = {};
      for (const group of chunk(ids, 200)) {
        // eslint-disable-next-line no-await-in-loop
        const { data: profs } = await sbx.from('profiles')
          .select('id, first_name, last_name, handle, avatar_url').in('id', group);
        (profs || []).forEach(p => { people[p.id] = p; });
      }

      if (!live) return;
      setState({
        rows: rows.map(r => ({ ...r, actor: people[r.actor_id] || null, targetProfile: people[r.entity_id] || null })),
        blocked: false, demo: false, hasMore: all.length > want,
      });
    })();
    return () => { live = false; };
  }, [demo, pages, nonce]);

  return [state, reload];
}

// Demo rows, so the screen shows its shape during a walkthrough rather than
// an empty state. Seeded, like the rest of demo mode.
function demoAudit() {
  const r = rngFor('audit', 'demo');
  const actors = [
    { id: 'demo-rob', first_name: 'Rob', last_name: 'P', handle: 'rob' },
    { id: 'demo-dan', first_name: 'Daniel', last_name: 'M', handle: 'manzi' },
  ];
  const specs = [
    ['tier_change', 'profiles', { tier: 'free' }, { tier: 'plus' }],
    ['set_password', 'auth.users', null, { email: 'member@example.com' }],
    ['payout_status', 'payouts', { course_net_cents: 387200, status: 'pending' }, { status: 'paid' }],
    ['match_delete', 'matches', { course_name: 'Melreese', status: 'completed', result: 'A' }, null],
    ['booking_delete', 'bookings', { price_charged: 24, status: 'reserved' }, null],
    ['course_access_grant', 'course_managers', null, { course_id: 'demo' }],
    ['account_delete', 'auth.users', { handle: 'lapsed', tier: 'free' }, null],
    ['admin_grant', 'profiles', null, { is_admin: true }],
  ];
  const rows = specs.map((s, i) => {
    const actor = actors[Math.floor(r() * actors.length)];
    return {
      id: `demo-a${i}`,
      actor_id: actor.id, actor,
      action: s[0], entity: s[1], entity_id: `demo-e${i}`,
      before: s[2], after: s[3],
      targetProfile: { id: `demo-e${i}`, first_name: 'Sample', last_name: 'Golfer', handle: 'sample' },
      created_at: new Date(Date.now() - (i * 3.5 + r() * 2) * 36e5).toISOString(),
      demo: true,
    };
  });
  return { rows, blocked: false, hasMore: false };
}

Object.assign(window, { useAuditLog, AUDIT_ACTIONS, auditSentence, auditName });
