#!/usr/bin/env node

/**
 * Superball Thrower Daemon
 * 
 * A standalone Node.js daemon implementation of the Superball Thrower functionality
 * extracted from the web-based thrower.html implementation.
 * 
 * This daemon handles:
 * - NOSTR event monitoring and processing
 * - NIP-44 encryption/decryption
 * - Superball protocol routing (SUP-01 through SUP-06)
 * - Event queue management with delayed processing
 * - Relay management and authentication testing
 * - Thrower Information Document publishing (SUP-06)
 * - Privacy-focused event forwarding with padding
 */

const WebSocket = require('ws');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

// Import NOSTR tools - these will be installed via npm
const { SimplePool, generateSecretKey, getPublicKey, finalizeEvent, nip44 } = require('nostr-tools');

// Configuration
class ThrowerConfig {
  constructor(configPath = './config.json') {
    this.configPath = configPath;
    this.config = this.loadConfig();
  }

  loadConfig() {
    try {
      if (fs.existsSync(this.configPath)) {
        const configData = fs.readFileSync(this.configPath, 'utf8');
        return JSON.parse(configData);
      }
    } catch (error) {
      console.error(`Error loading config: ${error.message}`);
    }
    
    // Default configuration
    return {
      thrower: {
        privateKey: null, // Will be generated if not provided
        name: 'My Superball Thrower',
        description: 'A privacy-focused Superball Thrower node',
        banner: '',
        icon: '',
        adminPubkey: '',
        contact: '',
        supportedSups: '1,2,3,4,5,6',
        software: 'https://git.laantungir.net/laantungir/super_ball.git',
        version: '1.0.0',
        privacyPolicy: '',
        termsOfService: '',
        refreshRate: 300,
        maxDelay: 86460
      },
      relays: [
        { url: 'wss://relay.laantungir.net', type: '', authStatus: 'unknown' }
      ],
      daemon: {
        logLevel: 'info', // debug, info, warn, error
        maxQueueSize: 1000,
        maxLogEntries: 1000,
        autoStart: false
      }
    };
  }

  saveConfig() {
    try {
      fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
      return true;
    } catch (error) {
      console.error(`Error saving config: ${error.message}`);
      return false;
    }
  }

  get(key) {
    return key.split('.').reduce((obj, k) => obj && obj[k], this.config);
  }

  set(key, value) {
    const keys = key.split('.');
    const lastKey = keys.pop();
    const target = keys.reduce((obj, k) => obj[k] = obj[k] || {}, this.config);
    target[lastKey] = value;
  }
}

// NOSTR Utilities
class NostrUtils {
  static generateKeyPair() {
    const privateKey = generateSecretKey();
    const publicKey = getPublicKey(privateKey);
    return { privateKey, publicKey };
  }

  static bytesToHex(bytes) {
    return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
  }

  static hexToBytes(hex) {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
      bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
    }
    return bytes;
  }

  static async encryptNip44(content, senderPrivateKey, recipientPublicKey) {
    const senderPrivateKeyHex = typeof senderPrivateKey === 'string' ? 
      senderPrivateKey : this.bytesToHex(senderPrivateKey);
    
    const conversationKey = nip44.v2.utils.getConversationKey(
      senderPrivateKeyHex,
      recipientPublicKey
    );
    
    return nip44.v2.encrypt(content, conversationKey);
  }

  static async decryptNip44(encryptedContent, recipientPrivateKey, senderPublicKey) {
    const recipientPrivateKeyHex = typeof recipientPrivateKey === 'string' ? 
      recipientPrivateKey : this.bytesToHex(recipientPrivateKey);
    
    const conversationKey = nip44.v2.utils.getConversationKey(
      recipientPrivateKeyHex,
      senderPublicKey
    );
    
    return nip44.v2.decrypt(encryptedContent, conversationKey);
  }
}

// Relay Management
class RelayManager {
  constructor(config, logger) {
    this.config = config;
    this.logger = logger;
    this.relays = config.get('relays') || [];
  }

