import { expect } from 'chai';
import { BigNumber, BigNumberish, Contract, ContractTransaction } from 'ethers';
import { ethers } from 'hardhat';

import { Baal, BaalSummoner, MultiSend, Poster } from '../../src/types';
import { encodeMultiAction } from '../../src/util';
import { moveForwardPeriods } from './evm';
import { getSaltNonce } from './safe';

export type DAOSettings = {
    PROPOSAL_OFFERING: any;
    GRACE_PERIOD_IN_SECONDS: any;
    VOTING_PERIOD_IN_SECONDS: any;
    QUORUM_PERCENT: any;
    SPONSOR_THRESHOLD: any;
    MIN_RETENTION_PERCENT: any;
    MIN_STAKING_PERCENT: any;
    TOKEN_NAME: any;
    TOKEN_SYMBOL: any;
};

export const abiCoder = ethers.utils.defaultAbiCoder;

export const revertMessages = {
    molochAlreadyInitialized: "Initializable: contract is already initialized",
    molochConstructorSharesCannotBe0: "shares cannot be 0",
    molochConstructorVotingPeriodCannotBe0: "votingPeriod cannot be 0",
    submitProposalExpired: "expired",
    submitProposalOffering: "Baal requires an offering",
    submitProposalVotingPeriod: "!votingPeriod",
    submitProposalArrays: "!array parity",
    submitProposalArrayMax: "array max",
    submitProposalFlag: "!flag",
    sponsorProposalExpired: "expired",
    sponsorProposalSponsor: "!sponsor",
    sponsorProposalExists: "!exist",
    sponsorProposalSponsored: "sponsored",
    submitVoteNotSponsored: "!sponsored",
    submitVoteTimeEnded: "ended",
    submitVoteVoted: "voted",
    submitVoteMember: "!member",
    submitVoteWithSigTimeEnded: "ended",
    submitVoteWithSigVoted: "voted",
    submitVoteWithSigMember: "!member",
    proposalMisnumbered: "!exist",
    unsetGuildTokensLastToken:
      "reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)",
    sharesTransferPaused: "shares: !transferable",
    sharesInsufficientBalance:
      "ERC20: transfer amount exceeds balance",

    // -----
    lootAlreadyInitialized: "Initializable: contract is already initialized",
    molochSetupSharesNoShares: "shares != 0",
    proposalNotSponsored: "!sponsor",
    sponsorProposalNotSubmitted: "!submitted",
    submitVoteNotVoting: "!voting",
    processProposalNotReady: "!ready",
    ragequitUnordered: "!order",
    // unsetGuildTokensLastToken: 'reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)',
    sharesInsufficientApproval: "ERC20: insufficient allowance", // Error: Transaction reverted without a reason string
    lootTransferPaused: "loot: !transferable",
    lootInsufficientBalance:
      "ERC20: transfer amount exceeds balance",
    // lootInsufficientApproval: 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
    lootInsufficientApproval: "ERC20: insufficient allowance", // Error: Transaction reverted without a reason string
    mintSharesArrayParity: "!array parity",
    burnSharesArrayParity: "!array parity",
    burnSharesInsufficientShares: "ERC20: burn amount exceeds balance",
    mintLootArrayParity: "!array parity",
    burnLootArrayParity: "!array parity",
    burnLootInsufficientShares:
      "ERC20: burn amount exceeds balance",
    cancelProposalNotVoting: "!voting",
    cancelProposalNotCancellable: "!cancellable",
    baalOrAdmin: "!baal & !admin",
    baalOrManager: "!baal & !manager",
    baalOrGovernor: "!baal & !governor",
    permitNotAuthorized: "!authorized",
    permitExpired: "expired",
    notEnoughGas: "not enough gas",
    baalGasToHigh: "baalGas to high",
    OwnableCallerIsNotTheOwner: "Ownable: caller is not the owner",
};

export type ProposalType = {
    flag: BigNumberish;
    account?: `0x${string}`;
    data: string;
    details: string;
    expiration: BigNumberish;
    baalGas: BigNumberish;
};
  
