import {ContainerType, Type, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {ArrayOf, BeaconState, Epoch, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types";
import {
  EmptyArgs,
  EmptyMeta,
  EmptyRequest,
  EmptyRequestCodec,
  EmptyResponseCodec,
  EmptyResponseData,
  JsonOnlyResponseCodec,
  WithVersion,
} from "../../utils/codecs.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {
  ExecutionOptimisticFinalizedAndVersionCodec,
  ExecutionOptimisticFinalizedAndVersionMeta,
  VersionCodec,
  VersionMeta,
} from "../../utils/metadata.js";
import {StateArgs} from "./beacon/state.js";
import {FilterGetPeers, NodePeer, PeerDirection, PeerState} from "./node.js";

export type SyncChainDebugState = {
  targetRoot: string | null;
  targetSlot: number | null;
  syncType: string;
  status: string;
  startEpoch: number;
  peers: number;
  // biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
  batches: any[];
};

export type GossipQueueItem = {
  topic: unknown;
  propagationSource: string;
  data: Uint8Array;
  addedTimeMs: number;
  seenTimestampSec: number;
};

export type PeerScoreStat = {
  peerId: string;
  lodestarScore: number;
  gossipScore: number;
  ignoreNegativeGossipScore: boolean;
  score: number;
  lastUpdate: number;
};

export type GossipPeerScoreStat = {
  peerId: string;
  // + Other un-typed options
};

/**
 * A multiaddr with peer ID or ENR string.
 *
 * Supported formats:
 * - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...`
 * - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...`
 *
 * For multiaddrs, the string must contain a /p2p/ component with the peer ID.
 * For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record.
 */
export type DirectPeer = string;

export type RegenQueueItem = {
  key: string;
  args: unknown;
  addedTimeMs: number;
};

export type BlockProcessorQueueItem = {
  blockSlots: Slot[];
  jobOpts: Record<string, string | number | boolean | undefined>;
  addedTimeMs: number;
};

export type StateCacheItem = {
  slot: Slot;
  root: RootHex;
  /** Total number of reads */
  reads: number;
  /** Unix timestamp (ms) of the last read */
  lastRead: number;
  checkpointState: boolean;
};

export type LodestarNodePeer = NodePeer & {
  agentVersion: string;
  status: unknown | null;
  metadata: unknown | null;
  agentClient: string;
  lastReceivedMsgUnixTsMs: number;
  lastStatusUnixTsMs: number;
  connectedUnixTsMs: number;
};

export type BlacklistedBlock = {root: RootHex; slot: Slot | null};

export type LodestarThreadType = "main" | "network" | "discv5";

const HistoricalSummariesResponseType = new ContainerType(
  {
    slot: ssz.Slot,
    historicalSummaries: ssz.capella.HistoricalSummaries,
    proof: ArrayOf(ssz.Bytes8),
  },
  {jsonCase: "eth2"}
);

export type HistoricalSummariesResponse = ValueOf<typeof HistoricalSummariesResponseType>;

export type CustodyInfo = {
  /** Earliest slot for which the node has custodied data columns */
  earliestCustodiedSlot: Slot;
  /** Number of custody groups the node is responsible for */
  custodyGroupCount: number;
  /** List of column indices the node is custodying */
  custodyColumns: number[];
};

export type Endpoints = {
  /** Trigger to write a heapdump to disk at `dirpath`. May take > 1min */
  writeHeapdump: Endpoint<
    "POST",
    {thread?: LodestarThreadType; dirpath?: string},
    {query: {thread?: LodestarThreadType; dirpath?: string}},
    {filepath: string},
    EmptyMeta
  >;
  /** Trigger to write 10m network thread profile to disk */
  writeProfile: Endpoint<
    "POST",
    {
      thread?: LodestarThreadType;
      duration?: number;
      dirpath?: string;
    },
    {query: {thread?: LodestarThreadType; duration?: number; dirpath?: string}},
    {result: string},
    EmptyMeta
  >;
  /** TODO: description */
  getLatestWeakSubjectivityCheckpointEpoch: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    Epoch,
    EmptyMeta
  >;
  /** TODO: description */
  getSyncChainsDebugState: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    SyncChainDebugState[],
    EmptyMeta
  >;
  /** Dump all items in a gossip queue, by gossipType */
  getGossipQueueItems: Endpoint<
    // ⏎
    "GET",
    {gossipType: string},
    {params: {gossipType: string}},
    unknown[],
    EmptyMeta
  >;
  /** Dump all items in the regen queue */
  getRegenQueueItems: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    RegenQueueItem[],
    EmptyMeta
  >;
  /** Dump all items in the block processor queue */
  getBlockProcessorQueueItems: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    BlockProcessorQueueItem[],
    EmptyMeta
  >;
  /** Dump a summary of the states in the block state cache and checkpoint state cache */
  getStateCacheItems: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    StateCacheItem[],
    EmptyMeta
  >;
  /** Dump peer gossip stats by peer */
  getGossipPeerScoreStats: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    GossipPeerScoreStat[],
    EmptyMeta
  >;
  /** Dump lodestar score stats by peer */
  getLodestarPeerScoreStats: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    PeerScoreStat[],
    EmptyMeta
  >;
  /** Run GC with `global.gc()` */
  runGC: Endpoint<
    // ⏎
    "POST",
    EmptyArgs,
    EmptyRequest,
    EmptyResponseData,
    EmptyMeta
  >;
  /** Drop all states in the state cache */
  dropStateCache: Endpoint<
    // ⏎
    "POST",
    EmptyArgs,
    EmptyRequest,
    EmptyResponseData,
    EmptyMeta
  >;

  /** Connect to peer at this multiaddress */
  connectPeer: Endpoint<
    // ⏎
    "POST",
    {peerId: string; multiaddrs: string[]},
    {query: {peerId: string; multiaddr: string[]}},
    EmptyResponseData,
    EmptyMeta
  >;
  /** Disconnect peer */
  disconnectPeer: Endpoint<
    // ⏎
    "POST",
    {peerId: string},
    {query: {peerId: string}},
    EmptyResponseData,
    EmptyMeta
  >;

  /**
   * Add a direct peer at runtime.
   * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
   * Accepts either a multiaddr with peer ID or an ENR string.
   */
  addDirectPeer: Endpoint<
    // ⏎
    "POST",
    {peer: DirectPeer},
    {query: {peer: string}},
    {peerId: string},
    EmptyMeta
  >;

  /** Remove a peer from direct peers */
  removeDirectPeer: Endpoint<
    // ⏎
    "DELETE",
    {peerId: string},
    {query: {peerId: string}},
    {removed: boolean},
    EmptyMeta
  >;

  /** Get list of direct peer IDs */
  getDirectPeers: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    string[],
    EmptyMeta
  >;

  /** Same to node api with new fields */
  getPeers: Endpoint<
    "GET",
    FilterGetPeers,
    {query: {state?: PeerState[]; direction?: PeerDirection[]}},
    LodestarNodePeer[],
    {count: number}
  >;

  /** Returns root/slot of blacklisted blocks */
  getBlacklistedBlocks: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    BlacklistedBlock[],
    EmptyMeta
  >;

  /** Returns historical summaries and proof for a given state ID */
  getHistoricalSummaries: Endpoint<
    // ⏎
    "GET",
    StateArgs,
    {params: {state_id: string}},
    HistoricalSummariesResponse,
    ExecutionOptimisticFinalizedAndVersionMeta
  >;

  getPersistedCheckpointState: Endpoint<
    "GET",
    {
      /** The checkpoint in `<root>:<epoch>` format to be returned instead of the latest safe checkpoint state */
      checkpointId?: string;
    },
    {query: {checkpoint_id?: string}},
    BeaconState,
    VersionMeta
  >;

  /**
   * Returns the validator indices that are currently being monitored by the validator monitor.
   */
  getMonitoredValidatorIndices: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    ValidatorIndex[],
    EmptyMeta
  >;

  /** Dump Discv5 Kad values */
  discv5GetKadValues: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    string[],
    EmptyMeta
  >;

  /**
   * Dump level-db entry keys for a given Bucket declared in code, or for all buckets.
   */
  dumpDbBucketKeys: Endpoint<
    "GET",
    {
      /** Must be the string name of a bucket entry: `allForks_blockArchive` */
      bucket: string;
    },
    {params: {bucket: string}},
    string[],
    EmptyMeta
  >;

  /** Return all entries in the StateArchive index with bucket index_stateArchiveRootIndex */
  dumpDbStateIndex: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    {root: RootHex; slot: Slot}[],
    EmptyMeta
  >;

  /** Get custody information for data columns */
  getCustodyInfo: Endpoint<
    // ⏎
    "GET",
    EmptyArgs,
    EmptyRequest,
    CustodyInfo,
    EmptyMeta
  >;
};

export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
  return {
    writeHeapdump: {
      url: "/eth/v1/lodestar/write_heapdump",
      method: "POST",
      req: {
        writeReq: ({thread, dirpath}) => ({query: {thread, dirpath}}),
        parseReq: ({query}) => ({thread: query.thread, dirpath: query.dirpath}),
        schema: {query: {thread: Schema.String, dirpath: Schema.String}},
      },
      resp: JsonOnlyResponseCodec,
    },
    writeProfile: {
      url: "/eth/v1/lodestar/write_profile",
      method: "POST",
      req: {
        writeReq: ({thread, duration, dirpath}) => ({query: {thread, duration, dirpath}}),
        parseReq: ({query}) => ({thread: query.thread, duration: query.duration, dirpath: query.dirpath}),
        schema: {query: {thread: Schema.String, duration: Schema.Uint, dirpath: Schema.String}},
      },
      resp: JsonOnlyResponseCodec,
    },
    getLatestWeakSubjectivityCheckpointEpoch: {
      url: "/eth/v1/lodestar/ws_epoch",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getSyncChainsDebugState: {
      url: "/eth/v1/lodestar/sync_chains_debug_state",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getGossipQueueItems: {
      url: "/eth/v1/lodestar/gossip_queue_items/:gossipType",
      method: "GET",
      req: {
        writeReq: ({gossipType}) => ({params: {gossipType}}),
        parseReq: ({params}) => ({gossipType: params.gossipType}),
        schema: {params: {gossipType: Schema.StringRequired}},
      },
      resp: JsonOnlyResponseCodec,
    },
    getRegenQueueItems: {
      url: "/eth/v1/lodestar/regen_queue_items",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getBlockProcessorQueueItems: {
      url: "/eth/v1/lodestar/block_processor_queue_items",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getStateCacheItems: {
      url: "/eth/v1/lodestar/state_cache_items",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getGossipPeerScoreStats: {
      url: "/eth/v1/lodestar/gossip_peer_score_stats",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getLodestarPeerScoreStats: {
      url: "/eth/v1/lodestar/lodestar_peer_score_stats",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    runGC: {
      url: "/eth/v1/lodestar/gc",
      method: "POST",
      req: EmptyRequestCodec,
      resp: EmptyResponseCodec,
    },
    dropStateCache: {
      url: "/eth/v1/lodestar/drop_state_cache",
      method: "POST",
      req: EmptyRequestCodec,
      resp: EmptyResponseCodec,
    },
    connectPeer: {
      url: "/eth/v1/lodestar/connect_peer",
      method: "POST",
      req: {
        writeReq: ({peerId, multiaddrs}) => ({query: {peerId, multiaddr: multiaddrs}}),
        parseReq: ({query}) => ({peerId: query.peerId, multiaddrs: query.multiaddr}),
        schema: {query: {peerId: Schema.StringRequired, multiaddr: Schema.StringArray}},
      },
      resp: EmptyResponseCodec,
    },
    disconnectPeer: {
      url: "/eth/v1/lodestar/disconnect_peer",
      method: "POST",
      req: {
        writeReq: ({peerId}) => ({query: {peerId}}),
        parseReq: ({query}) => ({peerId: query.peerId}),
        schema: {query: {peerId: Schema.StringRequired}},
      },
      resp: EmptyResponseCodec,
    },
    addDirectPeer: {
      url: "/eth/v1/lodestar/direct_peers",
      method: "POST",
      req: {
        writeReq: ({peer}) => ({query: {peer}}),
        parseReq: ({query}) => ({peer: query.peer}),
        schema: {query: {peer: Schema.StringRequired}},
      },
      resp: JsonOnlyResponseCodec,
    },
    removeDirectPeer: {
      url: "/eth/v1/lodestar/direct_peers",
      method: "DELETE",
      req: {
        writeReq: ({peerId}) => ({query: {peerId}}),
        parseReq: ({query}) => ({peerId: query.peerId}),
        schema: {query: {peerId: Schema.StringRequired}},
      },
      resp: JsonOnlyResponseCodec,
    },
    getDirectPeers: {
      url: "/eth/v1/lodestar/direct_peers",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getPeers: {
      url: "/eth/v1/lodestar/peers",
      method: "GET",
      req: {
        writeReq: ({state, direction}) => ({query: {state, direction}}),
        parseReq: ({query}) => ({state: query.state, direction: query.direction}),
        schema: {query: {state: Schema.StringArray, direction: Schema.StringArray}},
      },
      resp: JsonOnlyResponseCodec,
    },
    getBlacklistedBlocks: {
      url: "/eth/v1/lodestar/blacklisted_blocks",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getHistoricalSummaries: {
      url: "/eth/v1/lodestar/states/{state_id}/historical_summaries",
      method: "GET",
      req: {
        writeReq: ({stateId}) => ({params: {state_id: stateId.toString()}}),
        parseReq: ({params}) => ({stateId: params.state_id}),
        schema: {
          params: {state_id: Schema.StringRequired},
        },
      },
      resp: {
        data: HistoricalSummariesResponseType,
        meta: ExecutionOptimisticFinalizedAndVersionCodec,
      },
    },
    getPersistedCheckpointState: {
      url: "/eth/v1/lodestar/persisted_checkpoint_state",
      method: "GET",
      req: {
        writeReq: ({checkpointId}) => ({query: {checkpoint_id: checkpointId}}),
        parseReq: ({query}) => ({checkpointId: query.checkpoint_id}),
        schema: {
          query: {checkpoint_id: Schema.String},
        },
      },
      resp: {
        data: WithVersion((fork) => ssz[fork].BeaconState as Type<BeaconState>),
        meta: VersionCodec,
      },
      init: {
        // Default timeout is not sufficient to download state
        timeoutMs: 5 * 60 * 1000,
      },
    },
    getMonitoredValidatorIndices: {
      url: "/eth/v1/lodestar/monitored_validators",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    discv5GetKadValues: {
      url: "/eth/v1/debug/discv5_kad_values",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    dumpDbBucketKeys: {
      url: "/eth/v1/debug/dump_db_bucket_keys/:bucket",
      method: "GET",
      req: {
        writeReq: ({bucket}) => ({params: {bucket}}),
        parseReq: ({params}) => ({bucket: params.bucket}),
        schema: {params: {bucket: Schema.String}},
      },
      resp: JsonOnlyResponseCodec,
    },
    dumpDbStateIndex: {
      url: "/eth/v1/debug/dump_db_state_index",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
    getCustodyInfo: {
      url: "/eth/v1/lodestar/custody_info",
      method: "GET",
      req: EmptyRequestCodec,
      resp: JsonOnlyResponseCodec,
    },
  };
}