  async testRelayAuthentication(relayUrl) {
    return new Promise((resolve) => {
      const timeout = setTimeout(() => {
        this.logger.error(`Auth test timed out for ${relayUrl} (10s timeout)`);
        ws.close();
        resolve('error');
      }, 10000);

      const ws = new WebSocket(relayUrl);
      let authRequired = false;
      let publishAttempted = false;

      ws.on('open', () => {
        this.logger.info(`Connected to ${relayUrl} for auth test`);

        // Generate test event with random key
        const testKey = generateSecretKey();
        const testEvent = {
          kind: 1,
          content: `Auth test ${Date.now()}`,
          tags: [],
          created_at: Math.floor(Date.now() / 1000)
        };

        const signedEvent = finalizeEvent(testEvent, testKey);

        // Attempt to publish
        const publishMsg = JSON.stringify(['EVENT', signedEvent]);
        ws.send(publishMsg);
        publishAttempted = true;

        this.logger.info(`Sent test event ${signedEvent.id.substring(0, 16)}... to ${relayUrl}`);
      });

      ws.on('message', (data) => {
        try {
          const message = JSON.parse(data.toString());
          this.logger.debug(`Response from ${relayUrl}: ${JSON.stringify(message)}`);

          if (message[0] === 'AUTH') {
            authRequired = true;
            this.logger.info(`${relayUrl} requires AUTH - marking as read-only`);
            clearTimeout(timeout);
            ws.close();
            resolve('auth-required');
          } else if (message[0] === 'OK') {
            if (publishAttempted && !authRequired) {
              const accepted = message[2];
              const reason = message[3] || '';

              if (accepted) {
                this.logger.info(`${relayUrl} accepted test event - read/write capable`);
                clearTimeout(timeout);
                ws.close();
                resolve('no-auth');
              } else {
                this.logger.info(`${relayUrl} rejected test event: "${reason}" - marking as read-only`);
                clearTimeout(timeout);
                ws.close();
                resolve('auth-required');
              }
            }
          } else if (message[0] === 'NOTICE') {
            const notice = message[1] || '';
            this.logger.info(`Notice from ${relayUrl}: "${notice}"`);
            if (notice.toLowerCase().includes('auth')) {
              authRequired = true;
              this.logger.info(`${relayUrl} notice indicates auth required`);
              clearTimeout(timeout);
              ws.close();
              resolve('auth-required');
            }
          }
        } catch (error) {
          this.logger.error(`Error parsing message from ${relayUrl}: ${error.message}`);
        }
      });

      ws.on('error', (error) => {
        this.logger.error(`WebSocket error with ${relayUrl}: ${error.message}`);
        clearTimeout(timeout);
        resolve('error');
      });

      ws.on('close', (code, reason) => {
        this.logger.info(`Connection closed to ${relayUrl} - Code: ${code}, Reason: ${reason}`);
        clearTimeout(timeout);
        if (!authRequired && publishAttempted) {
          this.logger.info(`${relayUrl} closed without clear response - assuming no auth required`);
          resolve('no-auth');
        }
      });
    });
  }

  async testAllRelays() {
    this.logger.info(`Testing authentication for ${this.relays.length} relays...`);

    const testPromises = this.relays.map(async (relay, index) => {
      try {
        relay.authStatus = 'testing';
        const result = await this.testRelayAuthentication(relay.url);
        relay.authStatus = result;
        relay.lastTested = Date.now();
        return result;
      } catch (error) {
        this.logger.error(`Failed to test relay ${relay.url}: ${error.message}`);
        relay.authStatus = 'error';
        relay.lastTested = Date.now();
        return 'error';
      }
    });

    await Promise.allSettled(testPromises);

    const results = this.relays.map(r => r.authStatus);
    const readWriteCount = results.filter(r => r === 'no-auth').length;
    const readOnlyCount = results.filter(r => r === 'auth-required').length;
    const errorCount = results.filter(r => r === 'error').length;

    this.logger.info(`Relay auth testing complete: ${readWriteCount} read/write, ${readOnlyCount} read-only, ${errorCount} errors`);

    // Update relay capabilities based on auth status
    this.relays.forEach(relay => {
      if (relay.authStatus === 'auth-required') {
        // Can only read from auth-required relays
        relay.write = false;
        // Keep read capability as configured
      } else if (relay.authStatus === 'no-auth') {
        // Keep both read and write capabilities as configured
      } else if (relay.authStatus === 'error') {
        // Disable both read and write for error relays
        relay.read = false;
        relay.write = false;
      }
    });

    return { readWriteCount, readOnlyCount, errorCount };
  }

  getWriteCapableRelays() {
    return this.relays.filter(relay =>
      relay.write && (relay.authStatus === 'no-auth' || relay.authStatus === 'unknown')
    );
  }

  getReadCapableRelays() {
    return this.relays.filter(relay =>
      relay.read && relay.authStatus !== 'error'
    );
  }
}

// Event Processing
class EventProcessor {
  constructor(config, logger, relayManager) {
    this.config = config;
    this.logger = logger;
    this.relayManager = relayManager;
    this.eventQueue = [];
    this.processedEventIds = new Set();
    this.processedEvents = 0;
  }

