import { randomBytes, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import {
  bytesToHex,
  encodeFunctionData,
  formatEther,
  getAddress,
  isAddress,
  parseEther,
  type Address,
  type Hex,
} from "viem";
import { z } from "zod";
import type { AgentConfig } from "./config.js";
import { createChainClients } from "./chain.js";
import { PONS_FACTORY, ponsFactoryAbi } from "./contracts.js";
import { decodeLaunchReceipt } from "./receipt-data.js";
import { AgentState } from "./state.js";
import { sendTrackedTransaction } from "./broadcast.js";

const socialSchema = z.object({
  twitter: z.string().default(""),
  telegram: z.string().default(""),
  discord: z.string().default(""),
  website: z.string().default(""),
  farcaster: z.string().default(""),
}).default({});

export const launchInputSchema = z.object({
  name: z.string().trim().min(1).max(64),
  symbol: z.string().trim().min(1).max(16).transform((value) => value.toUpperCase()),
  logo: z.string().trim().url(),
  description: z.string().trim().max(1_000).default(""),
  socials: socialSchema,
  feeWallet: z.string().optional(),
});

export const launchPreparationSchema = z.object({
  salt: z.string().regex(/^0x[a-fA-F0-9]{64}$/).transform((value) => value as Hex),
  expectedTokenAddress: z.string().refine(isAddress, "Invalid predicted token address").transform((value) => getAddress(value)),
  expectedLaunchFeeWei: z.string().regex(/^\d+$/),
});

export type LaunchInput = z.input<typeof launchInputSchema>;
export type LaunchPreparation = z.input<typeof launchPreparationSchema>;

export type LaunchPrediction = {
  logo: string;
  salt: Hex;
  predictedTokenAddress: Address;
  launchFeeWei: string;
  launchFeeEth: string;
  initialBuyEth: string;
  totalValueWei: string;
  totalValueEth: string;
  walletBalanceWei: string;
  walletBalanceEth: string;
  launchEnabled: boolean;
  addressAvailable: boolean;
  canLaunch: boolean;
  blockers: string[];
};

export type LaunchResult = {
  mode: "simulated" | "confirmed";
  totalValueEth: string;
  txHash?: string;
  token?: string;
  pool?: string;
  executionId?: string;
};

type PublicClient = ReturnType<typeof createChainClients>["publicClient"];

export const createLaunchSalt = (): Hex => bytesToHex(randomBytes(32));

export const buildLaunchContractArgs = (
  rawInput: LaunchInput,
  defaultFeeWallet: Address,
  salt: Hex,
) => {
  const input = launchInputSchema.parse(rawInput);
  const feeWallet = input.feeWallet ? getAddress(input.feeWallet) : defaultFeeWallet;
  return [{
    name: input.name,
    symbol: input.symbol,
    logo: input.logo,
    description: input.description,
    socials: input.socials,
    feeWallet,
  }, 0n, 0n, salt] as const;
};

const readLaunchPrediction = async (
  config: AgentConfig,
  rawInput: LaunchInput,
  initialBuyEth: string,
  salt: Hex,
  publicClient: PublicClient,
): Promise<{ prediction: LaunchPrediction; args: ReturnType<typeof buildLaunchContractArgs> }> => {
  if (!config.account || !config.feeWallet) throw new Error("A configured execution wallet is required to predict a launch");

  const input = launchInputSchema.parse(rawInput);
  const initialBuyWei = parseEther(initialBuyEth);
  const args = buildLaunchContractArgs(input, config.feeWallet, salt);
  const [launchFee, launchEnabled, balance, predictedTokenAddress] = await Promise.all([
    publicClient.readContract({ address: PONS_FACTORY, abi: ponsFactoryAbi, functionName: "launchFee" }),
    publicClient.readContract({ address: PONS_FACTORY, abi: ponsFactoryAbi, functionName: "launchEnabled" }),
    publicClient.getBalance({ address: config.account.address }),
    publicClient.readContract({
      address: PONS_FACTORY,
      abi: ponsFactoryAbi,
      functionName: "predictTokenAddress",
      args: [args[0], args[1], args[2], args[3], config.account.address],
    }),
  ]);
  const predicted = getAddress(predictedTokenAddress);
  const bytecode = await publicClient.getBytecode({ address: predicted });
  const totalValue = launchFee + initialBuyWei;
  const blockers: string[] = [];
  if (!launchEnabled) blockers.push("Pons 当前已暂停发射");
  if (bytecode) blockers.push("预测 CA 已存在合约代码，请重新生成 Salt");
  if (totalValue > config.maxLaunchTotalWei) {
    blockers.push(`发射总额 ${formatEther(totalValue)} ETH 超过 Agent 上限 ${formatEther(config.maxLaunchTotalWei)} ETH`);
  }
  if (balance - totalValue < config.minGasReserveWei) {
    blockers.push(`余额不足：发射后必须保留 ${formatEther(config.minGasReserveWei)} ETH 作为 Gas`);
  }

  return {
    args,
    prediction: {
      logo: input.logo,
      salt,
      predictedTokenAddress: predicted,
      launchFeeWei: launchFee.toString(),
      launchFeeEth: formatEther(launchFee),
      initialBuyEth: formatEther(initialBuyWei),
      totalValueWei: totalValue.toString(),
      totalValueEth: formatEther(totalValue),
      walletBalanceWei: balance.toString(),
      walletBalanceEth: formatEther(balance),
      launchEnabled,
      addressAvailable: !bytecode,
      canLaunch: blockers.length === 0,
      blockers,
    },
  };
};

export const predictLaunchToken = async (
  config: AgentConfig,
  rawInput: LaunchInput,
  initialBuyEth: string,
  salt: Hex = createLaunchSalt(),
): Promise<LaunchPrediction> => {
  const { publicClient } = createChainClients(config);
  return (await readLaunchPrediction(config, rawInput, initialBuyEth, salt, publicClient)).prediction;
};

export const launchToken = async (
  config: AgentConfig,
  rawInput: LaunchInput,
  initialBuyEth: string,
  topic?: string,
  rawPreparation?: LaunchPreparation,
  onSubmitted?: (submission: {
    txHash: `0x${string}`;
    token: string;
    name: string;
    symbol: string;
    topic?: string;
    totalValueEth: string;
  }) => void | Promise<void>,
): Promise<LaunchResult> => {
  if (!config.account || !config.feeWallet) throw new Error("A configured execution wallet is required to launch a token");

  const input = launchInputSchema.parse(rawInput);
  const preparation = rawPreparation ? launchPreparationSchema.parse(rawPreparation) : null;
  const salt = preparation?.salt ?? createLaunchSalt();
  const clients = createChainClients(config);
  const { publicClient, walletClient } = clients;
  const { prediction, args } = await readLaunchPrediction(config, input, initialBuyEth, salt, publicClient);

  if (preparation && getAddress(preparation.expectedTokenAddress) !== prediction.predictedTokenAddress) {
    throw new Error(`预测 CA 已变化：原 ${preparation.expectedTokenAddress}，当前 ${prediction.predictedTokenAddress}。请重新预测后再发射`);
  }
  if (preparation && preparation.expectedLaunchFeeWei !== prediction.launchFeeWei) {
    throw new Error("Pons 发射费用已变化，请重新预测并确认费用后再发射");
  }
  if (!prediction.canLaunch) throw new Error(prediction.blockers.join("；"));

  await publicClient.simulateContract({
    account: config.account,
    address: PONS_FACTORY,
    abi: ponsFactoryAbi,
    functionName: "launchToken",
    args,
    value: BigInt(prediction.totalValueWei),
  });

  if (config.mode !== "live") {
    return {
      mode: "simulated",
      totalValueEth: prediction.totalValueEth,
      token: prediction.predictedTokenAddress,
    };
  }
  if (!walletClient) throw new Error("Wallet client is unavailable in live mode");

  const executionId = randomUUID();
  const plannedResult = {
    name: input.name,
    symbol: input.symbol,
    topic,
    token: prediction.predictedTokenAddress,
    totalValueEth: prediction.totalValueEth,
  };
  const plannedState = new AgentState(config.stateDbPath);
  plannedState.startExecution(config.account.address, { id: executionId, kind: "launch", target: topic ?? input.symbol, amount: prediction.totalValueWei, result: plannedResult });
  plannedState.close();

  let txHash: `0x${string}` | undefined;
  let preparedTxHash: `0x${string}` | undefined;
  let reverted = false;
  try {
    const data = encodeFunctionData({ abi: ponsFactoryAbi, functionName: "launchToken", args });
    txHash = await sendTrackedTransaction(config, clients, { to: PONS_FACTORY, data, value: BigInt(prediction.totalValueWei) }, (preparedHash) => {
      preparedTxHash = preparedHash;
      const submittedState = new AgentState(config.stateDbPath);
      submittedState.updateExecution(config.account!.address, executionId, "submitted", { txHash: preparedHash, result: plannedResult });
      submittedState.close();
    });
    await onSubmitted?.({
      txHash,
      token: prediction.predictedTokenAddress,
      name: input.name,
      symbol: input.symbol,
      topic,
      totalValueEth: prediction.totalValueEth,
    });

    const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 2, timeout: 120_000 });
    if (receipt.status !== "success") {
      reverted = true;
      throw new Error(`Launch transaction reverted: ${txHash}`);
    }
    const launched = decodeLaunchReceipt(receipt);
    if (!launched) throw new Error(`Launch succeeded but TokenLaunched could not be decoded: ${txHash}`);
    if (getAddress(launched.token) !== prediction.predictedTokenAddress) {
      throw new Error(`Launch returned ${launched.token}, expected ${prediction.predictedTokenAddress}`);
    }

    const result = {
      mode: "confirmed" as const,
      totalValueEth: prediction.totalValueEth,
      txHash,
      token: launched.token,
      pool: launched.pool,
      executionId,
    };
    const confirmedState = new AgentState(config.stateDbPath);
    confirmedState.recordLaunchExecution(config.account.address, executionId, {
      token: launched.token,
      pool: launched.pool,
      txHash,
      name: input.name,
      symbol: input.symbol,
      topic,
    }, result);
    confirmedState.close();
    return result;
  } catch (error) {
    const failureState = new AgentState(config.stateDbPath);
    const recoverableHash = txHash ?? preparedTxHash;
    if (recoverableHash && !reverted) {
      failureState.updateExecution(config.account.address, executionId, "uncertain", { txHash: recoverableHash, error: error instanceof Error ? error.message : String(error) });
    } else {
      failureState.updateExecution(config.account.address, executionId, "failed", { txHash: recoverableHash, error: error instanceof Error ? error.message : String(error) });
    }
    failureState.close();
    throw error;
  }
};

export const launchFromFile = async (config: AgentConfig, inputPath: string, initialBuyEth: string): Promise<LaunchResult> => {
  const input = JSON.parse(await readFile(inputPath, "utf8")) as LaunchInput;
  return launchToken(config, input, initialBuyEth);
};
