/**
 * Swarm Gossip Protocol
 * 
 * Implements epidemic-style gossip for node and handle propagation.
 * Nodes periodically exchange their known nodes/handles with random peers.
 */

import { db, handleRegistry } from '@/db';
import { desc, gt } from 'drizzle-orm';
import type { SwarmGossipPayload, SwarmGossipResponse, SwarmSyncResult, SwarmNodeInfo } from './types';
import { SWARM_CONFIG } from './types';
import {
  getNodesForGossip,
  getActiveSwarmNodes,
  getNodesSince,
  upsertSwarmNodes,
  markNodeSuccess,
  markNodeFailure,
  logSync,
} from './registry';
import { upsertHandleEntries } from '@/lib/federation/handles';
import { buildAnnouncement } from './discovery';
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';

/**
 * Build a gossip payload to send to another node
 */
export async function buildGossipPayload(since?: string): Promise<SwarmGossipPayload> {
  const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';

  // Get nodes to share
  let nodes: SwarmNodeInfo[];
  if (since) {
    nodes = await getNodesSince(new Date(since), SWARM_CONFIG.maxNodesPerGossip);
  } else {
    nodes = await getActiveSwarmNodes(SWARM_CONFIG.maxNodesPerGossip);
  }

  // Include ourselves in the node list
  const announcement = await buildAnnouncement();
  const selfNode: SwarmNodeInfo = {
    domain: announcement.domain,
    name: announcement.name,
    description: announcement.description,
    logoUrl: announcement.logoUrl,
    publicKey: announcement.publicKey,
    softwareVersion: announcement.softwareVersion,
    userCount: announcement.userCount,
    postCount: announcement.postCount,
    mediaCount: announcement.mediaCount,
    isNsfw: announcement.isNsfw,
    capabilities: announcement.capabilities,
    lastSeenAt: new Date().toISOString(),
  };

  // Get handles to share
  let handles: SwarmGossipPayload['handles'] = [];
  if (db) {
    const sinceDate = since ? new Date(since) : undefined;
    const handleEntries = await db.query.handleRegistry.findMany({
      where: sinceDate ? { updatedAt: { gt: sinceDate } } : undefined,
      orderBy: (handleRegistry, { desc }) => [desc(handleRegistry.updatedAt)],
      limit: SWARM_CONFIG.maxHandlesPerGossip,
    });

    handles = handleEntries.map(h => ({
      handle: h.handle,
      did: h.did,
      nodeDomain: h.nodeDomain,
      updatedAt: h.updatedAt?.toISOString(),
    }));
  }

  return {
    sender: ourDomain,
    nodes: isPublicSwarmDomain(ourDomain) ? [selfNode, ...nodes] : nodes,
    handles: handles.filter((handle) => isPublicSwarmDomain(handle.nodeDomain)),
    timestamp: new Date().toISOString(),
    since,
  };
}

/**
 * Process incoming gossip and return our response
 */
export async function processGossip(
  payload: SwarmGossipPayload
): Promise<SwarmGossipResponse> {
  const startTime = Date.now();

  // Process incoming nodes
  const nodeResult = await upsertSwarmNodes(payload.nodes, payload.sender);

  // Process incoming handles
  let handlesResult = { added: 0, updated: 0 };
  const publicHandles = payload.handles?.filter((handle) => isPublicSwarmDomain(handle.nodeDomain)) ?? [];
  if (publicHandles.length > 0) {
    handlesResult = await upsertHandleEntries(publicHandles);
  }

  // Build our response with nodes/handles to share back
  const responsePayload = await buildGossipPayload(payload.since);

  return {
    nodes: responsePayload.nodes,
    handles: responsePayload.handles,
    received: {
      nodes: nodeResult.added + nodeResult.updated,
      handles: handlesResult.added + handlesResult.updated,
    },
  };
}

/**
 * Send gossip to a specific node
 * 
 * SECURITY: Signs the gossip payload with the node's private key
 */