  async handleIncomingEvent(event) {
    // Deduplication check
    if (this.processedEventIds.has(event.id)) {
      this.logger.info(`Skipping duplicate event: ${event.id.substring(0, 16)}... (already processed)`);
      return;
    }

    this.processedEventIds.add(event.id);
    this.logger.info(`Received routing event: ${event.id.substring(0, 16)}...`);

    try {
      // Decrypt the event payload
      let decryptedPayload = await this.decryptRoutingEvent(event);

      if (!decryptedPayload) {
        this.logger.error(`Failed to decrypt event ${event.id.substring(0, 16)}...`);
        return;
      }

      this.logger.info(`First decryption successful for event ${event.id.substring(0, 16)}...`);

      // Check payload type according to DAEMON.md protocol
      if (decryptedPayload.padding !== undefined) {
        this.logger.info(`Detected Type 2 (Padding Payload) - discarding padding and performing second decryption`);

        const innerEvent = decryptedPayload.event;
        this.logger.info(`Discarding padding: "${decryptedPayload.padding}"`);

        // Second decryption to get the actual routing instructions
        decryptedPayload = await this.decryptRoutingEvent(innerEvent);

        if (!decryptedPayload) {
          this.logger.error(`Failed to decrypt inner event ${innerEvent.id.substring(0, 16)}...`);
          return;
        }

        this.logger.info(`Second decryption successful - found original routing instructions from builder`);
      } else {
        this.logger.info(`Detected Type 1 (Routing Payload) - processing routing instructions directly`);
      }

      this.logger.debug(`Final routing payload: ${JSON.stringify(decryptedPayload, null, 2)}`);

      // Parse routing instructions
      const { event: wrappedEvent, routing } = decryptedPayload;

      if (!routing) {
        this.logger.error(`No routing instructions found in final payload for ${event.id.substring(0, 16)}...`);
        return;
      }

      if (!this.validateRoutingInstructions(routing)) {
        this.logger.error(`Invalid routing instructions in event ${event.id.substring(0, 16)}...`);
        return;
      }

      // Create queue item
      const queueItem = {
        id: event.id,
        wrappedEvent,
        routing,
        receivedAt: Date.now(),
        processAt: Date.now() + (routing.delay * 1000),
        status: 'queued'
      };

      this.eventQueue.push(queueItem);
      this.logger.info(`Event queued for processing in ${routing.delay}s: ${event.id.substring(0, 16)}...`);

      // Schedule processing
      setTimeout(() => this.processQueuedEvent(queueItem), routing.delay * 1000);

    } catch (error) {
      this.logger.error(`Error processing event ${event.id.substring(0, 16)}...: ${error.message}`);
    }
  }

  async decryptRoutingEvent(event) {
    try {
      const privateKeyHex = this.config.get('thrower.privateKey');
      if (!privateKeyHex) {
        throw new Error('No private key configured for decryption');
      }

      this.logger.debug(`Attempting decryption of event ${event.id} from ${event.pubkey}`);

      const decrypted = await NostrUtils.decryptNip44(
        event.content,
        privateKeyHex,
        event.pubkey
      );

      this.logger.debug(`Decryption successful! Decrypted length: ${decrypted.length}`);
      return JSON.parse(decrypted);

    } catch (error) {
      this.logger.error(`Decryption failed: ${error.message}`);
      return null;
    }
  }

  validateRoutingInstructions(routing) {
    if (!routing || typeof routing !== 'object') return false;
    if (!Array.isArray(routing.relays) || routing.relays.length === 0) return false;
    if (typeof routing.delay !== 'number' || routing.delay < 0) return false;
    if (!routing.audit || typeof routing.audit !== 'string') return false;
    
    // Check maximum delay limit
    const maxDelay = this.config.get('thrower.maxDelay') || 86460;
    if (routing.delay > maxDelay) {
      this.logger.error(`Routing delay ${routing.delay}s exceeds maximum allowed delay of ${maxDelay}s`);
      return false;
    }
    
    return true;
  }

  async processQueuedEvent(queueItem) {
    this.logger.info(`Processing event ${queueItem.id.substring(0, 16)}...`);
    queueItem.status = 'processing';

    try {
      const { wrappedEvent, routing } = queueItem;

      this.logger.debug(`Decision point - routing.p = "${routing.p}" (${typeof routing.p})`);
      
      if (routing.p) {
        // Continue routing to next Superball with padding wrapper
        await this.forwardToNextSuperball(wrappedEvent, routing);
      } else {
        // Final posting - post the wrapped event directly
        await this.postFinalEvent(wrappedEvent, routing.relays);
      }

      this.processedEvents++;

      // Remove from queue
      const index = this.eventQueue.findIndex(item => item.id === queueItem.id);
      if (index !== -1) {
        this.eventQueue.splice(index, 1);
      }

      this.logger.info(`Successfully processed event ${queueItem.id.substring(0, 16)}...`);

    } catch (error) {
      this.logger.error(`Failed to process event ${queueItem.id.substring(0, 16)}...: ${error.message}`);
      queueItem.status = 'failed';
    }
  }