export const PROPOSAL_STATES = {
    UNBORN: 0,
    SUBMITTED: 1,
    VOTING: 2,
    CANCELLED: 3,
    GRACE: 4,
    READY: 5,
    PROCESSED: 6,
    DEEFEATED: 7,
};

export enum SHAMAN_PERMISSIONS {
    NONE,
    ADMIN,
    MANAGER,
    ADMIN_MANAGER,
    GOVERNANCE,
    ADMIN_GOVERNANCE,
    MANAGER_GOVERNANCE,
    ALL,
};

export const defaultDAOSettings: DAOSettings = {
    GRACE_PERIOD_IN_SECONDS: 43200,
    VOTING_PERIOD_IN_SECONDS: 432000,
    PROPOSAL_OFFERING: 0,
    SPONSOR_THRESHOLD: 1,
    MIN_RETENTION_PERCENT: 0,
    MIN_STAKING_PERCENT: 0,
    QUORUM_PERCENT: 0,
    TOKEN_NAME: "wrapped ETH",
    TOKEN_SYMBOL: "WETH",
};

export const defaultMetadataConfig = {
    CONTENT: '{"name":"test"}',
    TAG: "daohaus.summoner.daoProfile",
};

export const defaultProposalSettings = {
    DETAILS: 'all hail baal',
    EXPIRATION: 0,
    BAAL_GAS: 0,
};

export type SummonSetup = {
    loot: number;
    lootPaused: boolean;
    shamanPermissions: SHAMAN_PERMISSIONS;
    shares: number;
    sharesPaused: boolean;
};

export const defaultSummonSetup: SummonSetup = {
    loot: 500,
    lootPaused: false,
    shamanPermissions: SHAMAN_PERMISSIONS.ALL,
    shares: 100,
    sharesPaused: false,
};

export type NewBaalParams = {
    baalSummoner: Contract;
    baalSingleton: Baal;
    poster: Poster;
    config: DAOSettings;
    adminConfig: [boolean, boolean];
    shamans: [string[], number[]];
    shares: [string[], number[]];
    loots: [string[], number[]];
    safeAddress?: `0x${string}`;
    forwarderAddress?: `0x${string}`;
    lootAddress?: `0x${string}`;
    sharesAddress?: `0x${string}`;
    saltNonceOverride?: string;
}

export type NewBaalAddresses = {
    baal: string;
    loot: string;
    shares: string;
    safe: string;
}

export const getNewBaalAddresses = async (tx: ContractTransaction): Promise<NewBaalAddresses> => {
    const receipt = await ethers.provider.getTransactionReceipt(tx.hash);
    // console.log({logs: receipt.logs})
    let baalSummonAbi = [
        "event SummonBaal(address indexed baal, address indexed loot, address indexed shares, address safe, address forwarder, uint256 existingAddrs)",
    ];
    let iface = new ethers.utils.Interface(baalSummonAbi);
    let log = iface.parseLog(receipt.logs[receipt.logs.length - 1]);
    const { baal, loot, shares, safe } = log.args;
    return { baal, loot, shares, safe };
};

export const getBaalParams = async function (
    baal: Baal,
    poster: Poster,
    config: DAOSettings,
    adminConfig: [boolean, boolean],
    shamans: [string[], number[]],
    shares: [string[], number[]],
    loots: [string[], number[]],
    safeAddress: string = ethers.constants.AddressZero,
    forwarderAddress: string = ethers.constants.AddressZero,
    lootAddress: string = ethers.constants.AddressZero,
    sharesAddress: string = ethers.constants.AddressZero,
) {
    const governanceConfig = abiCoder.encode(
        ["uint32", "uint32", "uint256", "uint256", "uint256", "uint256"],
        [
            config.VOTING_PERIOD_IN_SECONDS,
            config.GRACE_PERIOD_IN_SECONDS,
            config.PROPOSAL_OFFERING,
            config.QUORUM_PERCENT,
            config.SPONSOR_THRESHOLD,
            config.MIN_RETENTION_PERCENT,
        ]
    );

    const setAdminConfig = await baal.interface.encodeFunctionData(
        "setAdminConfig",
        adminConfig
    );
    const setGovernanceConfig = await baal.interface.encodeFunctionData(
        "setGovernanceConfig",
        [governanceConfig]
    );
    const setShaman = await baal.interface.encodeFunctionData(
        "setShamans",
        shamans
    );
    const mintShares = await baal.interface.encodeFunctionData(
        "mintShares",
        shares
    );
    const mintLoot = await baal.interface.encodeFunctionData("mintLoot", loots);
    const postMetaData = await poster.interface.encodeFunctionData("post", [
        defaultMetadataConfig.CONTENT,
        defaultMetadataConfig.TAG,
    ]);
    const posterFromBaal = await baal.interface.encodeFunctionData(
        "executeAsBaal",
        [poster.address, 0, postMetaData]
    );

    const initalizationActions = [
        setAdminConfig,
        setGovernanceConfig,
        setShaman,
        mintShares,
        mintLoot,
        posterFromBaal,
    ];

    return {
        initParams: abiCoder.encode(
            ["string", "string", "address", "address", "address", "address"],
            [
                config.TOKEN_NAME,
                config.TOKEN_SYMBOL,
                safeAddress,
                forwarderAddress,
                lootAddress,
                sharesAddress,
            ],
        ),
        initalizationActions,
    };
};

