import {ForkDigestContext} from "@lodestar/config";
import {
  ATTESTATION_SUBNET_COUNT,
  ForkName,
  ForkSeq,
  SYNC_COMMITTEE_SUBNET_COUNT,
  isForkPostAltair,
  isForkPostElectra,
  isForkPostFulu,
} from "@lodestar/params";
import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types";
import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js";
import {NetworkConfig} from "../networkConfig.js";
import {DEFAULT_ENCODING} from "./constants.js";
import {GossipEncoding, GossipTopic, GossipTopicTypeMap, GossipType, SSZTypeOfGossipTopic} from "./interface.js";

export interface IGossipTopicCache {
  getTopic(topicStr: string): GossipTopic;
}

export class GossipTopicCache implements IGossipTopicCache {
  private topicsByTopicStr = new Map<string, Required<GossipTopic>>();

  constructor(private readonly forkDigestContext: ForkDigestContext) {}

  /** Returns cached GossipTopic, otherwise attempts to parse it from the str */
  getTopic(topicStr: string): GossipTopic {
    let topic = this.topicsByTopicStr.get(topicStr);
    if (topic === undefined) {
      topic = parseGossipTopic(this.forkDigestContext, topicStr);
      // TODO: Consider just throwing here. We should only receive messages from known subscribed topics
      this.topicsByTopicStr.set(topicStr, topic);
    }
    return topic;
  }

  /** Returns cached GossipTopic, otherwise returns undefined */
  getKnownTopic(topicStr: string): GossipTopic | undefined {
    return this.topicsByTopicStr.get(topicStr);
  }

  setTopic(topicStr: string, topic: GossipTopic): void {
    if (!this.topicsByTopicStr.has(topicStr)) {
      this.topicsByTopicStr.set(topicStr, {encoding: DEFAULT_ENCODING, ...topic});
    }
  }
}

/**
 * Stringify a GossipTopic into a spec-ed formated topic string
 */
export function stringifyGossipTopic(forkDigestContext: ForkDigestContext, topic: GossipTopic): string {
  const forkDigestHexNoPrefix = forkDigestContext.forkBoundary2ForkDigestHex(topic.boundary);
  const topicType = stringifyGossipTopicType(topic);
  const encoding = topic.encoding ?? DEFAULT_ENCODING;
  return `/eth2/${forkDigestHexNoPrefix}/${topicType}/${encoding}`;
}

/**
 * Stringify a GossipTopic into a spec-ed formated partial topic string
 */
function stringifyGossipTopicType(topic: GossipTopic): string {
  switch (topic.type) {
    case GossipType.beacon_block:
    case GossipType.beacon_aggregate_and_proof:
    case GossipType.voluntary_exit:
    case GossipType.proposer_slashing:
    case GossipType.attester_slashing:
    case GossipType.sync_committee_contribution_and_proof:
    case GossipType.light_client_finality_update:
    case GossipType.light_client_optimistic_update:
    case GossipType.bls_to_execution_change:
    case GossipType.execution_payload:
    case GossipType.payload_attestation_message:
    case GossipType.execution_payload_bid:
    case GossipType.proposer_preferences:
      return topic.type;
    case GossipType.beacon_attestation:
    case GossipType.sync_committee:
      return `${topic.type}_${topic.subnet}`;
    case GossipType.blob_sidecar:
      return `${topic.type}_${topic.subnet}`;
    case GossipType.data_column_sidecar:
      return `${topic.type}_${topic.subnet}`;
  }
}

export function getGossipSSZType(topic: GossipTopic) {
  const {fork} = topic.boundary;
  switch (topic.type) {
    case GossipType.beacon_block:
      // beacon_block is updated in altair to support the updated SignedBeaconBlock type
      return ssz[fork].SignedBeaconBlock;
    case GossipType.blob_sidecar:
      return ssz.deneb.BlobSidecar;
    case GossipType.data_column_sidecar:
      return isForkPostFulu(fork) ? sszTypesFor(fork).DataColumnSidecar : ssz.fulu.DataColumnSidecar;
    case GossipType.beacon_aggregate_and_proof:
      return sszTypesFor(fork).SignedAggregateAndProof;
    case GossipType.beacon_attestation:
      return sszTypesFor(fork).SingleAttestation;
    case GossipType.proposer_slashing:
      return ssz.phase0.ProposerSlashing;
    case GossipType.attester_slashing:
      return sszTypesFor(fork).AttesterSlashing;
    case GossipType.voluntary_exit:
      return ssz.phase0.SignedVoluntaryExit;
    case GossipType.sync_committee_contribution_and_proof:
      return ssz.altair.SignedContributionAndProof;
    case GossipType.sync_committee:
      return ssz.altair.SyncCommitteeMessage;
    case GossipType.light_client_optimistic_update:
      return isForkPostAltair(fork)
        ? sszTypesFor(fork).LightClientOptimisticUpdate
        : ssz.altair.LightClientOptimisticUpdate;
    case GossipType.light_client_finality_update:
      return isForkPostAltair(fork)
        ? sszTypesFor(fork).LightClientFinalityUpdate
        : ssz.altair.LightClientFinalityUpdate;
    case GossipType.bls_to_execution_change:
      return ssz.capella.SignedBLSToExecutionChange;
    case GossipType.execution_payload:
      return ssz.gloas.SignedExecutionPayloadEnvelope;
    case GossipType.payload_attestation_message:
      return ssz.gloas.PayloadAttestationMessage;
    case GossipType.execution_payload_bid:
      return ssz.gloas.SignedExecutionPayloadBid;
    case GossipType.proposer_preferences:
      return ssz.gloas.SignedProposerPreferences;
  }
}