  async forwardToNextSuperball(event, routing) {
    this.logger.info(`Forwarding to next Superball: ${routing.p.substring(0, 16)}...`);

    // Create new ephemeral keypair
    const ephemeralKey = generateSecretKey();
    const ephemeralPubkey = getPublicKey(ephemeralKey);

    // Generate padding based on add_padding_bytes instruction
    let paddingData = '';
    if (routing.add_padding_bytes && routing.add_padding_bytes > 0) {
      const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      for (let i = 0; i < routing.add_padding_bytes; i++) {
        paddingData += chars.charAt(Math.floor(Math.random() * chars.length));
      }
      this.logger.info(`Generated ${paddingData.length} bytes of padding`);
    }

    // Create padding-only payload
    const paddingPayload = {
      event: event, // This is the still-encrypted inner event
      padding: paddingData // Padding to discard
    };

    this.logger.debug(`Creating padding payload with ${paddingData.length} bytes of padding`);

    // Encrypt padding payload to next Superball
    const ephemeralKeyHex = NostrUtils.bytesToHex(ephemeralKey);
    const encryptedContent = await NostrUtils.encryptNip44(
      JSON.stringify(paddingPayload),
      ephemeralKeyHex,
      routing.p
    );

    // Create new routing event (forwarding to next hop)
    const routingEvent = {
      kind: 22222,
      content: encryptedContent,
      tags: [
        ['p', routing.p],           // Next Superball
        ['p', routing.audit]        // Audit tag (camouflage)
      ],
      created_at: Math.floor(Date.now() / 1000)
    };

    // Sign with ephemeral key
    const signedEvent = finalizeEvent(routingEvent, ephemeralKey);

    this.logger.debug(`Padding-wrapped event to publish: ${JSON.stringify(signedEvent, null, 2)}`);

    // Publish to specified relays
    await this.publishToRelays(signedEvent, routing.relays);

    this.logger.info(`Forwarded with padding layer to ${routing.p.substring(0, 16)}... (audit: ${routing.audit.substring(0, 16)}...)`);
  }

  async postFinalEvent(event, relays) {
    this.logger.info(`Posting final event to ${relays.length} relays`);
    await this.publishToRelays(event, relays);
    this.logger.info(`Final event posted to relays: ${event.id.substring(0, 16)}...`);
  }

  async publishToRelays(event, relays) {
    const pool = new SimplePool();

    // Filter to only use relays that don't require auth (write-capable)
    const writeRelays = relays.filter(relayUrl => {
      const relay = this.relayManager.relays.find(r => r.url === relayUrl);
      if (!relay) return true; // Default to allowing if relay not found

      return relay.authStatus === 'no-auth' || relay.authStatus === 'unknown';
    });

    if (writeRelays.length === 0) {
      throw new Error('No write-capable (non-AUTH) relays available for publishing');
    }

    if (writeRelays.length < relays.length) {
      const skippedCount = relays.length - writeRelays.length;
      this.logger.info(`Skipping ${skippedCount} AUTH-required relays, using ${writeRelays.length} write-capable relays`);
    }

    // Publish to each relay individually
    const publishPromises = writeRelays.map(relayUrl => {
      const promises = pool.publish([relayUrl], event);
      return promises[0]
        .then(result => ({ relayUrl, success: true, result }))
        .catch(error => ({ relayUrl, success: false, error: error.message }));
    });

    try {
      const results = await Promise.allSettled(publishPromises);

      let successCount = 0;
      let failureCount = 0;
      const successfulRelays = [];
      const failedRelays = [];

      results.forEach((promiseResult, index) => {
        if (promiseResult.status === 'fulfilled') {
          const { relayUrl, success, result, error } = promiseResult.value;

          if (success) {
            successCount++;
            successfulRelays.push(relayUrl);
            this.logger.info(`✅ Published successfully to ${relayUrl}`);
          } else {
            failureCount++;
            failedRelays.push({ relayUrl, error });
            this.logger.error(`❌ Failed to publish to ${relayUrl}: ${error}`);
          }
        } else {
          const relayUrl = writeRelays[index];
          failureCount++;
          failedRelays.push({ relayUrl, error: promiseResult.reason?.message || 'Unknown error' });
          this.logger.error(`❌ Failed to publish to ${relayUrl}: ${promiseResult.reason?.message || 'Unknown error'}`);
        }
      });

      this.logger.info(`Publishing summary: ${successCount} successful, ${failureCount} failed out of ${writeRelays.length} total relays`);

      if (successCount > 0) {
        this.logger.info(`Successfully published to: ${successfulRelays.join(', ')}`);
        this.logger.debug(`Full published event: ${JSON.stringify(event, null, 2)}`);
      }

      if (failureCount > 0) {
        const failureDetails = failedRelays.map(f => `${f.relayUrl}: ${f.error}`).join(', ');
        this.logger.error(`Failed to publish to: ${failureDetails}`);
      }

      if (successCount === 0) {
        throw new Error(`Failed to publish to any write-capable relay. Attempted: ${writeRelays.join(', ')}`);
      }

    } finally {
      pool.close(writeRelays);
    }
  }

