import { Command } from 'commander';
import {
  createNode,
  importKeyPair,
  createAgentCard,
  signAgentCard,
  createDHTOperations,
  createMessageRouter,
  sign,
  verify,
  extractPublicKey,
} 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 bootstrapPeerIds = bootstrapPeers
          .map((addr: string) => addr.split('/p2p/')[1])
          .filter((peerId: string | undefined): peerId is string => Boolean(peerId));

        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)
        let connected = false;
        await new Promise<void>((resolve) => {
          const timeout = setTimeout(resolve, 10000);
          node.libp2p.addEventListener('peer:connect', () => {
            connected = true;
            clearTimeout(timeout);
            resolve();
          }, { once: true });
        });

        // If discovery has not connected us yet, proactively dial bootstrap peers once.
        if (!connected && bootstrapPeers.length > 0) {
          info('No peer discovered yet, dialing bootstrap peers...');
          for (const bootstrapAddr of bootstrapPeers) {
            try {
              await node.libp2p.dial(bootstrapAddr);
              connected = true;
              break;
            } catch {
              // try next bootstrap peer
            }
          }
        }

        // Phase 2: wait for relay reservation using event + polling (more reliable across libp2p versions)
        const countRelayAddrs = () => node.getMultiaddrs().filter(a => a.includes('/p2p-circuit')).length;
        const initialRelayCount = countRelayAddrs();

        info(`Initial relay addresses: ${initialRelayCount}`);
        info('Waiting for relay reservation...');

        let reservationSucceeded = false;
        if (initialRelayCount === 0) {
          await new Promise<void>((resolve) => {
            const RELAY_WAIT_MS = 30000;
            const POLL_MS = 500;
            let settled = false;

            const finish = () => {
              if (settled) return;
              settled = true;
              clearTimeout(timeout);
              clearInterval(pollTimer);
              (node.libp2p as any).removeEventListener('relay:reservation', onReservation);
              (node.libp2p as any).removeEventListener('self:peer:update', onPeerUpdate);
              resolve();
            };

            const onReservation = () => {
              reservationSucceeded = true;
              info('✓ Relay reservation successful!');
              setTimeout(finish, 300);
            };

            const onPeerUpdate = () => {
              if (countRelayAddrs() > 0) {
                reservationSucceeded = true;
                info('✓ Relay address detected from peer update');
                finish();
              }
            };

            const pollTimer = setInterval(() => {
              if (countRelayAddrs() > 0) {
                reservationSucceeded = true;
                info('✓ Relay address detected');
                finish();
              }
            }, POLL_MS);

            const timeout = setTimeout(() => {
              if (!reservationSucceeded) {
                info(`Relay reservation timeout after ${RELAY_WAIT_MS / 1000}s.`);
                info(`Connected peers: ${node.libp2p.getPeers().map(p => p.toString()).join(', ')}`);
              }
              finish();
            }, RELAY_WAIT_MS);

            // Event names vary by libp2p internals; keep as any to avoid typing mismatch.
            (node.libp2p as any).addEventListener('relay:reservation', onReservation);
            (node.libp2p as any).addEventListener('self:peer:update', onPeerUpdate);
          });
        } else {
          reservationSucceeded = true;
          info('✓ Relay address already present');
        }

        if (!reservationSucceeded) {
          info('⚠ Relay reservation did not complete, continuing with fallback relay addresses');
        }

        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: string) => `${r}/p2p-circuit/p2p/${node.getPeerId()}`)
          : [];
        const allAddrs = [...directAddrs, ...relayAddrs, ...fallbackRelayAddrs];

        const capabilities = (card.capabilities ?? []).map((capability: string) => ({
          id: capability,
          name: capability,
          description: `Capability: ${capability}`,
        }));
        if (options.relay) {
          capabilities.push({
            id: 'relay',
            name: 'relay',
            description: 'Provides circuit relay service for NAT traversal',
          });
        }

        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 bootstrap connectivity stable: proactively re-dial missing bootstrap peers.
        const ensureBootstrapConnections = async () => {
          const connections = node.libp2p.getConnections();
          const connectedPeerIds = new Set(connections.map((conn) => conn.remotePeer.toString()));

          for (const bootstrapAddr of bootstrapPeers) {
            const targetPeerId = bootstrapAddr.split('/p2p/')[1];
            if (!targetPeerId || connectedPeerIds.has(targetPeerId)) continue;

            try {
              await node.libp2p.dial(bootstrapAddr);
              info(`Reconnected bootstrap peer: ${targetPeerId}`);
            } catch {
              // best effort; keep trying on next tick
            }
          }
        };

        // Keep connection alive by pinging peers periodically
        const pingInterval = setInterval(async () => {
          const peers = node.libp2p.getPeers();
          if (peers.length === 0) {
            // No peers connected, try to reconnect to bootstrap
            for (const bootstrapAddr of bootstrapPeers) {
              try {
                await node.libp2p.dial(bootstrapAddr);
                info('Reconnected to bootstrap peer');
                break;
              } catch {
                // try next bootstrap peer
              }
            }
          } else {
            await ensureBootstrapConnections();
            // Ping existing peers to keep connection alive
            for (const peer of peers) {
              try {
                await (node.libp2p.services.ping as any).ping(peer);
              } catch {
                // ignore ping failures
              }
            }
          }
        }, 15000); // ping every 15s (reduced from 30s for better stability)

        // If a bootstrap peer disconnects, attempt immediate recovery.
        const onPeerDisconnect = async (evt: any) => {
          const disconnectedPeerId = evt?.detail?.toString?.() ?? '';
          if (!bootstrapPeerIds.includes(disconnectedPeerId)) return;

          info(`Bootstrap peer disconnected: ${disconnectedPeerId}, attempting reconnect...`);
          for (const bootstrapAddr of bootstrapPeers) {
            if (!bootstrapAddr.endsWith(`/p2p/${disconnectedPeerId}`)) continue;
            try {
              await node.libp2p.dial(bootstrapAddr);
              info(`Recovered bootstrap connection: ${disconnectedPeerId}`);
              return;
            } catch {
              // Continue trying other bootstrap peers if available
            }
          }
        };
        node.libp2p.addEventListener('peer:disconnect', onPeerDisconnect);

        const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
          try {
            const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
            if (!decoded.from || typeof decoded.from !== 'string') return false;
            const senderPublicKey = extractPublicKey(decoded.from);
            return verify(signature, data, senderPublicKey);
          } catch {
            return false;
          }
        };

        // Register message handlers for incoming messages
        const router = createMessageRouter(
          node.libp2p,
          verifyFn,
          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);
          node.libp2p.removeEventListener('peer:disconnect', onPeerDisconnect);
          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);
      }
    });
}
