#!/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,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
  clusterApiUrl,
} = require("@solana/web3.js");
const {
  TOKEN_PROGRAM_ID,
  AuthorityType,
  getMintLen,
  createInitializeMintInstruction,
  getAssociatedTokenAddressSync,
  createAssociatedTokenAccountIdempotentInstruction,
  createMintToInstruction,
  createSetAuthorityInstruction,
} = require("@solana/spl-token");
const {
  MintMaxVoteWeightSource,
  VoteThreshold,
  VoteThresholdType,
  VoteTipping,
  GovernanceConfig,
  PROGRAM_VERSION_V3,
  GoverningTokenConfigAccountArgs,
  GoverningTokenType,
  withCreateRealm,
  withDepositGoverningTokens,
  withCreateGovernance,
  withCreateNativeTreasury,
  withSetRealmAuthority,
  SetRealmAuthorityAction,
} = require("@solana/spl-governance");
const { createUmi } = require("@metaplex-foundation/umi-bundle-defaults");
const {
  createSignerFromKeypair,
  signerIdentity,
  percentAmount,
  none,
  some,
} = require("@metaplex-foundation/umi");
const { fromWeb3JsKeypair, fromWeb3JsPublicKey } = require("@metaplex-foundation/umi-web3js-adapters");
const { mplTokenMetadata, createV1, TokenStandard } = require("@metaplex-foundation/mpl-token-metadata");

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 assertRequired(cfg, key) {
  if (!cfg[key]) throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
}

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

function lamportsToSol(lamports) {
  return Number(lamports) / 1_000_000_000;
}

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 для реального создания ПОЛНОГО DAO: ", resolve)
  );
  rl.close();
  return answer.trim() === "YES";
}

function ensureArweaveUri(name, uri) {
  if (!uri) throw new Error(`${name} пустой`);
  if (!(uri.startsWith("https://arweave.net/") || uri.startsWith("ar://"))) {
    throw new Error(`${name} должен указывать на Arweave (https://arweave.net/... или ar://...)`);
  }
}

async function attachTokenMetadataViaUmi(cfg, cluster, issuer, mintPubkey, mintKeypair) {
  ensureArweaveUri("DAO_GOV_TOKEN_METADATA_URI", cfg.DAO_GOV_TOKEN_METADATA_URI);
  ensureArweaveUri("DAO_GOV_TOKEN_IMAGE_URL", cfg.DAO_GOV_TOKEN_IMAGE_URL);

  const umi = createUmi(clusterApiUrl(cluster));
  const umiSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer));
  const umiMintSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(mintKeypair));
  umi.use(signerIdentity(umiSigner));
  umi.use(mplTokenMetadata());

  const builder = createV1(umi, {
    mint: umiMintSigner,
    authority: umiSigner,
    payer: umiSigner,
    updateAuthority: umiSigner,
    name: cfg.DAO_GOV_NFT_NAME,
    symbol: cfg.DAO_GOV_NFT_SYMBOL,
    uri: cfg.DAO_GOV_TOKEN_METADATA_URI,
    sellerFeeBasisPoints: percentAmount(0),
    tokenStandard: TokenStandard.Fungible,
    decimals: some(0),
    creators: none(),
    collection: none(),
    uses: none(),
    collectionDetails: none(),
    ruleSet: none(),
    printSupply: none(),
    primarySaleHappened: false,
    isMutable: true,
    isCollection: false,
    splTokenProgram: fromWeb3JsPublicKey(TOKEN_PROGRAM_ID),
  });

  const res = await builder.sendAndConfirm(umi);
  const sig = Buffer.from(res.signature).toString("base64");
  return sig;
}