  getQueueStatus() {
    return {
      queueLength: this.eventQueue.length,
      processedEvents: this.processedEvents,
      queueItems: this.eventQueue.map(item => ({
        id: item.id.substring(0, 16) + '...',
        status: item.status,
        timeLeft: Math.max(0, Math.ceil((item.processAt - Date.now()) / 1000)),
        relayCount: item.routing.relays.length,
        delay: item.routing.delay,
        nextHop: item.routing.p ? item.routing.p.substring(0, 16) + '...' : 'Final Posting'
      }))
    };
  }
}

// Thrower Information Document Manager (SUP-06)
class ThrowerInfoManager {
  constructor(config, logger, relayManager) {
    this.config = config;
    this.logger = logger;
    this.relayManager = relayManager;
    this.refreshInterval = null;
    this.lastPublish = null;
  }

  async publishThrowerInfo() {
    const privateKeyHex = this.config.get('thrower.privateKey');
    if (!privateKeyHex) {
      throw new Error('No private key configured for publishing');
    }

    const throwerInfo = this.config.get('thrower');
    
    // Build tags array according to SUP-06
    const tags = [];
    
    if (throwerInfo.name) tags.push(['name', throwerInfo.name]);
    if (throwerInfo.description) tags.push(['description', throwerInfo.description]);
    if (throwerInfo.banner) tags.push(['banner', throwerInfo.banner]);
    if (throwerInfo.icon) tags.push(['icon', throwerInfo.icon]);
    if (throwerInfo.adminPubkey) tags.push(['pubkey', throwerInfo.adminPubkey]);
    if (throwerInfo.contact) tags.push(['contact', throwerInfo.contact]);
    if (throwerInfo.supportedSups) tags.push(['supported_sups', throwerInfo.supportedSups]);
    if (throwerInfo.software) tags.push(['software', throwerInfo.software]);
    if (throwerInfo.version) tags.push(['version', throwerInfo.version]);
    if (throwerInfo.privacyPolicy) tags.push(['privacy_policy', throwerInfo.privacyPolicy]);
    if (throwerInfo.termsOfService) tags.push(['terms_of_service', throwerInfo.termsOfService]);
    tags.push(['refresh_rate', throwerInfo.refreshRate.toString()]);
    tags.push(['max_delay', (throwerInfo.maxDelay || 86460).toString()]);

    const eventTemplate = {
      kind: 12222,
      content: throwerInfo.content || '',
      tags: tags,
      created_at: Math.floor(Date.now() / 1000)
    };

    const privateKeyBytes = NostrUtils.hexToBytes(privateKeyHex);
    const signedEvent = finalizeEvent(eventTemplate, privateKeyBytes);

    this.logger.debug(`Thrower Info event to publish: ${JSON.stringify(signedEvent, null, 2)}`);

    // Publish to write-capable relays
    const writeRelays = this.relayManager.getWriteCapableRelays().map(r => r.url);
    
    if (writeRelays.length === 0) {
      throw new Error('No write-capable relays available for publishing Thrower Info');
    }

    const pool = new SimplePool();

    try {
      const publishPromises = writeRelays.map(relayUrl => {
        const promises = pool.publish([relayUrl], signedEvent);
        return promises[0]
          .then(() => ({ relayUrl, success: true }))
          .catch(error => ({ relayUrl, success: false, error: error.message }));
      });

      const results = await Promise.allSettled(publishPromises);
      let successCount = 0;

      results.forEach((promiseResult) => {
        if (promiseResult.status === 'fulfilled') {
          const { relayUrl, success, error } = promiseResult.value;

          if (success) {
            successCount++;
            this.logger.info(`✅ Thrower info published successfully to ${relayUrl}`);
          } else {
            this.logger.error(`❌ Failed to publish thrower info to ${relayUrl}: ${error}`);
          }
        }
      });

      if (successCount === 0) {
        throw new Error(`Failed to publish thrower info to any relay (attempted: ${writeRelays.join(', ')})`);
      }

      this.logger.info(`Thrower info published to ${successCount} out of ${writeRelays.length} relays`);
      this.lastPublish = signedEvent.created_at;

    } finally {
      pool.close(writeRelays);
    }

    // Also publish relay list (NIP-65) so web interface can show relay configuration
    await this.publishRelayList();
  }

