import { randomUUID } from "node:crypto";
import type { AgentConfig } from "./config.js";
import { claimFeesOnce } from "./claim.js";
import { runDoctor } from "./doctor.js";
import { PublicEventPublisher } from "./publisher.js";
import { scanOnce } from "./scan.js";
import { launchToken } from "./launch.js";
import { AgentState } from "./state.js";
import { convertBuybackAssetToWeth, executeBuyback } from "./buyback.js";
import { PONS_WETH } from "./contracts.js";
import { formatEther } from "viem";
import { reconcileExecutions } from "./reconcile.js";
import { ExecutionLease } from "./lease.js";
import { TokenAssetPublisher } from "./token-assets.js";
import { resolveExecutionConfig } from "./wallet-vault.js";
import { startOperatorControl } from "./operator-control.js";

const wait = (milliseconds: number, signal: AbortSignal) => new Promise<void>((resolve) => {
  const timeout = setTimeout(resolve, milliseconds);
  signal.addEventListener("abort", () => {
    clearTimeout(timeout);
    resolve();
  }, { once: true });
});

const publishSafely = async (
  publisher: PublicEventPublisher,
  ...args: Parameters<PublicEventPublisher["publish"]>
) => {
  try {
    return await publisher.publish(...args);
  } catch (error) {
    process.stderr.write(`${JSON.stringify({
      event: "public-event.failed",
      error: error instanceof Error ? error.message : String(error),
    })}\n`);
    return null;
  }
};

const parseExecutionResult = (value: string | null) => {
  if (!value) return {};
  try {
    const parsed: unknown = JSON.parse(value);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed)
      ? parsed as Record<string, unknown>
      : {};
  } catch {
    return {};
  }
};

const publishConfirmedExecutions = async (config: AgentConfig, publisher = new PublicEventPublisher(config)) => {
  if (!config.account) return;
  const state = new AgentState(config.stateDbPath);
  const executions = state.listConfirmedExecutionsWithoutPublicEvent(config.account.address);
  state.close();

  for (const execution of executions) {
    const result = parseExecutionResult(execution.resultJson);
    if (execution.kind === "launch") {
      await publishSafely(publisher, "launch.confirmed", "confirmed", {
        executionId: execution.id,
        topic: result.topic,
        name: result.name,
        symbol: result.symbol,
        token: result.token,
        pool: result.pool,
        totalValueEth: result.totalValueEth,
      }, execution.txHash);
    } else if (execution.kind === "claim") {
      await publishSafely(publisher, "fees.claimed", "confirmed", {
        executionId: execution.id,
        token: result.token ?? execution.target,
        token0: result.token0,
        token1: result.token1,
        creatorAmount0: result.amount0,
        creatorAmount1: result.amount1,
        buybackAllocationBps: 5_000,
      }, execution.txHash);
    } else {
      await publishSafely(publisher, "buyback.confirmed", "confirmed", {
        executionId: execution.id,
        token: result.token ?? execution.target,
        amountInEth: result.amountInEth,
        poolFee: result.poolFee,
        acquiredAmount: result.acquiredAmount,
        quotedAmountOut: result.quotedAmountOut,
        minimumAmountOut: result.minimumAmountOut,
        priceImpactBps: result.priceImpactBps,
        reason: result.reason,
      }, execution.txHash);
    }
  }
};

const processOperatorCommands = async (baseConfig: AgentConfig) => {
  while (true) {
    const config = resolveExecutionConfig(baseConfig);
    if (!config.account) return;
    const state = new AgentState(baseConfig.stateDbPath);
    const command = state.claimNextOperatorCommand(config.account.address);
    state.close();
    if (!command) return;

    try {
      if (command.walletAddress.toLowerCase() !== config.account.address.toLowerCase()) {
        throw new Error("Queued command belongs to a different Agent wallet");
      }
      const publisher = new PublicEventPublisher(config);
      const request = JSON.parse(command.requestJson) as Record<string, unknown>;
      let result: Record<string, unknown>;
      if (command.kind === "claim") {
        result = { claims: await claimFeesOnce(config) };
      } else {
        const name = String(request.name ?? "");
        const symbol = String(request.symbol ?? "");
        const topic = String(request.topic ?? "");
        const preparedLogo = typeof request.logo === "string" ? request.logo : null;
        const logo = preparedLogo ?? await new TokenAssetPublisher(config).publish({ name, symbol, topic });
        result = await launchToken(config, {
          name,
          symbol,
          description: String(request.description ?? ""),
          logo,
          socials: request.socials && typeof request.socials === "object"
            ? request.socials as Record<string, string>
            : {},
        }, String(request.initialBuyEth ?? "0"), topic, request.salt && request.expectedTokenAddress && request.expectedLaunchFeeWei ? {
          salt: String(request.salt),
          expectedTokenAddress: String(request.expectedTokenAddress),
          expectedLaunchFeeWei: String(request.expectedLaunchFeeWei),
        } : undefined, async (submission) => {
          await publishSafely(publisher, "launch.submitted", "submitted", {
            topic: submission.topic,
            name: submission.name,
            symbol: submission.symbol,
            token: submission.token,
            totalValueEth: submission.totalValueEth,
          }, submission.txHash);
        });
      }
      const completed = new AgentState(baseConfig.stateDbPath);
      completed.completeOperatorCommand(config.account.address, command.id, result);
      completed.close();
      await publishConfirmedExecutions(config, publisher);
    } catch (error) {
      const failed = new AgentState(baseConfig.stateDbPath);
      failed.failOperatorCommand(config.account.address, command.id, error instanceof Error ? error.message : String(error));
      failed.close();
    }
  }
};

