const net = require('net');
const { sha256 } = require('./hash');
const { envOrConfigList, envOrConfigNumber, envOrConfigString, getNetworkConfig } = require('./config');
const {
  base1NodeInfo,
  getBase1BlocksSince,
  getBase1ChainSummary,
  getBase1HeadProof,
  receiveBase1SolveCandidatePackage,
  recordBase1NetworkMessage,
  submitBase1Blocks,
} = require('./base1_state');

const P2P_PROTOCOL = 'mazexchain-p2p/1';
const P2P_VERSION = 1;
const NETWORK_CONFIG = getNetworkConfig().network || {};
const NETWORK_LIMITS = getNetworkConfig().limits || {};
const P2P_MAX_MESSAGE_BYTES = envOrConfigNumber('MAZEX_P2P_MAX_MESSAGE_BYTES', NETWORK_LIMITS.max_message_bytes, 256 * 1024, {
  min: 4096,
  integer: true,
});
const P2P_MAX_PEERS = envOrConfigNumber('MAZEX_P2P_MAX_PEERS', NETWORK_LIMITS.max_peers, 64, {
  min: 1,
  integer: true,
});
const P2P_RATE_WINDOW_MS = envOrConfigNumber('MAZEX_P2P_RATE_WINDOW_MS', NETWORK_LIMITS.rate_window_ms, 60_000, {
  min: 1000,
  integer: true,
});
const P2P_MAX_MESSAGES_PER_WINDOW = envOrConfigNumber(
  'MAZEX_P2P_MAX_MESSAGES_PER_WINDOW',
  NETWORK_LIMITS.max_messages_per_window,
  240,
  { min: 1, integer: true },
);
const P2P_BAN_MS = envOrConfigNumber('MAZEX_P2P_BAN_MS', NETWORK_LIMITS.p2p_ban_ms, 120_000, {
  min: 1000,
  integer: true,
});
const P2P_WATCH_INTERVAL_MS = Math.max(250, Math.trunc(Number(process.env.MAZEX_P2P_WATCH_INTERVAL_MS || 1000)));
const P2P_SEEN_MESSAGE_LIMIT = Math.max(100, Math.trunc(Number(process.env.MAZEX_P2P_SEEN_MESSAGE_LIMIT || 5000)));

function nowIso() {
  return new Date().toISOString();
}

function normalizePeerAddress(address) {
  const raw = String(address || '').trim();
  if (!raw) return null;
  const cleaned = raw.startsWith('tcp://') ? raw.slice('tcp://'.length) : raw;
  const [host, portText] = cleaned.split(':');
  const port = Math.trunc(Number(portText || 0));
  if (!host || !port) return null;
  return {
    host,
    port,
    address: `tcp://${host}:${port}`,
  };
}

function parseSeedPeers(value = envOrConfigList('MAZEX_P2P_SEED_PEERS', NETWORK_CONFIG.seed_peers, [])) {
  const peers = Array.isArray(value) ? value : String(value || '').split(',');
  return peers
    .map((item) => normalizePeerAddress(item))
    .filter(Boolean);
}

function compareHeadProof(left = {}, right = {}) {
  const leftProgress = [Number(left.current_layer || 0), Number(left.current_step || 0)];
  const rightProgress = [Number(right.current_layer || 0), Number(right.current_step || 0)];
  if (leftProgress[0] !== rightProgress[0]) return leftProgress[0] > rightProgress[0] ? 1 : -1;
  if (leftProgress[1] !== rightProgress[1]) return leftProgress[1] > rightProgress[1] ? 1 : -1;
  const leftWork = Number(left.cumulative_work_score || 0);
  const rightWork = Number(right.cumulative_work_score || 0);
  if (leftWork !== rightWork) return leftWork > rightWork ? 1 : -1;
  const leftHeight = Number(left.height || 0);
  const rightHeight = Number(right.height || 0);
  if (leftHeight !== rightHeight) return leftHeight > rightHeight ? 1 : -1;
  return String(left.head_hash || '').localeCompare(String(right.head_hash || '')) * -1;
}

class Base1P2PNode {
  constructor({
    host = envOrConfigString('MAZEX_P2P_HOST', NETWORK_CONFIG.p2p_host, '0.0.0.0'),
    port = envOrConfigNumber('MAZEX_P2P_PORT', NETWORK_CONFIG.p2p_port, 0, { min: 0, max: 65535, integer: true }),
    seedPeers = parseSeedPeers(),
  } = {}) {
    this.host = host;
    this.port = Math.trunc(Number(port || 0));
    this.seedPeers = seedPeers;
    this.server = null;
    this.peers = new Map();
    this.banned = new Map();
    this.seenMessages = new Map();
    this.lastHeadHash = null;
    this.watchTimer = null;
    this.startedAt = null;
  }