  async publishRelayList() {
    const privateKeyHex = this.config.get('thrower.privateKey');
    if (!privateKeyHex) {
      throw new Error('No private key configured for publishing relay list');
    }

    this.logger.info('Publishing relay list (NIP-65) for web interface compatibility...');

    const relays = this.config.get('relays') || [];
    const relayTags = [];
    
    // Add relay tags based on configuration and authentication status
    relays.forEach(relay => {
      if (relay.authStatus === 'error') {
        // Skip relays that are completely broken
        return;
      }

      // Add read capability if configured and relay is functional
      if (relay.read && relay.authStatus !== 'error') {
        relayTags.push(['r', relay.url, 'read']);
      }

      // Add write capability if configured and relay supports it
      if (relay.write && (relay.authStatus === 'no-auth' || relay.authStatus === 'unknown')) {
        relayTags.push(['r', relay.url, 'write']);
      }
    });

    if (relayTags.length === 0) {
      this.logger.warn('No relays available for relay list - skipping relay list publication');
      return;
    }

    const relayListEvent = {
      kind: 10002,
      content: '',
      tags: relayTags,
      created_at: Math.floor(Date.now() / 1000)
    };

    const privateKeyBytes = NostrUtils.hexToBytes(privateKeyHex);
    const signedEvent = finalizeEvent(relayListEvent, privateKeyBytes);

    this.logger.debug(`Relay list event to publish: ${JSON.stringify(signedEvent, null, 2)}`);

    // Publish to write-capable relays
    const writeRelays = this.relayManager.getWriteCapableRelays().map(r => r.url);
    
    if (writeRelays.length === 0) {
      this.logger.warn('No write-capable relays available for publishing relay list');
      return;
    }

    const pool = new SimplePool();

    try {
      const publishPromises = writeRelays.map(relayUrl => {
        const promises = pool.publish([relayUrl], signedEvent);
        return promises[0]
          .then(() => ({ relayUrl, success: true }))
          .catch(error => ({ relayUrl, success: false, error: error.message }));
      });

      const results = await Promise.allSettled(publishPromises);
      let successCount = 0;

      results.forEach((promiseResult) => {
        if (promiseResult.status === 'fulfilled') {
          const { relayUrl, success, error } = promiseResult.value;

          if (success) {
            successCount++;
            this.logger.info(`✅ Relay list published successfully to ${relayUrl}`);
          } else {
            this.logger.error(`❌ Failed to publish relay list to ${relayUrl}: ${error}`);
          }
        }
      });

      if (successCount === 0) {
        this.logger.warn(`Failed to publish relay list to any relay (attempted: ${writeRelays.join(', ')})`);
      } else {
        this.logger.info(`Relay list published to ${successCount} out of ${writeRelays.length} relays`);
      }

    } finally {
      pool.close(writeRelays);
    }
  }

  startAutoPublish() {
    const refreshRate = this.config.get('thrower.refreshRate') || 300;
    
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
    }

    // Schedule republishing 10 seconds before refresh rate expires
    const intervalMs = Math.max(10000, (refreshRate - 10) * 1000);

    this.logger.info(`Starting Thrower Info auto-publish every ${intervalMs / 1000} seconds`);

    this.refreshInterval = setInterval(async () => {
      try {
        this.logger.info('Auto-publishing Thrower Information Document...');
        await this.publishThrowerInfo();
        this.logger.info('Thrower Info auto-published successfully');
      } catch (error) {
        this.logger.error(`Auto-publish failed: ${error.message}`);
      }
    }, intervalMs);
  }

  stopAutoPublish() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
      this.refreshInterval = null;
      this.logger.info('Stopped Thrower Info auto-publish');
    }
  }
}

// WebSocket Connection Manager
class WebSocketManager {
  constructor(config, logger, eventProcessor) {
    this.config = config;
    this.logger = logger;
    this.eventProcessor = eventProcessor;
    this.connections = [];
    this.subscriptionId = null;
  }

  async startMonitoring() {
    const publicKey = this.config.get('thrower.publicKey');
    if (!publicKey) {
      throw new Error('No public key configured for monitoring');
    }

    const relays = this.config.get('relays') || [];
    const monitoringRelays = relays.map(r => r.url);

    this.logger.info(`Connecting to ${monitoringRelays.length} relays via WebSocket for kind 22222 events`);

    // Generate unique subscription ID
    this.subscriptionId = 'superball_' + Date.now();

    // Subscribe to kind 22222 events with p tag matching this node's pubkey
    const now = Math.floor(Date.now() / 1000);
    const subscriptionFilter = {
      kinds: [22222],
      '#p': [publicKey],
      since: now
    };

    this.logger.info(`Subscription ID: ${this.subscriptionId}`);
    this.logger.debug(`Subscription filter: ${JSON.stringify(subscriptionFilter)}`);

    // Connect to each relay
    monitoringRelays.forEach((relayUrl) => {
      this.connectToRelay(relayUrl, subscriptionFilter);
    });

    return monitoringRelays.length;
  }