const runCycle = async (config: AgentConfig) => {
  const publisher = new PublicEventPublisher(config);
  try {
    await publisher.flushPending();
  } catch (error) {
    process.stderr.write(`${JSON.stringify({
      event: "public-event.flush_failed",
      error: error instanceof Error ? error.message : String(error),
    })}\n`);
  }
  const reconciled = await reconcileExecutions(config);
  for (const item of reconciled) {
    process.stdout.write(`${JSON.stringify({ event: "execution.reconciled", ...item })}\n`);
  }
  await publishConfirmedExecutions(config, publisher);

  if (config.autoClaimEnabled && config.account) {
    await claimFeesOnce(config);
    await publishConfirmedExecutions(config, publisher);
  }

  const scan = await scanOnce(config);
  for (const decision of scan.decisions) {
    await publishSafely(publisher, "trend.detected", "observed", {
      topic: decision.topic,
      score: decision.score,
      stage: decision.stage,
      evidence: decision.evidence,
    });
    await publishSafely(publisher, "model.decision", "proposed", {
      topic: decision.topic,
      score: decision.score,
      eligible: decision.eligible,
      blockedReasons: decision.blockedReasons,
      buybackEligible: decision.buybackEligible,
      buybackBlockedReasons: decision.buybackBlockedReasons,
      token: decision.token,
      buybackTarget: decision.buybackTarget,
    });
  }

  if (config.autoLaunchEnabled && config.account) {
    const state = new AgentState(config.stateDbPath);
    const launchDecision = state.hasLaunchWithin(config.account.address, config.launchCooldownMinutes)
      ? undefined
      : [...scan.decisions]
        .filter((decision) => decision.eligible && decision.token && !state.hasLaunchedTopic(config.account!.address, decision.topic))
        .sort((left, right) => right.score - left.score)[0];
    state.close();

    if (launchDecision?.token) {
      const logo = await new TokenAssetPublisher(config).publish({
        name: launchDecision.token.name,
        symbol: launchDecision.token.symbol,
        topic: launchDecision.topic,
      });
      await launchToken(config, {
        name: launchDecision.token.name,
        symbol: launchDecision.token.symbol,
        description: launchDecision.token.description,
        logo,
        socials: {},
      }, config.initialBuyEth, launchDecision.topic, undefined, async (submission) => {
        await publishSafely(publisher, "launch.submitted", "submitted", {
          topic: submission.topic,
          name: submission.name,
          symbol: submission.symbol,
          token: submission.token,
          totalValueEth: submission.totalValueEth,
        }, submission.txHash);
      });
    }
  }

  if (config.autoBuybackEnabled && config.account) {
    const target = [...scan.decisions]
      .filter((decision) =>
        decision.buybackEligible
        && decision.buybackTarget
        && decision.buybackTarget.confidence >= config.minBuybackScore
        && (decision.stage === "emerging" || decision.stage === "rising")
        && decision.manipulationRisk === "low"
        && decision.legalRisk === "low",
      )
      .sort((left, right) => (right.buybackTarget?.confidence ?? 0) - (left.buybackTarget?.confidence ?? 0))[0]
      ?.buybackTarget;

    if (target) {
      const allocationState = new AgentState(config.stateDbPath);
      const nonWeth = allocationState.listBucketBalances(config.account.address, "buyback")
        .filter((balance) => balance.asset.toLowerCase() !== PONS_WETH.toLowerCase() && balance.balance > 0n)
        .sort((left, right) => left.asset.localeCompare(right.asset))[0];
      allocationState.close();
      if (nonWeth) {
        const conversionChunk = nonWeth.balance * BigInt(config.buybackChunkBps) / 10_000n;
        if (conversionChunk > 0n) await convertBuybackAssetToWeth(config, nonWeth.asset, conversionChunk);
      }

      const state = new AgentState(config.stateDbPath);
      const available = state.getBucketBalance(config.account.address, "buyback", PONS_WETH);
      state.close();
      const chunk = available * BigInt(config.buybackChunkBps) / 10_000n;
      if (chunk >= config.minBuybackWei) {
        const result = await executeBuyback(config, target.address, formatEther(chunk));
        if (result.executionId && result.mode === "confirmed") {
          const executionState = new AgentState(config.stateDbPath);
          executionState.updateExecution(config.account.address, result.executionId, "confirmed", {
            txHash: result.txHash,
            result: { ...result, reason: target.reason },
          });
          executionState.close();
        }
      }
    }
  }

  await publishConfirmedExecutions(config, publisher);

  return scan;
};

