// auto-engine.jsx — Automations rule engine for ASTRO
// ─────────────────────────────────────────────────────────────────
// Pure rule engine. No React. Defines:
//   - Trigger catalog (events the system emits)
//   - Condition operators (filters that gate actions)
//   - Action catalog (side-effects)
//   - Schedule expressions (cron-ish)
//   - Rule storage + activation
//   - Run loop: simulates events, evaluates rules, captures history
//
// All state lives on window.AutoEngine for cross-module access.
// History persists for the session; no backend.
// ─────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined') return;

  // ─── Trigger catalog ───────────────────────────────────────────
  // Each trigger declares: id, label, group, fields the event payload exposes.
  const TRIGGERS = [
    // CWR / registrations
    { id: 'cwr.ack.received',     label: 'CWR ack received',          group: 'Registrations', fields: ['society','workId','status','reason'] },
    { id: 'cwr.ack.rejected',     label: 'CWR ack rejected',          group: 'Registrations', fields: ['society','workId','status','reason'] },
    { id: 'cwr.timeout',          label: 'CWR registration timeout',  group: 'Registrations', fields: ['society','workId','daysOpen'] },
    { id: 'work.registered',      label: 'New work registered',       group: 'Registrations', fields: ['workId','title'] },

    // Royalties / statements
    { id: 'statement.imported',   label: 'Statement imported',        group: 'Royalties',     fields: ['source','period','amount','rows'] },
    { id: 'statement.variance',   label: 'Statement variance flagged',group: 'Royalties',     fields: ['source','expected','actual','varPct'] },
    { id: 'royalty.threshold',    label: 'Royalty crosses threshold', group: 'Royalties',     fields: ['workId','amount','threshold','direction'] },
    { id: 'recoupment.cleared',   label: 'Recoupment cleared',        group: 'Royalties',     fields: ['agreementId','balance'] },

    // Agreements
    { id: 'agreement.signed',     label: 'Agreement signed',          group: 'Agreements',    fields: ['agreementId','counterparty'] },
    { id: 'agreement.expiring',   label: 'Agreement expiring',        group: 'Agreements',    fields: ['agreementId','daysLeft'] },
    { id: 'reversion.due',        label: 'Reversion due',             group: 'Agreements',    fields: ['agreementId','reverter'] },

    // ML signals
    { id: 'leak.detected',        label: 'Royalty leak detected',     group: 'ML signals',    fields: ['workId','kind','severity','impact'] },
    { id: 'bbrank.high',          label: 'BB-rank score > threshold', group: 'ML signals',    fields: ['workId','score','recovery'] },
    { id: 'cover.detected',       label: 'Cover/sample/interp found', group: 'ML signals',    fields: ['workId','platform','recovery'] },

    // DSP
    { id: 'dsp.delivery.failed',  label: 'DSP delivery failed',       group: 'DSP',           fields: ['platform','releaseId','error'] },
    { id: 'dsp.takedown',         label: 'Track removed by DSP',      group: 'DSP',           fields: ['platform','recordingId','reason'] },

    // Catalog / intake
    { id: 'audio.uploaded',       label: 'Audio uploaded',            group: 'Catalog',       fields: ['recordingId','duration'] },
    { id: 'csv.uploaded',         label: 'CSV uploaded',              group: 'Catalog',       fields: ['kind','rows'] },
  ];

  // ─── Action catalog ────────────────────────────────────────────
  const ACTIONS = [
    // Notify
    { id: 'notify.inbox',          label: 'Send to inbox',             group: 'Notify',     params: ['priority'] },
    { id: 'notify.email',          label: 'Email someone',             group: 'Notify',     params: ['to','subject'] },
    { id: 'notify.slack',          label: 'Post to Slack',             group: 'Notify',     params: ['channel'] },

    // Catalog actions
    { id: 'catalog.flag',          label: 'Flag for review',           group: 'Catalog',    params: ['queue','reason'] },
    { id: 'catalog.tag',           label: 'Apply tag',                 group: 'Catalog',    params: ['tag'] },
    { id: 'catalog.assign',        label: 'Assign to person',          group: 'Catalog',    params: ['assignee'] },
    { id: 'fingerprint.run',       label: 'Run audio fingerprint',    group: 'Catalog',    params: [] },

    // Royalty actions
    { id: 'royalty.hold',          label: 'Hold payout',               group: 'Royalties',  params: ['until'] },
    { id: 'royalty.release',       label: 'Release payout',            group: 'Royalties',  params: [] },
    { id: 'statement.match',       label: 'Run statement matcher',     group: 'Royalties',  params: [] },

    // CWR / claims
    { id: 'cwr.regenerate',        label: 'Regenerate CWR',            group: 'Registrations', params: ['version'] },
    { id: 'cwr.resubmit',          label: 'Resubmit to society',       group: 'Registrations', params: ['society'] },
    { id: 'claim.file',            label: 'File claim',                group: 'Claims',     params: ['society'] },
    { id: 'takedown.send',         label: 'Send DMCA takedown',        group: 'Claims',     params: ['platform'] },

    // Documents
    { id: 'doc.generate.statement',label: 'Generate counterparty statement PDF', group: 'Documents', params: ['counterparty'] },
    { id: 'doc.generate.cwr',      label: 'Generate CWR file',         group: 'Documents',  params: [] },

    // Webhooks
    { id: 'webhook.post',          label: 'POST to webhook',           group: 'Integrations', params: ['url'] },
  ];

  // ─── Operators ─────────────────────────────────────────────────
  const OPERATORS = [
    { id: 'eq',     label: '=',           apply: (a, b) => String(a) === String(b) },
    { id: 'neq',    label: '≠',           apply: (a, b) => String(a) !== String(b) },
    { id: 'gt',     label: '>',           apply: (a, b) => Number(a) >  Number(b) },
    { id: 'gte',    label: '≥',           apply: (a, b) => Number(a) >= Number(b) },
    { id: 'lt',     label: '<',           apply: (a, b) => Number(a) <  Number(b) },
    { id: 'lte',    label: '≤',           apply: (a, b) => Number(a) <= Number(b) },
    { id: 'in',     label: 'is one of',   apply: (a, b) => String(b||'').split(',').map(s=>s.trim()).includes(String(a)) },
    { id: 'contains',label:'contains',    apply: (a, b) => String(a||'').toLowerCase().includes(String(b||'').toLowerCase()) },
  ];

  function evalCondition(c, payload) {
    const opDef = OPERATORS.find(o => o.id === c.op);
    if (!opDef) return true;
    return opDef.apply(payload?.[c.field], c.value);
  }

  function evalRule(rule, event) {
    if (rule.trigger !== event.type) return false;
    if (!rule.enabled) return false;
    const conds = rule.conditions || [];
    if (conds.length === 0) return true;
    if (rule.conditionMode === 'any') return conds.some(c => evalCondition(c, event.payload));
    return conds.every(c => evalCondition(c, event.payload));
  }

  // ─── Run history ───────────────────────────────────────────────
  // Each run = { id, ts, ruleId, ruleName, trigger, payload, actions: [{action, params, status, message}], status }
  const RUNS = [];
  function pushRun(rec) {
    RUNS.unshift(rec);
    if (RUNS.length > 500) RUNS.length = 500;
    window.dispatchEvent(new CustomEvent('astro-auto-run', { detail: rec }));
  }

  // ─── Action execution (mocked: returns plausible outcomes) ─────
  function executeAction(action, params, event) {
    // Mostly inert — ASTRO is a prototype. We log and return plausible status.
    const ts = Date.now();
    let status = 'success';
    let message = '';
    switch (action) {
      case 'notify.inbox':
        message = `Inbox · "${event.label || event.type}" · ${params.priority || 'normal'}`;
        // Drop into inbox if available
        try {
          if (window.INBOX_MESSAGES) {
            window.INBOX_MESSAGES.unshift({
              id: 'auto-' + ts, ts, kind: 'auto',
              from: 'Automations', subject: event.label || event.type,
              body: JSON.stringify(event.payload, null, 2), unread: true,
            });
          }
        } catch (_) {}
        break;
      case 'notify.email':       message = `Email → ${params.to}`; break;
      case 'notify.slack':       message = `Slack → #${params.channel || 'royalties'}`; break;
      case 'catalog.flag':       message = `Flagged in "${params.queue || 'review'}" queue`; break;
      case 'catalog.tag':        message = `Tagged: ${params.tag}`; break;
      case 'catalog.assign':     message = `Assigned to ${params.assignee}`; break;
      case 'fingerprint.run':    message = `Fingerprint job queued`; break;
      case 'royalty.hold':       message = `Payout held until ${params.until || 'review'}`; break;
      case 'royalty.release':    message = `Payout released`; break;
      case 'statement.match':    message = `Statement matcher invoked`; break;
      case 'cwr.regenerate':     message = `CWR ${params.version || 'v3.1'} regenerated`; break;
      case 'cwr.resubmit':       message = `Resubmitted to ${params.society}`; break;
      case 'claim.file':         message = `Claim filed at ${params.society}`; break;
      case 'takedown.send':      message = `DMCA notice → ${params.platform}`; break;
      case 'doc.generate.statement': message = `Statement PDF generated for ${params.counterparty}`; break;
      case 'doc.generate.cwr':   message = `CWR file generated`; break;
      case 'webhook.post':       message = `POST → ${params.url}`; break;
      default:                   message = `(unknown action ${action})`; status = 'error';
    }
    return { action, params, status, message, ts };
  }

  // ─── Public API: emit event, run all matching rules ────────────
  function emit(eventType, payload, eventLabel) {
    const event = { type: eventType, payload: payload || {}, label: eventLabel, ts: Date.now() };
    const rules = listRules();
    for (const rule of rules) {
      if (!evalRule(rule, event)) continue;
      const actions = (rule.actions || []).map(a => executeAction(a.action, a.params || {}, event));
      const overall = actions.every(a => a.status === 'success') ? 'success' : 'partial';
      pushRun({
        id: 'run-' + event.ts + '-' + rule.id,
        ts: event.ts,
        ruleId: rule.id,
        ruleName: rule.name,
        trigger: eventType,
        triggerLabel: eventLabel || eventType,
        payload: event.payload,
        actions,
        status: overall,
      });
      // Increment rule run-count
      rule.runCount = (rule.runCount || 0) + 1;
      rule.lastRun = event.ts;
    }
  }

  // ─── Rule storage ──────────────────────────────────────────────
  // Rules live on window.__AUTO_RULES; mutations broadcast via custom event.
  if (!window.__AUTO_RULES) window.__AUTO_RULES = [];

  function listRules()  { return window.__AUTO_RULES.slice(); }
  function getRule(id)  { return window.__AUTO_RULES.find(r => r.id === id); }
  function saveRule(r) {
    const existing = window.__AUTO_RULES.findIndex(x => x.id === r.id);
    if (existing >= 0) window.__AUTO_RULES[existing] = r;
    else window.__AUTO_RULES.push(r);
    window.dispatchEvent(new CustomEvent('astro-auto-rules-changed'));
  }
  function deleteRule(id) {
    window.__AUTO_RULES = window.__AUTO_RULES.filter(r => r.id !== id);
    window.dispatchEvent(new CustomEvent('astro-auto-rules-changed'));
  }
  function newRule(seed) {
    return Object.assign({
      id: 'rule-' + Math.random().toString(36).slice(2, 9),
      name: 'New rule',
      description: '',
      enabled: true,
      trigger: TRIGGERS[0].id,
      conditions: [],
      conditionMode: 'all',  // all | any
      actions: [],
      runCount: 0,
      lastRun: null,
      createdAt: Date.now(),
      schedule: null, // optional cron-like { kind: 'interval'|'cron', value: string }
    }, seed || {});
  }

  // ─── Schedules (cron-ish; ticked from caller) ──────────────────
  // Schedule kind:
  //   { kind: 'interval', minutes: 60 }
  //   { kind: 'daily',    hour: 8, minute: 0 }
  //   { kind: 'weekly',   day: 'mon', hour: 9 }
  //   { kind: 'monthly',  dayOfMonth: 1, hour: 8 }
  function describeSchedule(s) {
    if (!s) return null;
    if (s.kind === 'interval') return `Every ${s.minutes >= 60 ? Math.round(s.minutes/60) + 'h' : s.minutes + 'm'}`;
    if (s.kind === 'daily')    return `Daily at ${String(s.hour).padStart(2,'0')}:${String(s.minute||0).padStart(2,'0')}`;
    if (s.kind === 'weekly')   return `Weekly · ${s.day} · ${String(s.hour).padStart(2,'0')}:00`;
    if (s.kind === 'monthly')  return `Monthly · day ${s.dayOfMonth} · ${String(s.hour).padStart(2,'0')}:00`;
    return null;
  }

  function nextRunAt(s, now) {
    if (!s) return null;
    now = now || Date.now();
    const d = new Date(now);
    if (s.kind === 'interval') return now + (s.minutes || 60) * 60 * 1000;
    if (s.kind === 'daily') {
      const next = new Date(d);
      next.setHours(s.hour || 0, s.minute || 0, 0, 0);
      if (next.getTime() <= now) next.setDate(next.getDate() + 1);
      return next.getTime();
    }
    if (s.kind === 'weekly') {
      const days = ['sun','mon','tue','wed','thu','fri','sat'];
      const target = days.indexOf((s.day||'mon').toLowerCase());
      const next = new Date(d);
      const delta = (target - d.getDay() + 7) % 7 || 7;
      next.setDate(d.getDate() + delta);
      next.setHours(s.hour || 9, 0, 0, 0);
      return next.getTime();
    }
    if (s.kind === 'monthly') {
      const next = new Date(d.getFullYear(), d.getMonth(), s.dayOfMonth || 1, s.hour || 8);
      if (next.getTime() <= now) next.setMonth(next.getMonth() + 1);
      return next.getTime();
    }
    return null;
  }

  // ─── Run history accessors ─────────────────────────────────────
  function listRuns(opts) {
    opts = opts || {};
    let r = RUNS.slice();
    if (opts.ruleId) r = r.filter(x => x.ruleId === opts.ruleId);
    if (opts.status) r = r.filter(x => x.status === opts.status);
    if (opts.limit)  r = r.slice(0, opts.limit);
    return r;
  }

  // ─── Demo seeding: emit a stream of events on first load ───────
  function seedDemoActivity() {
    if (window.__AUTO_DEMO_SEEDED) return;
    window.__AUTO_DEMO_SEEDED = true;

    // Seed runs that look historical (last 14 days) for any pre-existing rules.
    const rules = listRules();
    if (!rules.length) return;
    const now = Date.now();
    const sampleTriggers = {
      'cwr.ack.received':    { society: 'ASCAP', workId: 'W-00184', status: 'accepted' },
      'cwr.ack.rejected':    { society: 'BMI',   workId: 'W-00211', status: 'rejected', reason: 'IPI mismatch' },
      'statement.imported':  { source: 'Spotify', period: '2025 Q1', amount: 18430, rows: 4128 },
      'statement.variance':  { source: 'Apple Music', expected: 12500, actual: 9430, varPct: -24.6 },
      'leak.detected':       { workId: 'W-00184', kind: 'under-report', severity: 'high', impact: 1240 },
      'bbrank.high':         { workId: 'W-00211', score: 87, recovery: 540 },
      'cover.detected':      { workId: 'W-00067', platform: 'YouTube', recovery: 320 },
      'agreement.expiring':  { agreementId: 'AGR-018', daysLeft: 28 },
    };

    for (let day = 0; day < 14; day++) {
      const dayStart = now - (day * 86400000);
      const eventCount = Math.floor(Math.random() * 4) + 1;
      for (let i = 0; i < eventCount; i++) {
        const triggers = Object.keys(sampleTriggers);
        const tt = triggers[Math.floor(Math.random() * triggers.length)];
        const matchingRule = rules.find(r => r.trigger === tt && r.enabled);
        if (!matchingRule) continue;
        const event = {
          type: tt,
          payload: sampleTriggers[tt],
          label: TRIGGERS.find(x => x.id === tt)?.label || tt,
          ts: dayStart - Math.floor(Math.random() * 86400000),
        };
        if (!evalRule(matchingRule, event)) continue;
        const actions = (matchingRule.actions || []).map(a => {
          const r = executeAction(a.action, a.params || {}, event);
          // Backdate
          r.ts = event.ts;
          return r;
        });
        const overall = actions.every(a => a.status === 'success') ? 'success'
                       : actions.length === 0 ? 'success' : 'partial';
        RUNS.push({
          id: 'run-' + event.ts + '-' + matchingRule.id,
          ts: event.ts,
          ruleId: matchingRule.id,
          ruleName: matchingRule.name,
          trigger: tt,
          triggerLabel: event.label,
          payload: event.payload,
          actions,
          status: overall,
        });
      }
    }
    RUNS.sort((a, b) => b.ts - a.ts);
  }

  // ─── Public surface ────────────────────────────────────────────
  window.AutoEngine = {
    TRIGGERS, ACTIONS, OPERATORS,
    listRules, getRule, saveRule, deleteRule, newRule,
    listRuns, emit,
    describeSchedule, nextRunAt,
    seedDemoActivity,
    // Helpers
    triggerById: (id) => TRIGGERS.find(t => t.id === id),
    actionById:  (id) => ACTIONS.find(a => a.id === id),
  };
  console.log('[AutoEngine] loaded · ' + TRIGGERS.length + ' triggers · ' + ACTIONS.length + ' actions');
})();