  connectToRelay(relayUrl, subscriptionFilter) {
    this.logger.info(`Connecting to relay: ${relayUrl}`);

    const ws = new WebSocket(relayUrl);

    ws.on('open', () => {
      this.logger.info(`Connected to relay: ${relayUrl}`);

      // Send subscription request
      const reqMessage = JSON.stringify([
        'REQ',
        this.subscriptionId,
        subscriptionFilter
      ]);

      this.logger.debug(`Sending REQ to ${relayUrl}: ${reqMessage}`);
      ws.send(reqMessage);
    });

    ws.on('message', (data) => {
      try {
        const message = JSON.parse(data.toString());
        this.logger.debug(`Received from ${relayUrl}: ${JSON.stringify(message).substring(0, 200)}...`);

        // Handle different message types
        if (message[0] === 'EVENT' && message[1] === this.subscriptionId) {
          const nostrEvent = message[2];
          this.logger.info(`Received EVENT from ${relayUrl}: ${nostrEvent.id.substring(0, 16)}...`);
          this.logger.debug(`Full received event: ${JSON.stringify(nostrEvent, null, 2)}`);
          this.eventProcessor.handleIncomingEvent(nostrEvent);
        } else if (message[0] === 'EOSE' && message[1] === this.subscriptionId) {
          this.logger.info(`End of stored events from ${relayUrl}`);
        } else if (message[0] === 'NOTICE') {
          this.logger.info(`Notice from ${relayUrl}: ${message[1]}`);
        } else if (message[0] === 'OK') {
          this.logger.debug(`OK response from ${relayUrl}: ${JSON.stringify(message)}`);
        }
      } catch (error) {
        this.logger.error(`Error parsing message from ${relayUrl}: ${error.message}`);
      }
    });

    ws.on('error', (error) => {
      this.logger.error(`WebSocket error with ${relayUrl}: ${error.message}`);
    });

    ws.on('close', (code, reason) => {
      this.logger.info(`Connection closed to ${relayUrl} - Code: ${code}, Reason: ${reason}`);
    });

    this.connections.push({ url: relayUrl, ws });
  }

  stopMonitoring() {
    this.logger.info('Stopping WebSocket monitoring...');

    this.connections.forEach(({ url, ws }) => {
      if (ws.readyState === ws.OPEN) {
        // Send CLOSE message for subscription
        if (this.subscriptionId) {
          const closeMsg = JSON.stringify(['CLOSE', this.subscriptionId]);
          ws.send(closeMsg);
          this.logger.info(`Sent CLOSE to ${url}`);
        }

        ws.close();
      }
    });

    this.connections = [];
    this.subscriptionId = null;
    this.logger.info('WebSocket monitoring stopped');
  }
}

// Logger
class Logger {
  constructor(level = 'info') {
    this.level = level;
    this.levels = { debug: 0, info: 1, warn: 2, error: 3 };
  }

  setLevel(level) {
    this.level = level;
  }

  log(level, message) {
    if (this.levels[level] >= this.levels[this.level]) {
      const timestamp = new Date().toISOString();
      console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
    }
  }

  debug(message) { this.log('debug', message); }
  info(message) { this.log('info', message); }
  warn(message) { this.log('warn', message); }
  error(message) { this.log('error', message); }
}

// Main Thrower Daemon Class
class ThrowerDaemon {
  constructor(configPath) {
    this.config = new ThrowerConfig(configPath);
    this.logger = new Logger(this.config.get('daemon.logLevel') || 'info');
    this.relayManager = new RelayManager(this.config, this.logger);
    this.eventProcessor = new EventProcessor(this.config, this.logger, this.relayManager);
    this.throwerInfoManager = new ThrowerInfoManager(this.config, this.logger, this.relayManager);
    this.wsManager = new WebSocketManager(this.config, this.logger, this.eventProcessor);
    this.running = false;
  }

  async initialize() {
    this.logger.info('Initializing Superball Thrower Daemon...');

    // Generate keypair if not configured
    let privateKey = this.config.get('thrower.privateKey');
    if (!privateKey) {
      this.logger.info('No private key found, generating new keypair...');
      const keyPair = NostrUtils.generateKeyPair();
      privateKey = NostrUtils.bytesToHex(keyPair.privateKey);
      const publicKey = NostrUtils.bytesToHex(keyPair.publicKey);
      
      this.config.set('thrower.privateKey', privateKey);
      this.config.set('thrower.publicKey', publicKey);
      this.config.saveConfig();
      
      this.logger.info(`Generated new keypair - Public key: ${publicKey}`);
    } else {
      // Derive public key from private key
      const privateKeyBytes = NostrUtils.hexToBytes(privateKey);
      const publicKey = getPublicKey(privateKeyBytes);
      this.config.set('thrower.publicKey', publicKey);
      this.logger.info(`Using existing keypair - Public key: ${publicKey}`);
    }

    this.logger.info('Daemon initialized successfully');
  }