/**
 * Deserialize a gossip serialized data into an ssz object.
 */
export function sszDeserialize<T extends GossipTopic>(topic: T, serializedData: Uint8Array): SSZTypeOfGossipTopic<T> {
  const sszType = getGossipSSZType(topic);
  try {
    return sszType.deserialize(serializedData) as SSZTypeOfGossipTopic<T>;
  } catch (_e) {
    throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE});
  }
}

/**
 * @deprecated
 * Deserialize a gossip serialized data into an Attestation object.
 * No longer used post-electra. Use `sszDeserializeSingleAttestation` instead
 */
export function sszDeserializeAttestation(fork: ForkName, serializedData: Uint8Array): Attestation {
  try {
    return sszTypesFor(fork).Attestation.deserialize(serializedData);
  } catch (_e) {
    throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE});
  }
}

/**
 * Deserialize a gossip seralized data into an SingleAttestation object.
 */
export function sszDeserializeSingleAttestation(fork: ForkName, serializedData: Uint8Array): SingleAttestation {
  try {
    if (isForkPostElectra(fork)) {
      return sszTypesFor(fork).SingleAttestation.deserialize(serializedData);
    }
    return sszTypesFor(fork).Attestation.deserialize(serializedData) as SingleAttestation;
  } catch (_e) {
    throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE});
  }
}

// Parsing

const gossipTopicRegex = /^\/eth2\/(\w+)\/(\w+)\/(\w+)/;

/**
 * Parse a `GossipTopic` object from its stringified form.
 * A gossip topic has the format
 * ```ts
 * /eth2/$FORK_DIGEST/$GOSSIP_TYPE/$ENCODING
 * ```
 */
export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr: string): Required<GossipTopic> {
  try {
    const matches = topicStr.match(gossipTopicRegex);
    if (matches === null) {
      throw Error(`Must match regex ${gossipTopicRegex}`);
    }

    const [, forkDigestHexNoPrefix, gossipTypeStr, encodingStr] = matches;

    const boundary = forkDigestContext.forkDigest2ForkBoundary(forkDigestHexNoPrefix);
    const encoding = parseEncodingStr(encodingStr);

    // Inline-d the parseGossipTopicType() function since spreading the resulting object x4 the time to parse a topicStr
    switch (gossipTypeStr) {
      case GossipType.beacon_block:
      case GossipType.beacon_aggregate_and_proof:
      case GossipType.voluntary_exit:
      case GossipType.proposer_slashing:
      case GossipType.attester_slashing:
      case GossipType.sync_committee_contribution_and_proof:
      case GossipType.light_client_finality_update:
      case GossipType.light_client_optimistic_update:
      case GossipType.bls_to_execution_change:
      case GossipType.execution_payload:
      case GossipType.payload_attestation_message:
      case GossipType.execution_payload_bid:
      case GossipType.proposer_preferences:
        return {type: gossipTypeStr, boundary, encoding};
    }

    for (const gossipType of [GossipType.beacon_attestation as const, GossipType.sync_committee as const]) {
      if (gossipTypeStr.startsWith(gossipType)) {
        const subnetStr = gossipTypeStr.slice(gossipType.length + 1); // +1 for '_' concatenating the topic name and the subnet
        const subnet = parseInt(subnetStr, 10);
        if (Number.isNaN(subnet)) throw Error(`Subnet ${subnetStr} is not a number`);
        return {type: gossipType, subnet, boundary, encoding};
      }
    }

    if (gossipTypeStr.startsWith(GossipType.blob_sidecar)) {
      const subnetStr = gossipTypeStr.slice(GossipType.blob_sidecar.length + 1); // +1 for '_' concatenating the topic name and the subnet
      const subnet = parseInt(subnetStr, 10);
      if (Number.isNaN(subnet)) throw Error(`subnet ${subnetStr} is not a number`);
      return {type: GossipType.blob_sidecar, subnet, boundary, encoding};
    }

    if (gossipTypeStr.startsWith(GossipType.data_column_sidecar)) {
      const subnetStr = gossipTypeStr.slice(GossipType.data_column_sidecar.length + 1); // +1 for '_' concatenating the topic name and the subnet
      const subnet = parseInt(subnetStr, 10);
      if (Number.isNaN(subnet)) throw Error(`subnet ${subnetStr} is not a number`);
      return {type: GossipType.data_column_sidecar, subnet, boundary, encoding};
    }

    throw Error(`Unknown gossip type ${gossipTypeStr}`);
  } catch (e) {
    (e as Error).message = `Invalid gossip topic ${topicStr}: ${(e as Error).message}`;
    throw e;
  }
}

