#!/usr/bin/env node
"use strict";

const fs = require("fs");
const path = require("path");
const readline = require("readline");
const BN = require("bn.js");
const {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  sendAndConfirmTransaction,
  clusterApiUrl,
} = require("@solana/web3.js");
const {
  PROGRAM_VERSION_V3,
  Vote,
  YesNoVote,
  VoteType,
  InstructionData,
  AccountMetaData,
  withRevokeGoverningTokens,
  withCreateProposal,
  withInsertTransaction,
  withSignOffProposal,
  withCastVote,
  withExecuteTransaction,
  withFinalizeVote,
  getTokenOwnerRecordAddress,
  getProposalTransactionAddress,
} = require("@solana/spl-governance");

function parseEnvConfig(configPath) {
  const raw = fs.readFileSync(configPath, "utf8");
  const out = {};
  for (const line of raw.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
    const eq = trimmed.indexOf("=");
    if (eq === -1) continue;
    const key = trimmed.slice(0, eq).trim();
    let val = trimmed.slice(eq + 1).trim();
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    val = val.replace(/\$HOME/g, process.env.HOME || "");
    out[key] = val;
  }
  return out;
}

function loadKeypair(filePath) {
  const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
  return Keypair.fromSecretKey(Uint8Array.from(arr));
}