  async start() {
    if (this.running) {
      this.logger.warn('Daemon is already running');
      return;
    }

    try {
      this.logger.info('Starting Superball Thrower Daemon...');

      // Test relay authentication
      this.logger.info('Testing relay authentication capabilities...');
      await this.relayManager.testAllRelays();

      // Start WebSocket monitoring
      const relayCount = await this.wsManager.startMonitoring();
      this.logger.info(`Monitoring ${relayCount} relays for routing events`);

      // Publish Thrower Info Document to announce availability
      try {
        this.logger.info('Publishing Thrower Information Document to announce availability...');
        await this.throwerInfoManager.publishThrowerInfo();
        this.logger.info('Thrower availability announced successfully');
      } catch (error) {
        this.logger.error(`Failed to announce thrower availability: ${error.message}`);
      }

      // Start auto-publishing Thrower Info Document
      this.throwerInfoManager.startAutoPublish();

      this.running = true;
      this.logger.info('Superball Thrower Daemon started successfully');

    } catch (error) {
      this.logger.error(`Failed to start daemon: ${error.message}`);
      throw error;
    }
  }

  async stop() {
    if (!this.running) {
      this.logger.warn('Daemon is not running');
      return;
    }

    this.logger.info('Stopping Superball Thrower Daemon...');

    // Stop WebSocket monitoring
    this.wsManager.stopMonitoring();

    // Stop auto-publishing
    this.throwerInfoManager.stopAutoPublish();

    // Clear event queue
    this.eventProcessor.eventQueue = [];
    this.eventProcessor.processedEventIds.clear();

    this.running = false;
    this.logger.info('Superball Thrower Daemon stopped successfully');
  }

  getStatus() {
    const queueStatus = this.eventProcessor.getQueueStatus();
    const relayStatus = {
      total: this.relayManager.relays.length,
      writeCapable: this.relayManager.getWriteCapableRelays().length,
      readCapable: this.relayManager.getReadCapableRelays().length
    };

    return {
      running: this.running,
      publicKey: this.config.get('thrower.publicKey'),
      throwerName: this.config.get('thrower.name'),
      relays: relayStatus,
      queue: queueStatus,
      lastThrowerInfoPublish: this.throwerInfoManager.lastPublish
    };
  }
}

// CLI Interface
async function main() {
  const args = process.argv.slice(2);
  const command = args[0] || 'help';
  const configPath = args.find(arg => arg.startsWith('--config='))?.split('=')[1] || './config.json';

  const daemon = new ThrowerDaemon(configPath);

  switch (command) {
    case 'init':
      await daemon.initialize();
      console.log('Daemon initialized. Edit config.json and run "start" to begin.');
      break;

    case 'start':
      await daemon.initialize();
      await daemon.start();
      
      // Handle graceful shutdown
      process.on('SIGINT', async () => {
        console.log('\nReceived SIGINT, shutting down gracefully...');
        await daemon.stop();
        process.exit(0);
      });

      process.on('SIGTERM', async () => {
        console.log('\nReceived SIGTERM, shutting down gracefully...');
        await daemon.stop();
        process.exit(0);
      });

      // Keep the process running
      setInterval(() => {
        const status = daemon.getStatus();
        if (status.running) {
          console.log(`[${new Date().toISOString()}] Status: Running | Queue: ${status.queue.queueLength} | Processed: ${status.queue.processedEvents}`);
        }
      }, 60000); // Status update every minute

      break;

    case 'stop':
      await daemon.stop();
      break;

    case 'status':
      const status = daemon.getStatus();
      console.log(JSON.stringify(status, null, 2));
      break;

    case 'test-relays':
      await daemon.initialize();
      const results = await daemon.relayManager.testAllRelays();
      console.log(`Relay test results: ${results.readWriteCount} read/write, ${results.readOnlyCount} read-only, ${results.errorCount} errors`);
      break;

    case 'help':
    default:
      console.log(`
Superball Thrower Daemon

Usage: node daemon.js <command> [options]

Commands:
  init                 Initialize daemon with new keypair and default config
  start                Start the thrower daemon
  stop                 Stop the thrower daemon  
  status               Show daemon status
  test-relays          Test relay authentication capabilities
  help                 Show this help message

Options:
  --config=<path>      Path to config file (default: ./config.json)

Examples:
  node daemon.js init
  node daemon.js start
  node daemon.js start --config=/etc/thrower/config.json
  node daemon.js status
      `);
      break;
  }
}

// Run CLI if this file is executed directly
if (require.main === module) {
  main().catch(error => {
    console.error('Fatal error:', error.message);
    process.exit(1);
  });
}

module.exports = { ThrowerDaemon, ThrowerConfig, NostrUtils };