  start() {
    if (this.server) return this;
    if (!this.port) throw new Error('MAZEX_P2P_PORT is required when P2P is enabled');
    this.server = net.createServer((socket) => this.acceptSocket(socket));
    this.server.listen(this.port, this.host, () => {
      this.startedAt = nowIso();
      this.lastHeadHash = getBase1ChainSummary().head_hash;
      recordBase1NetworkMessage({
        direction: 'local',
        peer: `tcp://${this.host}:${this.port}`,
        type: 'P2P_LISTEN',
        status: 'started',
        detail: { protocol: P2P_PROTOCOL },
      });
      for (const peer of this.seedPeers) this.connect(peer.address);
      this.watchTimer = setInterval(() => this.broadcastHeadIfChanged(), P2P_WATCH_INTERVAL_MS);
    });
    return this;
  }

  stop() {
    if (this.watchTimer) clearInterval(this.watchTimer);
    this.watchTimer = null;
    for (const peer of this.peers.values()) peer.socket.destroy();
    this.peers.clear();
    if (this.server) this.server.close();
    this.server = null;
  }

  status() {
    return {
      enabled: Boolean(this.server),
      protocol: P2P_PROTOCOL,
      version: P2P_VERSION,
      listen: this.server ? `tcp://${this.host}:${this.port}` : null,
      started_at: this.startedAt,
      peer_count: this.peers.size,
      peers: Array.from(this.peers.values()).map((peer) => ({
        id: peer.id,
        address: peer.address,
        direction: peer.direction,
        connected_at: peer.connected_at,
        last_seen_at: peer.last_seen_at,
        messages_in: peer.messages_in,
        messages_out: peer.messages_out,
        invalid_count: peer.invalid_count,
      })),
      security: {
        max_message_bytes: P2P_MAX_MESSAGE_BYTES,
        max_peers: P2P_MAX_PEERS,
        rate_window_ms: P2P_RATE_WINDOW_MS,
        max_messages_per_window: P2P_MAX_MESSAGES_PER_WINDOW,
        ban_ms: P2P_BAN_MS,
      },
    };
  }

  connect(address) {
    const peerAddress = normalizePeerAddress(address);
    if (!peerAddress) return { ok: false, code: 'INVALID_P2P_PEER_ADDRESS' };
    if (this.isBanned(peerAddress.address)) return { ok: false, code: 'P2P_PEER_BANNED', peer: peerAddress.address };
    if (this.peers.has(peerAddress.address)) return { ok: true, status: 'already_connected', peer: peerAddress.address };
    const socket = net.createConnection({ host: peerAddress.host, port: peerAddress.port }, () => {
      this.attachSocket(socket, {
        address: peerAddress.address,
        direction: 'outbound',
      });
      this.send(socket, 'HELLO', {
        node: base1NodeInfo(),
        head: getBase1HeadProof(),
      });
    });
    socket.on('error', (error) => {
      recordBase1NetworkMessage({
        direction: 'outbound',
        peer: peerAddress.address,
        type: 'P2P_CONNECT',
        status: 'failed',
        detail: { error: error.message },
      });
    });
    return { ok: true, status: 'connecting', peer: peerAddress.address };
  }

  acceptSocket(socket) {
    const address = `tcp://${socket.remoteAddress}:${socket.remotePort}`;
    if (this.peers.size >= P2P_MAX_PEERS || this.isBanned(address)) {
      socket.destroy();
      return;
    }
    this.attachSocket(socket, {
      address,
      direction: 'inbound',
    });
  }

  attachSocket(socket, { address, direction }) {
    socket.setNoDelay(true);
    const peer = {
      id: sha256({ address, direction, connected_at: nowIso() }).slice(0, 18),
      address,
      direction,
      socket,
      buffer: '',
      connected_at: nowIso(),
      last_seen_at: nowIso(),
      messages_in: 0,
      messages_out: 0,
      invalid_count: 0,
      rate_count: 0,
      rate_reset_at: Date.now() + P2P_RATE_WINDOW_MS,
    };
    this.peers.set(address, peer);
    socket.on('data', (chunk) => this.onData(peer, chunk));
    socket.on('close', () => this.peers.delete(address));
    socket.on('error', (error) => this.markInvalid(peer, 'SOCKET_ERROR', { error: error.message }));
    recordBase1NetworkMessage({
      direction,
      peer: address,
      type: 'P2P_PEER_CONNECTED',
      status: 'connected',
      detail: { id: peer.id },
    });
    if (direction === 'inbound') {
      this.send(socket, 'HELLO', {
        node: base1NodeInfo(),
        head: getBase1HeadProof(),
      });
    }
  }