async function main() {
  const configPath = process.argv[2]
    ? path.resolve(process.argv[2])
    : path.resolve(__dirname, "dao.config.env");
  if (!fs.existsSync(configPath)) throw new Error(`Конфиг не найден: ${configPath}`);

  const cfg = parseEnvConfig(configPath);
  [
    "DAO_CLUSTER",
    "DAO_REALM_NAME",
    "DAO_GOV_NFT_NAME",
    "DAO_GOV_NFT_SYMBOL",
    "DAO_GOV_NFT_SUPPLY",
    "DAO_VOTING_TIME_SEC",
    "DAO_APPROVAL_THRESHOLD_PERCENT",
    "DAO_ISSUER_KEYPAIR",
    "SPL_GOVERNANCE_PROGRAM_ID",
    "DAO_GOV_TOKEN_METADATA_URI",
    "DAO_GOV_TOKEN_IMAGE_URL",
  ].forEach((k) => assertRequired(cfg, k));

  const cluster = cfg.DAO_CLUSTER;
  const connection = new Connection(clusterApiUrl(cluster), "confirmed");
  const issuer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR));
  const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);

  const supply = Number(cfg.DAO_GOV_NFT_SUPPLY);
  const votingTimeSec = Number(cfg.DAO_VOTING_TIME_SEC);
  const thresholdPct = Number(cfg.DAO_APPROVAL_THRESHOLD_PERCENT);
  if (!Number.isInteger(supply) || supply <= 0) throw new Error("DAO_GOV_NFT_SUPPLY должен быть целым > 0");
  if (!Number.isInteger(votingTimeSec) || votingTimeSec < 3600)
    throw new Error("DAO_VOTING_TIME_SEC должен быть >= 3600 (ограничение Realms)");
  if (!Number.isInteger(thresholdPct) || thresholdPct < 51 || thresholdPct > 100)
    throw new Error("DAO_APPROVAL_THRESHOLD_PERCENT должен быть в диапазоне 51..100");

  const [realmPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("governance"), Buffer.from(cfg.DAO_REALM_NAME, "utf8")],
    governanceProgramId
  );
  const realmExists = (await connection.getAccountInfo(realmPda)) !== null;
  if (realmExists) throw new Error(`Realm уже существует: ${realmPda.toBase58()}`);

  const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
  console.log("============================================================");
  console.log("СОЗДАНИЕ DAO (FULL)");
  console.log("------------------------------------------------------------");
  console.log("Сеть:                ", cluster);
  console.log("Realm name:          ", cfg.DAO_REALM_NAME);
  console.log("Realm PDA:           ", realmPda.toBase58());
  console.log("Governance program:  ", governanceProgramId.toBase58());
  console.log("Issuer:              ", issuer.publicKey.toBase58());
  console.log("Баланс до старта:    ", `${lamportsToSol(startBalance)} SOL`);
  console.log("Token name/symbol:   ", `${cfg.DAO_GOV_NFT_NAME} / ${cfg.DAO_GOV_NFT_SYMBOL}`);
  console.log("Token supply:        ", supply);
  console.log("Voting time sec:     ", votingTimeSec);
  console.log("Threshold %:         ", thresholdPct);
  console.log("Arweave metadata URI:", cfg.DAO_GOV_TOKEN_METADATA_URI);
  console.log("Arweave image URL:   ", cfg.DAO_GOV_TOKEN_IMAGE_URL);
  console.log("============================================================");

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

  const mintKeypair = Keypair.generate();
  const mintLen = getMintLen([]);
  const mintRent = await connection.getMinimumBalanceForRentExemption(mintLen);
  const issuerAta = getAssociatedTokenAddressSync(mintKeypair.publicKey, issuer.publicKey, false, TOKEN_PROGRAM_ID);

  const txMint = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: issuer.publicKey,
      newAccountPubkey: mintKeypair.publicKey,
      space: mintLen,
      lamports: mintRent,
      programId: TOKEN_PROGRAM_ID,
    }),
    createInitializeMintInstruction(mintKeypair.publicKey, 0, issuer.publicKey, issuer.publicKey, TOKEN_PROGRAM_ID),
    createAssociatedTokenAccountIdempotentInstruction(
      issuer.publicKey,
      issuerAta,
      issuer.publicKey,
      mintKeypair.publicKey,
      TOKEN_PROGRAM_ID
    ),
    createMintToInstruction(mintKeypair.publicKey, issuerAta, issuer.publicKey, supply, [], TOKEN_PROGRAM_ID)
  );
  const sigMint = await sendAndConfirmTransaction(connection, txMint, [issuer, mintKeypair], {
    commitment: "confirmed",
  });

  const sigMetadata = await attachTokenMetadataViaUmi(
    cfg,
    cluster,
    issuer,
    mintKeypair.publicKey,
    mintKeypair
  );

  const programVersion = PROGRAM_VERSION_V3;
  const ixRealm = [];
  const communityTokenConfig = new GoverningTokenConfigAccountArgs({
    voterWeightAddin: undefined,
    maxVoterWeightAddin: undefined,
    tokenType: GoverningTokenType.Membership,
  });
  const realmPk = await withCreateRealm(
    ixRealm,
    governanceProgramId,
    programVersion,
    cfg.DAO_REALM_NAME,
    issuer.publicKey,
    mintKeypair.publicKey,
    issuer.publicKey,
    undefined,
    MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
    new BN(1),
    communityTokenConfig,
    undefined
  );
  const sigRealm = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealm), [issuer], {
    commitment: "confirmed",
  });

  const ixDeposit = [];
  const tokenOwnerRecordPk = await withDepositGoverningTokens(
    ixDeposit,
    governanceProgramId,
    programVersion,
    realmPk,
    issuerAta,
    mintKeypair.publicKey,
    issuer.publicKey,
    issuer.publicKey,
    issuer.publicKey,
    new BN(supply),
    true
  );
  const sigDeposit = await sendAndConfirmTransaction(connection, new Transaction().add(...ixDeposit), [issuer], {
    commitment: "confirmed",
  });

  const governanceConfig = new GovernanceConfig({
    communityVoteThreshold: new VoteThreshold({ type: VoteThresholdType.YesVotePercentage, value: thresholdPct }),
    minCommunityTokensToCreateProposal: new BN(1),
    minInstructionHoldUpTime: 0,
    baseVotingTime: votingTimeSec,
    communityVoteTipping: VoteTipping.Early,
    minCouncilTokensToCreateProposal: new BN(0),
    councilVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
    councilVetoVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
    communityVetoVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
    councilVoteTipping: VoteTipping.Disabled,
    votingCoolOffTime: 0,
    depositExemptProposalCount: 0,
  });

  const ixGov = [];
  const governancePk = await withCreateGovernance(
    ixGov,
    governanceProgramId,
    programVersion,
    realmPk,
    realmPk,
    governanceConfig,
    tokenOwnerRecordPk,
    issuer.publicKey,
    issuer.publicKey
  );
  const treasuryPk = await withCreateNativeTreasury(ixGov, governanceProgramId, programVersion, governancePk, issuer.publicKey);
  const sigGov = await sendAndConfirmTransaction(connection, new Transaction().add(...ixGov), [issuer], {
    commitment: "confirmed",
  });

  // Для DAO revoke governing tokens mint authority должен быть у governance PDA.
  const ixSetMintAuthority = [
    createSetAuthorityInstruction(
      mintKeypair.publicKey,
      issuer.publicKey,
      AuthorityType.MintTokens,
      governancePk,
      [],
      TOKEN_PROGRAM_ID
    ),
  ];
  const sigSetMintAuthority = await sendAndConfirmTransaction(
    connection,
    new Transaction().add(...ixSetMintAuthority),
    [issuer],
    { commitment: "confirmed" }
  );

  const ixRealmAuthority = [];
  withSetRealmAuthority(
    ixRealmAuthority,
    governanceProgramId,
    programVersion,
    realmPk,
    issuer.publicKey,
    governancePk,
    SetRealmAuthorityAction.SetChecked
  );
  const sigSetRealmAuthority = await sendAndConfirmTransaction(
    connection,
    new Transaction().add(...ixRealmAuthority),
    [issuer],
    { commitment: "confirmed" }
  );

  const endBalance = await connection.getBalance(issuer.publicKey, "confirmed");
  const spentLamports = startBalance - endBalance;
  const report = {
    createdAt: new Date().toISOString(),
    cluster,
    configPath,
    realmName: cfg.DAO_REALM_NAME,
    governanceProgramId: governanceProgramId.toBase58(),
    issuer: issuer.publicKey.toBase58(),
    communityMint: mintKeypair.publicKey.toBase58(),
    issuerAta: issuerAta.toBase58(),
    realm: realmPk.toBase58(),
    tokenOwnerRecord: tokenOwnerRecordPk.toBase58(),
    governance: governancePk.toBase58(),
    nativeTreasury: treasuryPk.toBase58(),
    metadataUri: cfg.DAO_GOV_TOKEN_METADATA_URI,
    imageUrl: cfg.DAO_GOV_TOKEN_IMAGE_URL,
    txMint: sigMint,
    txMetadata: sigMetadata,
    txRealm: sigRealm,
    txDeposit: sigDeposit,
    txGovernanceTreasury: sigGov,
    txSetMintAuthorityToGovernance: sigSetMintAuthority,
    txSetRealmAuthority: sigSetRealmAuthority,
    votingTimeSec,
    thresholdPercent: thresholdPct,
    tokenSupply: supply,
    startBalanceLamports: startBalance,
    endBalanceLamports: endBalance,
    spentLamports,
    startBalanceSol: lamportsToSol(startBalance),
    endBalanceSol: lamportsToSol(endBalance),
    spentSol: lamportsToSol(spentLamports),
  };
  const reportDir = path.resolve(__dirname, "runs");
  fs.mkdirSync(reportDir, { recursive: true });
  const reportBaseName = `${nowStamp()}_${cfg.DAO_REALM_NAME.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80)}_full`;
  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}`,
      `realmName: ${report.realmName}`,
      `governanceProgramId: ${report.governanceProgramId}`,
      `issuer: ${report.issuer}`,
      `communityMint: ${report.communityMint}`,
      `issuerAta: ${report.issuerAta}`,
      `realm: ${report.realm}`,
      `tokenOwnerRecord: ${report.tokenOwnerRecord}`,
      `governance: ${report.governance}`,
      `nativeTreasury: ${report.nativeTreasury}`,
      `metadataUri: ${report.metadataUri}`,
      `imageUrl: ${report.imageUrl}`,
      `txMint: ${report.txMint}`,
      `txMetadata: ${report.txMetadata}`,
      `txRealm: ${report.txRealm}`,
      `txDeposit: ${report.txDeposit}`,
      `txGovernanceTreasury: ${report.txGovernanceTreasury}`,
      `txSetMintAuthorityToGovernance: ${report.txSetMintAuthorityToGovernance}`,
      `txSetRealmAuthority: ${report.txSetRealmAuthority}`,
      `tokenSupply: ${report.tokenSupply}`,
      `votingTimeSec: ${report.votingTimeSec}`,
      `thresholdPercent: ${report.thresholdPercent}`,
      `startBalanceSol: ${report.startBalanceSol}`,
      `endBalanceSol: ${report.endBalanceSol}`,
      `spentSol: ${report.spentSol}`,
      `configPath: ${report.configPath}`,
    ].join("\n") + "\n"
  );

  console.log("============================================================");
  console.log("DAO FULL СОЗДАНО");
  console.log("------------------------------------------------------------");
  console.log("Community mint (SPL + metadata):               ", mintKeypair.publicKey.toBase58());
  console.log("Realm:                                         ", realmPk.toBase58());
  console.log("Governance:                                    ", governancePk.toBase58());
  console.log("Native treasury PDA:                           ", treasuryPk.toBase58());
  console.log("Tx mint:                                       ", sigMint);
  console.log("Tx metadata:                                   ", sigMetadata);
  console.log("Tx realm:                                      ", sigRealm);
  console.log("Tx deposit:                                    ", sigDeposit);
  console.log("Tx governance+treasury:                        ", sigGov);
  console.log("Tx set mint authority -> governance:           ", sigSetMintAuthority);
  console.log("Tx set realm authority -> governance:           ", sigSetRealmAuthority);
  console.log("Баланс после:                                  ", `${lamportsToSol(endBalance)} SOL`);
  console.log("Потрачено:                                     ", `${lamportsToSol(spentLamports)} SOL`);
  console.log("Отчёт JSON:                                    ", reportJsonPath);
  console.log("Отчёт TXT:                                     ", reportTxtPath);
  console.log("============================================================");
}

main().catch((e) => {
  console.error("Ошибка создания DAO FULL:", e?.message || e);
  process.exit(1);
});