function nowStamp() {
  const d = new Date();
  const p = (n) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
    d.getMinutes()
  )}-${p(d.getSeconds())}`;
}

async function askYes() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const answer = await new Promise((resolve) =>
    rl.question("Введите YES для proposal->vote->execute revoke: ", resolve)
  );
  rl.close();
  return answer.trim() === "YES";
}

function toGovernanceInstructionData(ix) {
  return new InstructionData({
    programId: ix.programId,
    accounts: ix.keys.map(
      (k) =>
        new AccountMetaData({
          pubkey: k.pubkey,
          isSigner: !!k.isSigner,
          isWritable: !!k.isWritable,
        })
    ),
    data: Uint8Array.from(ix.data),
  });
}

function classifyExecuteError(msg) {
  const s = String(msg || "").toLowerCase();
  if (s.includes("0x20d") || s.includes("hold up time")) {
    return "HOLD_UP_TIME";
  }
  if (s.includes("0x21d") || s.includes("invalid mint authority")) {
    return "INVALID_MINT_AUTHORITY";
  }
  return "OTHER";
}

async function main() {
  const configPath = process.argv[2]
    ? path.resolve(process.argv[2])
    : path.resolve(__dirname, "dao.config.env");
  const realmStr = process.argv[3];
  const governanceStr = process.argv[4];
  const mintStr = process.argv[5];
  const targetOwnerStr = process.argv[6];
  const amountStr = process.argv[7] || "1";
  if (!realmStr || !governanceStr || !mintStr || !targetOwnerStr) {
    throw new Error(
      "Использование: node scripts/dao/propose_vote_execute_revoke_full_exec.js <config.env> <realm> <governance> <mint> <target_owner_pubkey> [amount]"
    );
  }

  const cfg = parseEnvConfig(configPath);
  const cluster = cfg.DAO_CLUSTER || "devnet";
  const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
  const proposerKpPath = cfg.DAO_ISSUER_KEYPAIR;
  if (!proposerKpPath) throw new Error("В конфиге нет DAO_ISSUER_KEYPAIR");
  const proposer = loadKeypair(path.resolve(proposerKpPath));

  const realm = new PublicKey(realmStr);
  const governance = new PublicKey(governanceStr);
  const mint = new PublicKey(mintStr);
  const targetOwner = new PublicKey(targetOwnerStr);
  const amount = new BN(amountStr);
  if (amount.lten(0)) throw new Error("amount должен быть > 0");

  const connection = new Connection(clusterApiUrl(cluster), "confirmed");
  const proposerRecord = await getTokenOwnerRecordAddress(
    governanceProgramId,
    realm,
    mint,
    proposer.publicKey
  );

  console.log("============================================================");
  console.log("DAO REVOKE THROUGH VOTE");
  console.log("------------------------------------------------------------");
  console.log("Сеть:                ", cluster);
  console.log("Governance program:  ", governanceProgramId.toBase58());
  console.log("Realm:               ", realm.toBase58());
  console.log("Governance:          ", governance.toBase58());
  console.log("Mint:                ", mint.toBase58());
  console.log("Target owner:        ", targetOwner.toBase58());
  console.log("Amount:              ", amount.toString());
  console.log("Proposer:            ", proposer.publicKey.toBase58());
  console.log("Proposer record:     ", proposerRecord.toBase58());
  console.log("============================================================");

  const ok = await askYes();
  if (!ok) {
    console.log("Отменено пользователем.");
    return;
  }

  const proposalName = `Revoke ${amount.toString()} from ${targetOwner
    .toBase58()
    .slice(0, 8)}...`;
  const proposalDescription = cfg.DAO_REVOKE_PROPOSAL_URI || cfg.DAO_GOV_TOKEN_METADATA_URI || "https://arweave.net/";

  const ixCreateProposal = [];
  const proposalPk = await withCreateProposal(
    ixCreateProposal,
    governanceProgramId,
    PROGRAM_VERSION_V3,
    realm,
    governance,
    proposerRecord,
    proposalName,
    proposalDescription,
    mint,
    proposer.publicKey,
    undefined,
    VoteType.SINGLE_CHOICE,
    ["Approve"],
    true,
    proposer.publicKey
  );
  const sigCreateProposal = await sendAndConfirmTransaction(
    connection,
    new Transaction().add(...ixCreateProposal),
    [proposer],
    { commitment: "confirmed" }
  );

  const ixRawRevoke = [];
  await withRevokeGoverningTokens(
    ixRawRevoke,
    governanceProgramId,
    PROGRAM_VERSION_V3,
    realm,
    targetOwner,
    mint,
    governance,
    amount
  );
  if (ixRawRevoke.length !== 1) throw new Error("Ожидалась одна инструкция revoke");
  const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);

  const ixInsert = [];
  const proposalTxPk = await withInsertTransaction(
    ixInsert,
    governanceProgramId,
    PROGRAM_VERSION_V3,
    governance,
    proposalPk,
    proposerRecord,
    proposer.publicKey,
    0,
    0,
    0,
    [revokeInstructionData],
    proposer.publicKey
  );
  const sigInsert = await sendAndConfirmTransaction(connection, new Transaction().add(...ixInsert), [proposer], {
    commitment: "confirmed",
  });

  const ixSignOff = [];
  withSignOffProposal(
    ixSignOff,
    governanceProgramId,
    PROGRAM_VERSION_V3,
    realm,
    governance,
    proposalPk,
    proposer.publicKey,
    undefined,
    proposerRecord
  );
  const sigSignOff = await sendAndConfirmTransaction(connection, new Transaction().add(...ixSignOff), [proposer], {
    commitment: "confirmed",
  });

  const ixVote = [];
  const vote = Vote.fromYesNoVote(YesNoVote.Yes);
  const voteRecordPk = await withCastVote(
    ixVote,
    governanceProgramId,
    PROGRAM_VERSION_V3,
    realm,
    governance,
    proposalPk,
    proposerRecord,
    proposerRecord,
    proposer.publicKey,
    mint,
    vote,
    proposer.publicKey
  );
  const sigVote = await sendAndConfirmTransaction(connection, new Transaction().add(...ixVote), [proposer], {
    commitment: "confirmed",
  });

  const computedProposalTxPk = await getProposalTransactionAddress(
    governanceProgramId,
    PROGRAM_VERSION_V3,
    proposalPk,
    0,
    0
  );
  if (!computedProposalTxPk.equals(proposalTxPk)) {
    throw new Error("Несовпадение адреса proposal transaction");
  }

  let sigFinalize = null;
  try {
    const ixFinalize = [];
    await withFinalizeVote(
      ixFinalize,
      governanceProgramId,
      PROGRAM_VERSION_V3,
      realm,
      governance,
      proposalPk,
      proposerRecord,
      mint
    );
    sigFinalize = await sendAndConfirmTransaction(connection, new Transaction().add(...ixFinalize), [proposer], {
      commitment: "confirmed",
    });
  } catch (_) {
    // Может быть уже tipped/succeeded без finalize.
  }

  let sigExecute = null;
  let executeError = null;
  let executeErrorKind = null;
  try {
    const ixExecute = [];
    await withExecuteTransaction(
      ixExecute,
      governanceProgramId,
      PROGRAM_VERSION_V3,
      governance,
      proposalPk,
      proposalTxPk,
      [revokeInstructionData]
    );
    sigExecute = await sendAndConfirmTransaction(connection, new Transaction().add(...ixExecute), [proposer], {
      commitment: "confirmed",
    });
  } catch (e) {
    executeError = e?.message || String(e);
    executeErrorKind = classifyExecuteError(executeError);
  }

  const report = {
    createdAt: new Date().toISOString(),
    cluster,
    configPath,
    governanceProgramId: governanceProgramId.toBase58(),
    realm: realm.toBase58(),
    governance: governance.toBase58(),
    mint: mint.toBase58(),
    targetOwner: targetOwner.toBase58(),
    amount: amount.toString(),
    proposer: proposer.publicKey.toBase58(),
    proposerRecord: proposerRecord.toBase58(),
    proposal: proposalPk.toBase58(),
    proposalTransaction: proposalTxPk.toBase58(),
    voteRecord: voteRecordPk.toBase58(),
    txCreateProposal: sigCreateProposal,
    txInsertTransaction: sigInsert,
    txSignOff: sigSignOff,
    txVote: sigVote,
    txFinalize: sigFinalize,
    txExecute: sigExecute,
    executeError,
    executeErrorKind,
  };

  const reportDir = path.resolve(__dirname, "runs");
  fs.mkdirSync(reportDir, { recursive: true });
  const reportBaseName = `${nowStamp()}_revoke_${targetOwner.toBase58().slice(0, 10)}`;
  const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
  const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
  fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
  fs.writeFileSync(
    reportTxtPath,
    [
      `createdAt: ${report.createdAt}`,
      `cluster: ${report.cluster}`,
      `realm: ${report.realm}`,
      `governance: ${report.governance}`,
      `mint: ${report.mint}`,
      `targetOwner: ${report.targetOwner}`,
      `amount: ${report.amount}`,
      `proposer: ${report.proposer}`,
      `proposal: ${report.proposal}`,
      `proposalTransaction: ${report.proposalTransaction}`,
      `voteRecord: ${report.voteRecord}`,
      `txCreateProposal: ${report.txCreateProposal}`,
      `txInsertTransaction: ${report.txInsertTransaction}`,
      `txSignOff: ${report.txSignOff}`,
      `txVote: ${report.txVote}`,
      `txFinalize: ${report.txFinalize || "-"}`,
      `txExecute: ${report.txExecute || "-"}`,
      `executeError: ${report.executeError || "-"}`,
      `executeErrorKind: ${report.executeErrorKind || "-"}`,
    ].join("\n") + "\n"
  );

  console.log("============================================================");
  console.log("REVOKE ЧЕРЕЗ DAO ГОЛОСОВАНИЕ ВЫПОЛНЕН");
  console.log("------------------------------------------------------------");
  console.log("Proposal:                                      ", proposalPk.toBase58());
  console.log("Proposal Tx:                                   ", proposalTxPk.toBase58());
  console.log("Tx create proposal:                            ", sigCreateProposal);
  console.log("Tx insert revoke instruction:                  ", sigInsert);
  console.log("Tx sign off:                                   ", sigSignOff);
  console.log("Tx cast vote:                                  ", sigVote);
  if (sigFinalize) console.log("Tx finalize vote:                              ", sigFinalize);
  if (sigExecute) {
    console.log("Tx execute:                                    ", sigExecute);
  } else {
    console.log("Execute сейчас не прошел (ожидание voting/hold-up):");
    console.log("Ошибка execute:                                ", executeError);
    if (executeErrorKind === "HOLD_UP_TIME") {
      console.log("Причина:                                      ", "слишком рано для execute (hold-up / окно голосования еще не завершено)");
    } else if (executeErrorKind === "INVALID_MINT_AUTHORITY") {
      console.log("Причина:                                      ", "community mint authority не передан на governance PDA при создании DAO");
    }
    console.log("Повтор execute через время этой командой:");
    console.log(
      `node scripts/dao/execute_revoke_transaction_full_exec.js ${configPath} ${realm.toBase58()} ${governance.toBase58()} ${proposalPk.toBase58()} ${proposalTxPk.toBase58()} ${mint.toBase58()} ${targetOwner.toBase58()} ${amount.toString()}`
    );
  }
  console.log("Отчёт JSON:                                    ", reportJsonPath);
  console.log("Отчёт TXT:                                     ", reportTxtPath);
  console.log("============================================================");
}

main().catch((e) => {
  console.error("Ошибка proposal/vote/execute revoke:", e?.message || e);
  process.exit(1);
});