// export const setupBaal = async (params: NewBaalParams) => {
export const setupBaal = async ({
    baalSummoner,
    baalSingleton,
    poster,
    config,
    adminConfig,
    shamans,
    shares,
    loots,
    safeAddress,
    forwarderAddress,
    lootAddress,
    sharesAddress,
    saltNonceOverride
}: NewBaalParams) => {
    const saltNonce = saltNonceOverride || getSaltNonce();
    const encodedInitParams = await getBaalParams(
        baalSingleton,
        poster,
        config,
        adminConfig,
        shamans,
        shares,
        loots,
        safeAddress,
        forwarderAddress,
        lootAddress,
        sharesAddress,
    );
    const tx = await (baalSummoner as BaalSummoner).summonBaal(
        encodedInitParams.initParams,
        encodedInitParams.initalizationActions,
        saltNonce,
    );
    return await getNewBaalAddresses(tx);
};

export type ProposalParams = {
    baal: Baal;
    encodedAction: string;
    proposal: ProposalType;
    proposalId?: BigNumberish;
    daoSettings?: DAOSettings;
    extraSeconds?: number;
};

export const submitAndProcessProposal = async ({
    baal,
    encodedAction,
    proposal,
    proposalId,
    daoSettings = defaultDAOSettings,
    extraSeconds = 2,
  }: ProposalParams) => {
    await baal.submitProposal(encodedAction, proposal.expiration, proposal.baalGas, ethers.utils.id(proposal.details));
    const id = proposalId ? proposalId : await baal.proposalCount();
    await baal.submitVote(id, true);
    await moveForwardPeriods(daoSettings.VOTING_PERIOD_IN_SECONDS, extraSeconds);
    return await baal.processProposal(id, encodedAction);
  };
  
export const setShamanProposal = async (
    baal: Baal,
    multisend: MultiSend,
    shamanAddress: string,
    permission: BigNumberish,
    daoSettings = defaultDAOSettings,
    extraSeconds = 2,
) => {
    const setShaman = baal.interface.encodeFunctionData('setShamans', [
      [shamanAddress],
      [permission],
    ]);
    const setShamanAction = encodeMultiAction(
      multisend,
      [setShaman],
      [baal.address],
      [BigNumber.from(0)],
      [0]
    );
  
    // ----
    await baal.submitProposal(setShamanAction, 0, 0, '');
    const proposalId = await baal.proposalCount();
    await baal.submitVote(proposalId, true);
    await moveForwardPeriods(daoSettings.VOTING_PERIOD_IN_SECONDS, extraSeconds);
    await baal.processProposal(proposalId, setShamanAction);
    return proposalId;
};

export const verifyProposal = (prop1: any, prop2: any, overrides?: any) => {
    for (let key in prop1) {
      if (Number.isInteger(+key)) {
        continue;
      }
      if (overrides && key in overrides) {
        // console.log('override', key)
        expect(prop1[key]).to.equal(overrides[key]);
      } else {
        // console.log('check', key)
        expect(prop1[key]).to.equal(prop2[key]);
      }
    }
};
