#!/usr/bin/env node
/**
 * OpenClaw Orchestration — Cross-Session Protocol
 *
 * Structured message types for inter-session communication.
 * Migrated from ECC `ecc2/src/comms/mod.rs` (MessageType enum)
 *
 * Message types:
 *   TaskHandoff  — Delegate a task to another session
 *   Query        — Ask a question to another session
 *   Response     — Answer a query
 *   Completed    — Notify completion of delegated task
 *   Conflict     — Signal resource conflict between sessions
 *
 * Usage:
 *   const { MessageBus } = require('./cross-session');
 *   const bus = new MessageBus();
 *   bus.send({ type: 'TaskHandoff', from: 's1', to: 's2', task: '...' });
 */

const fs = require('fs');
const path = require('path');

// ─── Message Types ───────────────────────────────────────────────

const MessageType = Object.freeze({
  TASK_HANDOFF: 'TaskHandoff',
  QUERY:        'Query',
  RESPONSE:     'Response',
  COMPLETED:    'Completed',
  CONFLICT:     'Conflict',
  NOTIFICATION: 'Notification',
});

const MessagePriority = Object.freeze({
  LOW:      0,
  NORMAL:   1,
  HIGH:     2,
  CRITICAL: 3,
});

// ─── Message Validation ──────────────────────────────────────────

const MESSAGE_SCHEMAS = {
  [MessageType.TASK_HANDOFF]: {
    required: ['from', 'to', 'task'],
    optional: ['context', 'priority', 'files', 'deadline'],
  },
  [MessageType.QUERY]: {
    required: ['from', 'to', 'question'],
    optional: ['context', 'priority', 'expectedBy'],
  },
  [MessageType.RESPONSE]: {
    required: ['from', 'to', 'answer', 'replyTo'],
    optional: ['files', 'data'],
  },
  [MessageType.COMPLETED]: {
    required: ['from', 'to', 'summary'],
    optional: ['filesChanged', 'artifacts', 'durationMs', 'success'],
  },
  [MessageType.CONFLICT]: {
    required: ['from', 'to', 'file', 'description'],
    optional: ['suggestedResolution', 'severity'],
  },
  [MessageType.NOTIFICATION]: {
    required: ['from', 'message'],
    optional: ['to', 'priority', 'data'],
  },
};

// ─── MessageBus Class ────────────────────────────────────────────

class MessageBus {
  constructor(options = {}) {
    this.inboxPath = options.inboxPath || path.join(
      process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, '.openclaw'),
      'messages.json'
    );
    this.messages = [];
    this.handlers = new Map();    // sessionId → callback
    this.wildcardHandlers = [];   // * handlers for all messages
    this._load();
  }

  /**
   * Send a structured message.
   * Validates required fields per message type.
   */
  send(msg) {
    // Set defaults
    const message = {
      id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
      type: msg.type || MessageType.NOTIFICATION,
      from: msg.from || 'system',
      to: msg.to || '*',
      priority: msg.priority || MessagePriority.NORMAL,
      timestamp: msg.timestamp || new Date().toISOString(),
      delivered: false,
      ...msg,
    };

    // Validate
    const schema = MESSAGE_SCHEMAS[message.type];
    if (schema) {
      for (const field of schema.required) {
        if (!message[field]) {
          throw new Error(`Message type '${message.type}' requires field: ${field}`);
        }
      }
    }

    this.messages.push(message);
    this._save();

    // Deliver to handlers
    this._deliver(message);

    return message;
  }

  /**
   * Register a handler for messages addressed to a session.
   */
  onMessage(sessionId, callback) {
    if (!this.handlers.has(sessionId)) {
      this.handlers.set(sessionId, []);
    }
    this.handlers.get(sessionId).push(callback);
  }

  /**
   * Register a wildcard handler (receives ALL messages).
   */
  onAny(callback) {
    this.wildcardHandlers.push(callback);
  }

  /**
   * Deliver a message to registered handlers.
   */
  _deliver(message) {
    // Targeted delivery
    if (message.to !== '*') {
      const sessionHandlers = this.handlers.get(message.to) || [];
      for (const handler of sessionHandlers) {
        try { handler(message); } catch (e) {}
      }
    }

    // Wildcard handlers
    for (const handler of this.wildcardHandlers) {
      try { handler(message); } catch (e) {}
    }

    message.delivered = true;
  }

  /**
   * Get undelivered messages for a session.
   */
  inbox(sessionId) {
    return this.messages.filter(
      m => (m.to === sessionId || m.to === '*') && !m.read
    );
  }

  /**
   * Mark messages as read.
   */
  markRead(messageIds) {
    for (const msg of this.messages) {
      if (messageIds.includes(msg.id)) {
        msg.read = true;
        msg.readAt = new Date().toISOString();
      }
    }
  }

  /**
   * Create a TaskHandoff message (convenience).
   *
   * ECC: Comms::MessageType::TaskHandoff (ecc2/src/comms/mod.rs)
   */
  handoff({ from, to, task, context = '', priority = MessagePriority.NORMAL, files = [] }) {
    return this.send({
      type: MessageType.TASK_HANDOFF,
      from, to, task, context, priority, files,
    });
  }

  /**
   * Create a Query message.
   */
  query({ from, to, question, context = '' }) {
    return this.send({
      type: MessageType.QUERY,
      from, to, question, context,
    });
  }

  /**
   * Create a Response message.
   */
  respond({ from, to, replyTo, answer, data }) {
    return this.send({
      type: MessageType.RESPONSE,
      from, to, replyTo, answer, data,
    });
  }

  /**
   * Create a Completed notification.
   */
  notifyComplete({ from, to, summary, filesChanged = [], success = true, durationMs = 0 }) {
    return this.send({
      type: MessageType.COMPLETED,
      from, to, summary, filesChanged, success, durationMs,
    });
  }

  /**
   * Create a Conflict signal.
   */
  signalConflict({ from, to, file, description, suggestedResolution = '' }) {
    return this.send({
      type: MessageType.CONFLICT,
      from, to, file, description, suggestedResolution,
      priority: MessagePriority.HIGH,
    });
  }

  /**
   * Get message stats.
   */
  stats() {
    return {
      total: this.messages.length,
      undelivered: this.messages.filter(m => !m.delivered).length,
      unread: this.messages.filter(m => !m.read).length,
      byType: this.messages.reduce((acc, m) => {
        acc[m.type] = (acc[m.type] || 0) + 1;
        return acc;
      }, {}),
    };
  }

  /**
   * Prune old read messages.
   */
  prune(olderThanMs = 24 * 3600 * 1000) {
    const cutoff = Date.now() - olderThanMs;
    const before = this.messages.length;
    this.messages = this.messages.filter(m =>
      new Date(m.timestamp).getTime() > cutoff || !m.read
    );
    this._save();
    return { pruned: before - this.messages.length, remaining: this.messages.length };
  }

  _save() {
    const dir = path.dirname(this.inboxPath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

    // Only persist recent messages (last 1000)
    const toSave = this.messages.slice(-1000);
    fs.writeFileSync(this.inboxPath, JSON.stringify(toSave, null, 2), 'utf8');
  }

  _load() {
    try {
      if (fs.existsSync(this.inboxPath)) {
        this.messages = JSON.parse(fs.readFileSync(this.inboxPath, 'utf8'));
      }
    } catch {
      this.messages = [];
    }
  }
}