export const runAgent = async (config: AgentConfig, once = false) => {
  if (!config.xApi.key) throw new Error("XAPI_KEY is required to run the Agent");
  if (!config.model.baseUrl || !config.model.apiKey || !config.model.name) {
    throw new Error("MODEL_BASE_URL, MODEL_API_KEY, and MODEL_NAME are required to run the Agent");
  }
  if (config.autoLaunchEnabled && (!config.account
    || !config.publicEvents.agentId
    || !config.tokenAssets.endpoint
    || !config.tokenAssets.publicBaseUrl
    || !config.imageModel.baseUrl
    || !config.imageModel.apiKey
    || !config.imageModel.name)) {
    throw new Error("Wallet, Agent identity, token asset, and image model configuration are required when AUTO_LAUNCH_ENABLED=true");
  }
  const initialExecutionConfig = resolveExecutionConfig(config);
  const checks = await runDoctor(initialExecutionConfig);
  const failures = checks.filter((check) => check.status === "fail");
  if (failures.length) throw new Error(`Agent doctor failed: ${failures.map((check) => check.name).join(", ")}`);

  const startupConfig = resolveExecutionConfig(config);
  const publisher = new PublicEventPublisher(startupConfig);
  const controller = new AbortController();
  const stop = () => controller.abort();
  process.once("SIGINT", stop);
  process.once("SIGTERM", stop);

  const executionLease = new ExecutionLease(config.stateDbPath);
  if (!executionLease.acquire()) throw new Error("Another Agent runner or manual command owns the wallet execution lease");

  try {
    let serialWork = Promise.resolve();
    const queueSerial = (work: () => Promise<void>) => {
      serialWork = serialWork.then(work, work);
      return serialWork;
    };
    const controlServer = startOperatorControl(config, () => {
      void queueSerial(() => processOperatorCommands(config));
    });
    if (!once) {
      await publishSafely(publisher, "agent.resumed", "observed", { reason: "runner started" });
    }
    do {
      executionLease.assertActive();
      const state = new AgentState(config.stateDbPath);
      const cycleId = randomUUID();
      state.startCycle(cycleId);
      state.close();

      try {
        await queueSerial(() => processOperatorCommands(config));
        const result = await (async () => {
          let cycleResult: Awaited<ReturnType<typeof runCycle>> | undefined;
          await queueSerial(async () => { cycleResult = await runCycle(resolveExecutionConfig(config)); });
          return cycleResult!;
        })();
        const completedState = new AgentState(config.stateDbPath);
        completedState.completeCycle(cycleId, result.decisions.length);
        completedState.close();
        process.stdout.write(`${JSON.stringify({ event: "cycle.completed", cycleId, observedAt: result.observedAt, decisions: result.decisions.length })}\n`);
      } catch (error) {
        const failedState = new AgentState(config.stateDbPath);
        failedState.failCycle(cycleId, error instanceof Error ? error.message : String(error));
        failedState.close();
        if (once) throw error;
        process.stderr.write(`${JSON.stringify({ event: "cycle.failed", cycleId, error: error instanceof Error ? error.message : String(error) })}\n`);
      }
      if (once) break;
      await wait(config.scanIntervalSeconds * 1_000, controller.signal);
    } while (!controller.signal.aborted);
    await serialWork;
    controlServer?.close();
  } finally {
    if (!once) {
      await publishSafely(new PublicEventPublisher(resolveExecutionConfig(config)), "agent.paused", "observed", { reason: "runner stopped" });
    }
    executionLease.release();
  }
};