export async function gossipToNode(
  targetDomain: string,
  since?: string
): Promise<SwarmSyncResult> {
  const startTime = Date.now();
  const publicTargetDomain = getPublicSwarmDomain(targetDomain);

  if (!isPublicSwarmDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN) || !publicTargetDomain) {
    return {
      success: false,
      nodesReceived: 0,
      nodesSent: 0,
      handlesReceived: 0,
      handlesSent: 0,
      error: 'Public swarm participation requires real ICANN domains',
      durationMs: Date.now() - startTime,
    };
  }

  try {
    const payload = await buildGossipPayload(since);

    // SECURITY: Sign the gossip payload with our private key
    const { signPayload, getNodePrivateKey } = await import('./signature');
    const privateKey = await getNodePrivateKey();
    const signature = signPayload(payload, privateKey);

    const signedPayload = {
      ...payload,
      signature,
    };

    const baseUrl = `https://${publicTargetDomain}`;
    const url = `${baseUrl}/api/swarm/gossip`;

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify(signedPayload),
    });

    const durationMs = Date.now() - startTime;

    if (!response.ok) {
      const error = `HTTP ${response.status}`;
      await markNodeFailure(publicTargetDomain);
      await logSync(publicTargetDomain, 'push', {
        success: false,
        nodesReceived: 0,
        nodesSent: payload.nodes.length,
        handlesReceived: 0,
        handlesSent: payload.handles?.length || 0,
        error,
        durationMs,
      });
      return {
        success: false,
        nodesReceived: 0,
        nodesSent: payload.nodes.length,
        handlesReceived: 0,
        handlesSent: payload.handles?.length || 0,
        error,
        durationMs,
      };
    }

    const gossipResponse = await response.json() as SwarmGossipResponse;

    // Process the response (nodes and handles they sent back)
    const nodeResult = await upsertSwarmNodes(gossipResponse.nodes, publicTargetDomain);

    let handlesResult = { added: 0, updated: 0 };
    const publicHandles = gossipResponse.handles?.filter((handle) => isPublicSwarmDomain(handle.nodeDomain)) ?? [];
    if (publicHandles.length > 0) {
      handlesResult = await upsertHandleEntries(publicHandles);
    }

    await markNodeSuccess(publicTargetDomain);

    const result: SwarmSyncResult = {
      success: true,
      nodesReceived: nodeResult.added + nodeResult.updated,
      nodesSent: payload.nodes.length,
      handlesReceived: handlesResult.added + handlesResult.updated,
      handlesSent: payload.handles?.length || 0,
      durationMs,
    };

    await logSync(publicTargetDomain, 'push', result);
    return result;
  } catch (error) {
    const durationMs = Date.now() - startTime;
    const errorMsg = error instanceof Error ? error.message : 'Unknown error';

    await markNodeFailure(publicTargetDomain);

    const result: SwarmSyncResult = {
      success: false,
      nodesReceived: 0,
      nodesSent: 0,
      handlesReceived: 0,
      handlesSent: 0,
      error: errorMsg,
      durationMs,
    };

    await logSync(publicTargetDomain, 'push', result);
    return result;
  }
}

/**
 * Run a gossip round - contact random nodes and exchange info
 */
export async function runGossipRound(): Promise<{
  contacted: number;
  successful: number;
  totalNodesReceived: number;
  totalHandlesReceived: number;
}> {
  if (!isPublicSwarmDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN)) {
    return { contacted: 0, successful: 0, totalNodesReceived: 0, totalHandlesReceived: 0 };
  }

  // Get random nodes to gossip with
  const targets = await getNodesForGossip(SWARM_CONFIG.gossipFanout);

  let contacted = 0;
  let successful = 0;
  let totalNodesReceived = 0;
  let totalHandlesReceived = 0;

  for (const target of targets) {
    contacted++;
    const result = await gossipToNode(target.domain);

    if (result.success) {
      successful++;
      totalNodesReceived += result.nodesReceived;
      totalHandlesReceived += result.handlesReceived;
    }
  }

  return {
    contacted,
    successful,
    totalNodesReceived,
    totalHandlesReceived,
  };
}
