import { spawn } from "child_process";
import { fileURLToPath } from "url";
import path from "path";
import process from "process";
import { readFile, writeFile } from "fs/promises";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, "..");
const blockchainDir = path.join(rootDir, "blockchain");

const rpcUrl = "http://127.0.0.1:8545";
const defaultDevPrivateKey =
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const bootstrapFlag = "__LEXICHAIN_CHAIN_BOOTSTRAPPED";
const deployStatePath = path.join(blockchainDir, ".dev-deploy.json");

function spawnProcess(command, args, options) {
  return spawn(command, args, {
    shell: true,
    stdio: "inherit",
    ...options,
  });
}

async function waitForRpc(timeoutMs) {
  const started = Date.now();
  while (Date.now() - started < timeoutMs) {
    try {
      const response = await fetch(rpcUrl, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: 1,
          method: "eth_blockNumber",
          params: [],
        }),
      });
      if (response.ok) return;
    } catch {
      // Ignore transient connection failures while the node starts.
    }
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
  throw new Error("Hardhat RPC did not become ready in time.");
}

async function isRpcReady() {
  try {
    const response = await fetch(rpcUrl, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "eth_blockNumber",
        params: [],
      }),
    });
    return response.ok;
  } catch {
    return false;
  }
}

function isHexAddress(value) {
  return /^0x[a-fA-F0-9]{40}$/.test(value);
}

async function hasContractCode(address) {
  if (!isHexAddress(address)) return false;
  try {
    const response = await fetch(rpcUrl, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "eth_getCode",
        params: [address, "latest"],
      }),
    });
    if (!response.ok) return false;
    const payload = await response.json();
    const code = typeof payload?.result === "string" ? payload.result : "0x";
    return code !== "0x";
  } catch {
    return false;
  }
}

async function readSavedAddress() {
  try {
    const raw = await readFile(deployStatePath, "utf8");
    const parsed = JSON.parse(raw);
    const address = parsed?.address;
    return isHexAddress(address) ? address : "";
  } catch {
    return "";
  }
}

async function saveAddress(address) {
  try {
    await writeFile(
      deployStatePath,
      JSON.stringify({ address }, null, 2),
      "utf8",
    );
  } catch {
    // Non-fatal in dev.
  }
}

async function resolveContractAddress() {
  const candidates = [];
  if (process.env.BLOCKCHAIN_CONTRACT_ADDRESS) {
    candidates.push(process.env.BLOCKCHAIN_CONTRACT_ADDRESS);
  }

  const savedAddress = await readSavedAddress();
  if (savedAddress) candidates.push(savedAddress);

  const checked = new Set();
  for (const candidate of candidates) {
    if (!candidate || checked.has(candidate)) continue;
    checked.add(candidate);
    if (await hasContractCode(candidate)) {
      console.log(`Using existing deployed contract: ${candidate}`);
      return candidate;
    }
  }

  console.log("Deploying contract to localhost...");
  const deployedAddress = await runDeploy();
  await saveAddress(deployedAddress);
  return deployedAddress;
}

function runDeploy() {
  return new Promise((resolve, reject) => {
    const deploy = spawn(
      "npx",
      ["hardhat", "run", "scripts/deploy.ts", "--network", "localhost"],
      {
        cwd: blockchainDir,
        shell: true,
        stdio: ["inherit", "pipe", "pipe"],
        env: process.env,
      },
    );

    let output = "";
    deploy.stdout.on("data", (chunk) => {
      const text = chunk.toString();
      output += text;
      process.stdout.write(text);
    });
    deploy.stderr.on("data", (chunk) => {
      const text = chunk.toString();
      output += text;
      process.stderr.write(text);
    });

    deploy.on("close", (code) => {
      if (code !== 0) {
        reject(new Error("Hardhat deploy failed."));
        return;
      }

      const addressMatch =
        output.match(/BLOCKCHAIN_CONTRACT_ADDRESS=(0x[a-fA-F0-9]{40})/) ||
        output.match(/deployed to:\s*(0x[a-fA-F0-9]{40})/i);

      if (!addressMatch) {
        reject(new Error("Could not detect deployed contract address."));
        return;
      }

      resolve(addressMatch[1]);
    });
  });
}

let nodeProcess;
let nextProcess;
let startedNodeHere = false;

function shutdown(code) {
  if (nextProcess && !nextProcess.killed) {
    nextProcess.kill("SIGINT");
  }
  if (startedNodeHere && nodeProcess && !nodeProcess.killed) {
    nodeProcess.kill("SIGINT");
  }
  process.exit(code);
}

process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));

(async () => {
  const rpcReady = await isRpcReady();
  if (rpcReady) {
    console.log("Detected existing Hardhat node on 8545. Reusing it.");
  } else {
    console.log("Starting Hardhat node...");
    startedNodeHere = true;
    nodeProcess = spawnProcess("npx", ["hardhat", "node"], {
      cwd: blockchainDir,
    });
    await waitForRpc(30000);
  }

  const contractAddress = await resolveContractAddress();
  console.log(`Using contract address: ${contractAddress}`);

  const env = {
    ...process.env,
    BLOCKCHAIN_NETWORK: "hardhat",
    BLOCKCHAIN_RPC_URL: rpcUrl,
    BLOCKCHAIN_CONTRACT_ADDRESS: contractAddress,
    BLOCKCHAIN_PRIVATE_KEY:
      process.env.BLOCKCHAIN_PRIVATE_KEY || defaultDevPrivateKey,
    [bootstrapFlag]: "1",
  };

  if (process.env[bootstrapFlag] === "1") {
    throw new Error(
      "Recursive dev bootstrap detected. Use npm run dev:next inside the bootstrap script.",
    );
  }

  console.log("Starting Next.js dev server...");
  nextProcess = spawn("npm", ["run", "dev:next"], {
    cwd: rootDir,
    shell: true,
    stdio: "inherit",
    env,
  });

  nextProcess.on("close", (code) => shutdown(code ?? 0));
})().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  shutdown(1);
});