/**
 * De-duplicate logic to pick fork topics between subscribeCoreTopicsAtFork and unsubscribeCoreTopicsAtFork
 */
export function getCoreTopicsAtFork(
  networkConfig: NetworkConfig,
  fork: ForkName,
  opts: {subscribeAllSubnets?: boolean; disableLightClientServer?: boolean}
): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
  // Common topics for all forks
  const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [
    {type: GossipType.beacon_block},
    {type: GossipType.beacon_aggregate_and_proof},
    {type: GossipType.voluntary_exit},
    {type: GossipType.proposer_slashing},
    {type: GossipType.attester_slashing},
  ];

  if (ForkSeq[fork] >= ForkSeq.gloas) {
    topics.push({type: GossipType.execution_payload});
    topics.push({type: GossipType.payload_attestation_message});
    topics.push({type: GossipType.execution_payload_bid});
    topics.push({type: GossipType.proposer_preferences});
  }

  // After fulu also track data_column_sidecar_{index}
  if (ForkSeq[fork] >= ForkSeq.fulu) {
    topics.push(...getDataColumnSidecarTopics(networkConfig));
  }

  // After Deneb and before Fulu also track blob_sidecar_{subnet_id}
  if (ForkSeq[fork] >= ForkSeq.deneb && ForkSeq[fork] < ForkSeq.fulu) {
    const {config} = networkConfig;
    const subnetCount = isForkPostElectra(fork)
      ? config.BLOB_SIDECAR_SUBNET_COUNT_ELECTRA
      : config.BLOB_SIDECAR_SUBNET_COUNT;

    for (let subnet = 0; subnet < subnetCount; subnet++) {
      topics.push({type: GossipType.blob_sidecar, subnet});
    }
  }

  // capella
  if (ForkSeq[fork] >= ForkSeq.capella) {
    topics.push({type: GossipType.bls_to_execution_change});
  }

  // Any fork after altair included
  if (ForkSeq[fork] >= ForkSeq.altair) {
    topics.push({type: GossipType.sync_committee_contribution_and_proof});
    if (!opts.disableLightClientServer) {
      topics.push({type: GossipType.light_client_optimistic_update});
      topics.push({type: GossipType.light_client_finality_update});
    }
  }

  if (opts.subscribeAllSubnets) {
    for (let subnet = 0; subnet < ATTESTATION_SUBNET_COUNT; subnet++) {
      topics.push({type: GossipType.beacon_attestation, subnet});
    }
    if (ForkSeq[fork] >= ForkSeq.altair) {
      for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) {
        topics.push({type: GossipType.sync_committee, subnet});
      }
    }
  }

  return topics;
}

/**
 * Pick data column subnets to subscribe to post-fulu.
 */
export function getDataColumnSidecarTopics(
  networkConfig: NetworkConfig
): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
  const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [];

  const subnets = networkConfig.custodyConfig.sampledSubnets;
  for (const subnet of subnets) {
    topics.push({type: GossipType.data_column_sidecar, subnet});
  }

  return topics;
}

/**
 * Validate that a `encodingStr` is a known `GossipEncoding`
 */
function parseEncodingStr(encodingStr: string): GossipEncoding {
  switch (encodingStr) {
    case GossipEncoding.ssz_snappy:
      return encodingStr;

    default:
      throw Error(`Unknown encoding ${encodingStr}`);
  }
}

// TODO: Review which yes, and which not
export const gossipTopicIgnoreDuplicatePublishError: Record<GossipType, boolean> = {
  [GossipType.beacon_block]: true,
  [GossipType.blob_sidecar]: true,
  [GossipType.data_column_sidecar]: true,
  [GossipType.beacon_aggregate_and_proof]: true,
  [GossipType.beacon_attestation]: true,
  [GossipType.voluntary_exit]: true,
  [GossipType.proposer_slashing]: false, // Why not this ones?
  [GossipType.attester_slashing]: false,
  [GossipType.sync_committee_contribution_and_proof]: true,
  [GossipType.sync_committee]: true,
  [GossipType.light_client_finality_update]: false,
  [GossipType.light_client_optimistic_update]: false,
  [GossipType.bls_to_execution_change]: true,
  [GossipType.execution_payload]: true,
  [GossipType.payload_attestation_message]: true,
  [GossipType.execution_payload_bid]: true,
  [GossipType.proposer_preferences]: true,
};
