import {
  decodeEventLog,
  getAddress,
  padHex,
  parseAbiItem,
  toEventSelector,
  type Address,
  type Hex,
} from "viem";
import { z } from "zod";
import type { AgentConfig } from "./config.js";
import { createChainClients } from "./chain.js";
import {
  PONS_FACTORY,
  PONS_FACTORY_START_BLOCK,
  ROBINHOOD_BLOCKSCOUT_API,
  erc20MetadataAbi,
  ponsFactoryAbi,
} from "./contracts.js";

const tokenLaunchedTopic = toEventSelector(
  "TokenLaunched(address,address,address,address,address,uint256,uint256,uint256,uint256,uint256)",
);
const tokenLaunchedEvent = parseAbiItem(
  "event TokenLaunched(address indexed token, address indexed deployer, address indexed dexFactory, address pairToken, address pool, uint256 dexId, uint256 launchConfigId, uint256 positionId, uint256 restrictionsEndBlock, uint256 initialBuyAmount)",
);
const RPC_BLOCK_CHUNK = 2_000n;

const blockscoutLogSchema = z.object({
  address: z.string(),
  topics: z.array(z.string()).min(3),
  data: z.string(),
  blockNumber: z.string(),
  timeStamp: z.string().optional(),
  transactionHash: z.string(),
});

const blockscoutResponseSchema = z.object({
  status: z.string(),
  message: z.string().optional(),
  result: z.union([z.array(blockscoutLogSchema), z.string()]),
});

type DiscoveryClient = {
  readContract: (request: Record<string, unknown>) => Promise<unknown>;
  getBlock: (request: { blockNumber: bigint }) => Promise<{ timestamp: bigint }>;
  getBlockNumber?: () => Promise<bigint>;
  getLogs?: (request: Record<string, unknown>) => Promise<Array<{
    args?: { token?: Address; deployer?: Address; pool?: Address };
    blockNumber?: bigint;
    transactionHash?: Hex;
  }>>;
};

export type DiscoveredLaunch = {
  token: Address;
  pool: Address;
  txHash: Hex;
  name: string;
  symbol: string;
  createdAt: string;
};

const chainInteger = (value: string) => {
  if (!/^(?:0x[0-9a-fA-F]+|\d+)$/.test(value)) throw new Error(`Invalid Blockscout integer: ${value}`);
  return BigInt(value);
};

export const blockscoutLaunchLogsUrl = (wallet: Address) => {
  const url = new URL(ROBINHOOD_BLOCKSCOUT_API);
  url.searchParams.set("module", "logs");
  url.searchParams.set("action", "getLogs");
  url.searchParams.set("address", PONS_FACTORY);
  url.searchParams.set("fromBlock", PONS_FACTORY_START_BLOCK.toString());
  url.searchParams.set("toBlock", "latest");
  url.searchParams.set("topic0", tokenLaunchedTopic);
  url.searchParams.set("topic2", padHex(wallet, { size: 32 }));
  url.searchParams.set("topic0_2_opr", "and");
  return url;
};

export const discoverWalletLaunches = async (
  config: AgentConfig,
  dependencies: { fetchImpl?: typeof fetch; client?: DiscoveryClient } = {},
): Promise<DiscoveredLaunch[]> => {
  if (!config.account) throw new Error("Execution wallet is required to discover Pons launches");
  const wallet = getAddress(config.account.address);
  const fetchImpl = dependencies.fetchImpl ?? fetch;
  const client = dependencies.client
    ?? createChainClients(config).publicClient as unknown as DiscoveryClient;
  let decoded: Array<{
    token: Address;
    pool: Address;
    txHash: Hex;
    blockNumber: bigint;
    timestamp: bigint | null;
  }>;
  try {
    const response = await fetchImpl(blockscoutLaunchLogsUrl(wallet), {
      headers: { accept: "application/json" },
      signal: AbortSignal.timeout(30_000),
    });
    if (!response.ok) throw new Error(`Blockscout launch discovery failed (${response.status})`);

    const payload = blockscoutResponseSchema.parse(await response.json());
    if (typeof payload.result === "string") {
      if (/no (?:records|logs)/i.test(payload.result) || payload.result.trim() === "") return [];
      throw new Error(`Blockscout launch discovery failed: ${payload.result.slice(0, 300)}`);
    }
    decoded = payload.result.flatMap((log) => {
      if (log.address.toLowerCase() !== PONS_FACTORY.toLowerCase()) return [];
      try {
        const event = decodeEventLog({
          abi: ponsFactoryAbi,
          data: log.data as Hex,
          topics: log.topics as [Hex, ...Hex[]],
        });
        if (event.eventName !== "TokenLaunched") return [];
        if (event.args.deployer.toLowerCase() !== wallet.toLowerCase()) return [];
        return [{
          token: getAddress(event.args.token),
          pool: getAddress(event.args.pool),
          txHash: log.transactionHash as Hex,
          blockNumber: chainInteger(log.blockNumber),
          timestamp: log.timeStamp ? chainInteger(log.timeStamp) : null,
        }];
      } catch {
        return [];
      }
    });
  } catch (blockscoutError) {
    if (!client.getBlockNumber || !client.getLogs) throw blockscoutError;
    const latestBlock = await client.getBlockNumber();
    decoded = [];
    for (let fromBlock = PONS_FACTORY_START_BLOCK; fromBlock <= latestBlock; fromBlock += RPC_BLOCK_CHUNK) {
      const toBlock = fromBlock + RPC_BLOCK_CHUNK - 1n > latestBlock
        ? latestBlock
        : fromBlock + RPC_BLOCK_CHUNK - 1n;
      const logs = await client.getLogs({
        address: PONS_FACTORY,
        event: tokenLaunchedEvent,
        args: { deployer: wallet },
        fromBlock,
        toBlock,
      });
      for (const log of logs) {
        if (!log.args?.token || !log.args.pool || !log.args.deployer || !log.blockNumber || !log.transactionHash) continue;
        if (log.args.deployer.toLowerCase() !== wallet.toLowerCase()) continue;
        decoded.push({
          token: getAddress(log.args.token),
          pool: getAddress(log.args.pool),
          txHash: log.transactionHash,
          blockNumber: log.blockNumber,
          timestamp: null,
        });
      }
    }
  }

  const unique = [...new Map(decoded.map((launch) => [launch.token.toLowerCase(), launch])).values()];
  return Promise.all(unique.map(async (launch) => {
    const [name, symbol, blockTimestamp] = await Promise.all([
      client.readContract({ address: launch.token, abi: erc20MetadataAbi, functionName: "name" })
        .then(String).catch(() => "Unknown token"),
      client.readContract({ address: launch.token, abi: erc20MetadataAbi, functionName: "symbol" })
        .then(String).catch(() => "UNKNOWN"),
      launch.timestamp === null
        ? client.getBlock({ blockNumber: launch.blockNumber }).then((block) => block.timestamp)
        : Promise.resolve(launch.timestamp),
    ]);
    return {
      token: launch.token,
      pool: launch.pool,
      txHash: launch.txHash,
      name,
      symbol,
      createdAt: new Date(Number(blockTimestamp) * 1_000).toISOString(),
    };
  }));
};