  onData(peer, chunk) {
    peer.buffer += chunk.toString('utf8');
    if (Buffer.byteLength(peer.buffer, 'utf8') > P2P_MAX_MESSAGE_BYTES) {
      this.markInvalid(peer, 'P2P_MESSAGE_TOO_LARGE');
      this.ban(peer.address, 'P2P_MESSAGE_TOO_LARGE');
      peer.socket.destroy();
      return;
    }
    let index = peer.buffer.indexOf('\n');
    while (index >= 0) {
      const line = peer.buffer.slice(0, index).trim();
      peer.buffer = peer.buffer.slice(index + 1);
      if (line) this.handleRawMessage(peer, line);
      index = peer.buffer.indexOf('\n');
    }
  }

  handleRawMessage(peer, line) {
    if (!this.takePeerToken(peer)) return;
    if (Buffer.byteLength(line, 'utf8') > P2P_MAX_MESSAGE_BYTES) {
      this.markInvalid(peer, 'P2P_MESSAGE_TOO_LARGE');
      return;
    }
    let message;
    try {
      message = JSON.parse(line);
    } catch {
      this.markInvalid(peer, 'P2P_INVALID_JSON');
      return;
    }
    this.handleMessage(peer, message);
  }

  takePeerToken(peer) {
    const now = Date.now();
    if (peer.rate_reset_at <= now) {
      peer.rate_count = 0;
      peer.rate_reset_at = now + P2P_RATE_WINDOW_MS;
    }
    peer.rate_count += 1;
    if (peer.rate_count > P2P_MAX_MESSAGES_PER_WINDOW) {
      this.markInvalid(peer, 'P2P_RATE_LIMITED');
      this.ban(peer.address, 'P2P_RATE_LIMITED');
      peer.socket.destroy();
      return false;
    }
    return true;
  }

  handleMessage(peer, message) {
    if (message.protocol !== P2P_PROTOCOL || Number(message.version) !== P2P_VERSION) {
      this.markInvalid(peer, 'P2P_PROTOCOL_MISMATCH');
      return;
    }
    if (!message.id || this.seenMessages.has(message.id)) return;
    this.rememberMessage(message.id);
    peer.messages_in += 1;
    peer.last_seen_at = nowIso();
    const payload = message.payload || {};
    if (message.type === 'HELLO') {
      this.onPeerHead(peer, payload.head);
      this.send(peer.socket, 'HEAD', getBase1HeadProof());
    } else if (message.type === 'PING') {
      this.send(peer.socket, 'PONG', { at: nowIso(), head: getBase1HeadProof() });
    } else if (message.type === 'HEAD_REQUEST') {
      this.send(peer.socket, 'HEAD', getBase1HeadProof());
    } else if (message.type === 'HEAD') {
      this.onPeerHead(peer, payload);
    } else if (message.type === 'BLOCKS_REQUEST') {
      const response = getBase1BlocksSince({
        afterHeight: payload.after_height ?? payload.afterHeight ?? -1,
        afterHash: payload.after_hash || payload.afterHash || null,
        limit: payload.limit || 100,
      });
      this.send(peer.socket, 'BLOCKS_RESPONSE', response);
    } else if (message.type === 'BLOCKS_RESPONSE') {
      const result = submitBase1Blocks({ blocks: payload.blocks || [], source: peer.address });
      recordBase1NetworkMessage({
        direction: 'inbound',
        peer: peer.address,
        type: 'P2P_BLOCKS_RESPONSE',
        status: result.ok ? 'accepted' : result.code || 'rejected',
        detail: { block_count: (payload.blocks || []).length, adopted: result.adopted || false },
      });
      if (result.ok && result.adopted) this.broadcastHead();
    } else if (message.type === 'SOLVE_CANDIDATE') {
      const result = receiveBase1SolveCandidatePackage({
        solver: payload.solver || 'p2p-solver',
        candidateStep: payload.candidateStep || payload.candidate || null,
        expectedHead: payload.expectedHead || payload.expected_head || null,
        verifierOptions: payload.verifierOptions || {},
        networkTimeProof: payload.networkTimeProof || payload.network_time_proof || null,
        signedPayloadHash: payload.signedPayloadHash || payload.signed_payload_hash || null,
        answerHash: payload.answer_hash || payload.answerHash || null,
        source: peer.address,
      });
      this.send(peer.socket, 'SOLVE_CANDIDATE_RESULT', result);
    } else if (message.type === 'SOLVE_CANDIDATE_RESULT') {
      recordBase1NetworkMessage({
        direction: 'inbound',
        peer: peer.address,
        type: 'P2P_SOLVE_CANDIDATE_RESULT',
        status: payload.ok ? 'accepted' : payload.code || 'rejected',
        detail: payload,
      });
    } else {
      this.markInvalid(peer, 'P2P_UNKNOWN_MESSAGE_TYPE', { type: message.type });
    }
  }

