import { findReplicaMintAddress } from "@quarryprotocol/quarry-sdk";
import type { Network } from "@saberhq/solana-contrib";
import { exists } from "@saberhq/solana-contrib";
import type { TokenInfo, TokenList } from "@saberhq/token-utils";
import { deserializeMint, networkToChainId, Token } from "@saberhq/token-utils";
import { PublicKey } from "@solana/web3.js";
import * as fs from "fs/promises";
import { chunk, uniq, zip } from "lodash";
import invariant from "tiny-invariant";

import type { RewarderInfo, RewarderMeta } from "../types";
import { makeProvider, stringify } from "../utils";

const dedupeTokenList = (tokens: TokenInfo[]): TokenInfo[] => {
  return tokens
    .filter((tok, i) => {
      const prev = tokens.findIndex(
        (otherTok) => tok.address === otherTok.address
      );
      return prev === i;
    })
    .sort((a, b) => a.address.localeCompare(b.address));
};

const makeIOUTokenInfo = (
  mint: PublicKey,
  underlying: TokenInfo
): TokenInfo => ({
  ...underlying,
  symbol: `iou${underlying.symbol}`,
  name: `${underlying.name} (IOU)`,
  address: mint.toString(),
  tags: [...(underlying.tags ?? []), "quarry-iou"],
  extensions: {
    ...underlying.extensions,
    underlyingTokens: [underlying.address],
    source: "quarry-iou",
  },
});

const makeReplicaTokenInfo = (
  mint: PublicKey,
  primary: TokenInfo
): TokenInfo => ({
  ...primary,
  symbol: `qr${primary.symbol}`,
  name: `${primary.name} (Replica)`,
  address: mint.toString(),
  tags: [...(primary.tags ?? []), "quarry-merge-mine-replica"],
  extensions: {
    ...primary.extensions,
    underlyingTokens: [primary.address],
    source: "quarry-merge-mine-replica",
  },
});

