import { Command } from 'commander';
import {
  createNode,
  importKeyPair,
  createAgentCard,
  signAgentCard,
  createDHTOperations,
  createMessageRouter,
  sign,
} from '@highway1/core';
import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
import { success, error, spinner, printHeader, info } from '../ui.js';

export function registerJoinCommand(program: Command): void {
  program
    .command('join')
    .description('Join the Clawiverse network')
    .option('--bootstrap <peers...>', 'Bootstrap peer addresses')
    .option('--relay', 'Run as a relay server and advertise relay capability')
    .action(async (options) => {
      try {
        printHeader('Join Clawiverse Network');

        const identity = getIdentity();
        const card = getAgentCard();

        if (!identity || !card) {
          error('No identity found. Run "hw1 init" first.');
          process.exit(1);
        }

        const spin = spinner('Starting libp2p node...');

        const keyPair = importKeyPair({
          publicKey: identity.publicKey,
          privateKey: identity.privateKey,
        });

        const bootstrapPeers = options.bootstrap || getBootstrapPeers();

        const node = await createNode({
          keyPair,
          bootstrapPeers,
          enableDHT: true,
          reserveRelaySlot: true,
          enableRelay: options.relay ?? false,
        });

        await node.start();

        spin.succeed('Node started successfully!');

        info(`Peer ID: ${node.getPeerId()}`);
        info(`DID: ${identity.did}`);
        info(`Listening on: ${node.getMultiaddrs().join(', ')}`);

        // Wait for bootstrap peer connection before publishing to DHT
        const connectSpin = spinner('Connecting to bootstrap peers...');

        // Phase 1: wait for peer:connect (up to 10s)
        await new Promise<void>((resolve) => {
          const timeout = setTimeout(resolve, 10000);
          node.libp2p.addEventListener('peer:connect', () => {
            clearTimeout(timeout);
            resolve();
          }, { once: true });
        });

        // Phase 2: wait for relay reservation (up to 10s)
        const hasRelayAddr = () => node.getMultiaddrs().some(a => a.includes('/p2p-circuit'));
        if (!hasRelayAddr()) {
          info('Waiting for relay reservation...');
          await new Promise<void>((resolve) => {
            const timeout = setTimeout(() => {
              info(`Relay reservation timeout after 10s. Multiaddrs: ${node.getMultiaddrs().join(', ')}`);
              resolve();
            }, 10000);

            // Listen for relay:reservation event (fires when reservation succeeds)
            const onReservation = () => {
              info(`Relay reservation successful!`);
              clearTimeout(timeout);
              // Wait a bit for multiaddrs to update
              setTimeout(() => {
                info(`Relay addresses: ${node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).join(', ')}`);
                resolve();
              }, 500);
            };

            // Also listen for self:peer:update as fallback
            const onPeerUpdate = () => {
              if (hasRelayAddr()) {
                info(`Relay address acquired via peer update`);
                clearTimeout(timeout);
                resolve();
              }
            };

            node.libp2p.addEventListener('relay:reservation', onReservation, { once: true });
            node.libp2p.addEventListener('self:peer:update', onPeerUpdate);

            setTimeout(() => {
              node.libp2p.removeEventListener('relay:reservation', onReservation);
              node.libp2p.removeEventListener('self:peer:update', onPeerUpdate);
            }, 10000);
          });
        } else {
          info(`Relay address already present: ${node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).join(', ')}`);
        }

        connectSpin.succeed('Connected to network!');

        // Publish Agent Card with peerId and multiaddrs so others can dial us
        const cardSpin = spinner('Publishing Agent Card to DHT...');

        // Use relay addresses from libp2p (populated after reservation), fall back to manual construction
        const directAddrs = node.getMultiaddrs().filter(a => !a.includes('/p2p-circuit'));
        const relayAddrs = node.getMultiaddrs().filter(a => a.includes('/p2p-circuit'));
        const fallbackRelayAddrs = relayAddrs.length === 0
          ? bootstrapPeers.map(r => `${r}/p2p-circuit/p2p/${node.getPeerId()}`)
          : [];
        const allAddrs = [...directAddrs, ...relayAddrs, ...fallbackRelayAddrs];

        const capabilities = [...(card.capabilities ?? [])];
        if (options.relay) capabilities.push('relay');

        const agentCard = createAgentCard(
          identity.did,
          card.name,
          card.description,
          capabilities,
          allAddrs,
          node.getPeerId()
        );

        const signedCard = await signAgentCard(agentCard, (data) =>
          sign(data, keyPair.privateKey)
        );

        const dht = createDHTOperations(node.libp2p);
        await dht.publishAgentCard(signedCard);

        cardSpin.succeed('Agent Card published!');

        // Keep connection alive by pinging bootstrap peers periodically
        const pingInterval = setInterval(async () => {
          for (const peer of node.libp2p.getPeers()) {
            try {
              await node.libp2p.services.ping.ping(peer);
            } catch {
              // ignore ping failures
            }
          }
        }, 30000); // ping every 30s

        // Register message handlers for incoming messages
        const router = createMessageRouter(
          node.libp2p,
          async () => true,
          dht
        );

        // Generic message handler that accepts any protocol
        const messageHandler = async (envelope: any) => {
          const payload = envelope.payload as Record<string, unknown>;
          console.log();
          success(`>>> Received message from ${envelope.from}`);
          info(`    Message ID: ${envelope.id}`);
          info(`    Protocol:   ${envelope.protocol}`);
          info(`    Type:       ${envelope.type}`);
          info(`    Payload:    ${JSON.stringify(payload, null, 2)}`);
          console.log();

          // If this is a request, send back a simple acknowledgment response
          if (envelope.type === 'request') {
            info('    Sending acknowledgment response...');

            const { createEnvelope, signEnvelope, sign } = await import('@highway1/core');
            const identity = getIdentity();
            const keyPair = importKeyPair({
              publicKey: identity!.publicKey,
              privateKey: identity!.privateKey,
            });

            const responseEnvelope = createEnvelope(
              envelope.to,      // We are the recipient, now we're the sender
              envelope.from,    // Original sender is now the recipient
              'response',
              envelope.protocol,
              {
                status: 'received',
                message: 'Message received and processed',
                originalPayload: payload,
                timestamp: Date.now()
              },
              envelope.id       // replyTo: original message ID
            );

            const signedResponse = await signEnvelope(responseEnvelope, (data) =>
              sign(data, keyPair.privateKey)
            );

            return signedResponse;
          }

          return undefined;
        };

        // Register handlers for common protocols
        router.registerHandler('/clawiverse/msg/1.0.0', messageHandler);
        router.registerHandler('/clawiverse/greet/1.0.0', messageHandler);

        // Register catch-all handler for any other protocol
        router.registerCatchAllHandler(messageHandler);

        await router.start();

        console.log();
        success('Successfully joined the Clawiverse network!');
        info('Listening for incoming messages...');
        info('Press Ctrl+C to stop');

        process.on('SIGINT', async () => {
          console.log();
          const stopSpin = spinner('Stopping node...');
          clearInterval(pingInterval);
          await router.stop();
          await node.stop();
          stopSpin.succeed('Node stopped');
          process.exit(0);
        });

        await new Promise(() => {});
      } catch (err) {
        error(`Failed to join network: ${(err as Error).message}`);
        process.exit(1);
      }
    });
}