  onPeerHead(peer, head) {
    if (!head?.head_hash) return;
    recordBase1NetworkMessage({
      direction: 'inbound',
      peer: peer.address,
      type: 'P2P_HEAD',
      status: 'received',
      detail: { height: head.height, head_hash: head.head_hash },
    });
    const local = getBase1HeadProof();
    if (compareHeadProof(head, local) > 0) {
      this.send(peer.socket, 'BLOCKS_REQUEST', {
        after_height: local.height,
        after_hash: local.head_hash,
        limit: 100,
      });
    }
  }

  send(socket, type, payload = {}) {
    const peer = Array.from(this.peers.values()).find((item) => item.socket === socket);
    if (!peer || socket.destroyed) return false;
    const envelope = {
      protocol: P2P_PROTOCOL,
      version: P2P_VERSION,
      id: sha256({ type, payload, source: base1NodeInfo().node_id, created_at: nowIso(), nonce: Math.random() }),
      type,
      source: base1NodeInfo().node_id,
      created_at: nowIso(),
      payload,
    };
    const raw = `${JSON.stringify(envelope)}\n`;
    if (Buffer.byteLength(raw, 'utf8') > P2P_MAX_MESSAGE_BYTES) {
      recordBase1NetworkMessage({
        direction: 'outbound',
        peer: peer.address,
        type: `P2P_${type}`,
        status: 'not_sent_too_large',
        detail: { max_message_bytes: P2P_MAX_MESSAGE_BYTES },
      });
      return false;
    }
    peer.messages_out += 1;
    socket.write(raw);
    return true;
  }

  broadcast(type, payload = {}) {
    let sent = 0;
    for (const peer of this.peers.values()) {
      if (this.send(peer.socket, type, payload)) sent += 1;
    }
    return { ok: true, type, sent, peer_count: this.peers.size };
  }

  broadcastHead() {
    return this.broadcast('HEAD', getBase1HeadProof());
  }

  broadcastHeadIfChanged() {
    const head = getBase1ChainSummary();
    if (head.head_hash && head.head_hash !== this.lastHeadHash) {
      this.lastHeadHash = head.head_hash;
      this.broadcastHead();
    }
  }

  rememberMessage(id) {
    this.seenMessages.set(id, Date.now());
    if (this.seenMessages.size > P2P_SEEN_MESSAGE_LIMIT) {
      const oldest = Array.from(this.seenMessages.keys()).slice(0, this.seenMessages.size - P2P_SEEN_MESSAGE_LIMIT);
      for (const key of oldest) this.seenMessages.delete(key);
    }
  }

  isBanned(address) {
    const until = this.banned.get(address) || 0;
    if (until > Date.now()) return true;
    if (until) this.banned.delete(address);
    return false;
  }

  ban(address, reason) {
    this.banned.set(address, Date.now() + P2P_BAN_MS);
    recordBase1NetworkMessage({
      direction: 'local',
      peer: address,
      type: 'P2P_PEER_BANNED',
      status: 'banned',
      detail: { reason, ban_ms: P2P_BAN_MS },
    });
  }

  markInvalid(peer, code, detail = {}) {
    peer.invalid_count += 1;
    recordBase1NetworkMessage({
      direction: 'inbound',
      peer: peer.address,
      type: 'P2P_INVALID_MESSAGE',
      status: code,
      detail,
    });
  }
}

function createBase1P2PNode(options = {}) {
  return new Base1P2PNode(options);
}

module.exports = {
  Base1P2PNode,
  createBase1P2PNode,
  normalizePeerAddress,
  parseSeedPeers,
};