export const buildTokenList = async (network: Network): Promise<void> => {
  const provider = makeProvider(network);

  const dir = `${__dirname}/../../data/${network}`;
  const lists = JSON.parse(
    (await fs.readFile(".tmp.token-list.json")).toString()
  ) as TokenList[];

  const rewarderList = JSON.parse(
    (await fs.readFile(`${dir}/rewarder-list.json`)).toString()
  ) as RewarderInfo[];
  const allRewarders = JSON.parse(
    (await fs.readFile(`${dir}/all-rewarders.json`)).toString()
  ) as Record<string, RewarderMeta>;

  const allMints = uniq([
    ...rewarderList
      .map((rwl) => rwl.redeemer?.underlyingToken)
      .filter((x): x is string => !!x),
    ...Object.values(allRewarders).map((r) => r.rewardsToken.mint),
    ...Object.keys(
      JSON.parse(
        (await fs.readFile(`${dir}/rewarders-by-mint.json`)).toString()
      ) as Record<string, unknown>
    ),
  ]).map((x) => new PublicKey(x));

  const allTokens = lists.flatMap((l) => l.tokens);

  const missingMints: PublicKey[] = [];
  const tokenListTokens = allMints
    .map((mint) => {
      const info = allTokens.find(
        (tok) =>
          tok.chainId === networkToChainId(network) &&
          tok.address === mint.toString()
      );
      if (info) {
        return info;
      }
      missingMints.push(mint);
      return null;
    })
    .filter((x): x is TokenInfo => !!x);

  const iouTokens = rewarderList
    .filter((rwl) => !!rwl.redeemer?.underlyingToken)
    .map((rwl) => {
      const underlyingStr = rwl.redeemer?.underlyingToken;
      const real = allRewarders[rwl.address];
      if (!real || !underlyingStr) {
        return null;
      }
      const redemptionInfo = tokenListTokens.find(
        (tok) => tok.address === real.rewardsToken.mint
      );
      if (redemptionInfo && rwl.redeemer?.method !== "quarry-redeemer") {
        return redemptionInfo;
      }
      const underlyingInfo = tokenListTokens.find(
        (tok) => tok.address === underlyingStr
      );
      if (!underlyingInfo) {
        return null;
      }
      return makeIOUTokenInfo(
        new PublicKey(real.rewardsToken.mint),
        underlyingInfo
      );
    })
    .filter(exists);
  console.log(`Found ${iouTokens.length} Quarry Redeemer IOU tokens`);

  const underlyingTokens = tokenListTokens
    .flatMap((tok) => {
      return (
        tok.extensions?.underlyingTokens?.map((ut) => {
          return allTokens.find(
            (t) =>
              t.address === ut.toString() &&
              t.chainId === networkToChainId(network)
          );
        }) ?? []
      );
    })
    .filter((t): t is TokenInfo => !!t);

  // check for all replicas that have a quarry
  const replicaMappings = (
    await Promise.all(
      allMints.map(async (mint) => {
        const [replicaMint] = await findReplicaMintAddress({
          primaryMint: mint,
        });
        return { replicaMint, underlyingMint: mint };
      })
    )
  ).filter((rm) => allMints.find((m) => m.equals(rm.replicaMint)));

  const missingReplicaMappings: {
    replicaMint: PublicKey;
    underlyingMint: PublicKey;
  }[] = [];
  const tokenListReplicas = missingMints
    .map((replicaMint): TokenInfo | null => {
      const replicaMapping = replicaMappings.find((rm) =>
        rm.replicaMint.equals(replicaMint)
      );
      if (replicaMapping) {
        const existingToken = tokenListTokens.find(
          (tok) => tok.address === replicaMapping.underlyingMint.toString()
        );
        if (existingToken) {
          return makeReplicaTokenInfo(
            replicaMapping.replicaMint,
            existingToken
          );
        } else {
          missingReplicaMappings.push(replicaMapping);
        }
      }
      return null;
    })
    .filter((x): x is TokenInfo => !!x);

  const missingMintsNonReplica = missingMints.filter(
    (mm) => !missingReplicaMappings.find((mrm) => mrm.replicaMint.equals(mm))
  );
  const missingMintsData = (
    await Promise.all(
      chunk(missingMintsNonReplica, 100).map(async (mintsChunk) =>
        provider.connection.getMultipleAccountsInfo(mintsChunk)
      )
    )
  ).flat();
  const missingTokens = zip(missingMintsNonReplica, missingMintsData).map(
    ([mint, mintDataRaw]) => {
      invariant(mint);
      invariant(mintDataRaw);
      return Token.fromMint(mint, deserializeMint(mintDataRaw.data).decimals, {
        chainId: networkToChainId(network),
      }).info;
    }
  );
  const missingReplicaTokens = missingReplicaMappings.map(
    ({ replicaMint, underlyingMint }) => {
      const existingToken = missingTokens.find(
        (tok) => tok.address === underlyingMint.toString()
      );
      invariant(
        existingToken,
        `missing ${underlyingMint.toString()} for ${replicaMint.toString()}`
      );
      return makeReplicaTokenInfo(replicaMint, existingToken);
    }
  );

  const tokens = dedupeTokenList([
    ...iouTokens,
    ...tokenListTokens,
    ...underlyingTokens,
    ...tokenListReplicas,
    ...missingTokens,
    ...missingReplicaTokens,
  ]);

  const list: TokenList = {
    name: `Quarry Token List (${network})`,
    logoURI:
      "https://raw.githubusercontent.com/QuarryProtocol/rewarder-list/master/icon.png",
    tags: lists.reduce((acc, list) => ({ ...acc, ...list.tags }), {}),
    timestamp: new Date().toISOString(),
    tokens,
  };

  await fs.mkdir("data/", { recursive: true });
  await fs.writeFile(`${dir}/token-list.json`, stringify(list));
};

Promise.all([buildTokenList("mainnet-beta"), buildTokenList("devnet")]).catch(
  (err) => {
    console.error(err);
    process.exit(1);
  }
);