// ─── Memory Connectors ───────────────────────────────────────────

/**
 * External memory connectors for importing structured data.
 * ECC: MemoryConnectorConfig (ecc2/src/config/mod.rs:128-170)
 *
 * Supported formats:
 *   - JSONL files/directories
 *   - Markdown files/directories
 *   - Dotenv files (secrets stripped)
 */

class MemoryConnectors {
  /**
   * Import JSONL file into context graph.
   * Each line = one entity/observation.
   */
  static importJsonl(filePath, contextGraph) {
    const content = fs.readFileSync(filePath, 'utf8');
    const lines = content.split('\n').filter(l => l.trim());
    const imported = [];

    for (const line of lines) {
      try {
        const entry = JSON.parse(line);
        const entityId = entry.id || entry.entity || `imported_${imported.length}`;
        const entity = contextGraph.entity(entityId, {
          type: entry.type || 'imported',
          name: entry.name || entityId,
          path: filePath,
          summary: entry.summary || '',
          metadata: entry.metadata || {},
        });

        if (entry.observation) {
          contextGraph.observe(entityId, {
            type: entry.observationType || 'note',
            summary: entry.observation,
            details: entry.details || {},
            priority: entry.priority || 'normal',
          });
        }

        imported.push(entity);
      } catch {}
    }

    return imported;
  }

  /**
   * Import Markdown file — parse sections as entities.
   * Headers become entity names, content becomes observations.
   */
  static importMarkdown(filePath, contextGraph) {
    const content = fs.readFileSync(filePath, 'utf8');
    const imported = [];
    let currentEntity = null;
    let currentContent = [];

    const headerRe = /^#{1,3}\s+(.+)$/;

    for (const line of content.split('\n')) {
      const headerMatch = line.match(headerRe);
      if (headerMatch) {
        // Save previous entity
        if (currentEntity && currentContent.length > 0) {
          contextGraph.observe(currentEntity, {
            type: 'note',
            summary: currentContent.slice(0, 5).join('\n'),
            details: { fullContent: currentContent.join('\n') },
          });
        }

        const name = headerMatch[1].trim();
        currentEntity = name;
        currentContent = [];

        contextGraph.entity(name, {
          type: 'markdown_section',
          name,
          path: filePath,
          summary: name,
        });
        imported.push(currentEntity);
      } else if (currentEntity && line.trim()) {
        currentContent.push(line.trim());
      }
    }

    // Save last entity
    if (currentEntity && currentContent.length > 0) {
      contextGraph.observe(currentEntity, {
        type: 'note',
        summary: currentContent.slice(0, 5).join('\n'),
        details: { fullContent: currentContent.join('\n') },
      });
    }

    return imported;
  }

  /**
   * Import Dotenv file — strip values, keep keys.
   */
  static importDotenv(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    const vars = {};

    for (const line of content.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const eqIdx = trimmed.indexOf('=');
      if (eqIdx < 0) continue;
      const key = trimmed.substring(0, eqIdx).trim();
      const value = trimmed.substring(eqIdx + 1).trim();
      // Strip secrets: show first 4 chars + length
      const safeValue = value.length > 8
        ? value.substring(0, 4) + '...[' + value.length + ' chars]'
        : '***';
      vars[key] = safeValue;
    }

    return vars;
  }
}

// ─── Module exports ──────────────────────────────────────────────

module.exports = {
  MessageBus,
  MessageType,
  MessagePriority,
  MemoryConnectors,
};
