import { BigNumber, BigNumberish, ethers } from 'ethers';
import { parseUnits } from 'ethers/lib/utils';
import { Splendid } from '@typechain/Splendid';
import * as splendidJson from '../out/Splendid.sol/Splendid.json';
import { BasisPoints, MerkleTreeNode, HumanReadableAmount } from './types';
import { Duration } from 'luxon';
import { MerkleTree } from 'merkletreejs';
import { MIN_EPOCH_LENGTH_SECONDS } from './constants';
import { keccak256 } from '@ethersproject/keccak256';

export class Client {
  private splendid: Splendid;
  private splendidInterface: ethers.utils.Interface;

  constructor(splendidContractAddress: string, signerOProvider: ethers.providers.Provider | ethers.Signer) {
    if (!signerOProvider) {
      throw new Error('Must provide a provider or signer');
    }

    if (!splendidContractAddress) {
      throw new Error('RPC URL passed to client must be defined');
    }

    this.splendid = new ethers.Contract(splendidContractAddress, splendidJson.abi, signerOProvider) as Splendid;

    this.splendidInterface = new ethers.utils.Interface(splendidJson.abi);
  }

  public async createCollection({
    curatorFee,
    initialCollectionContent,
    epochLength,
  }: {
    curatorFee: HumanReadableAmount;
    initialCollectionContent: MerkleTreeNode[];
    epochLength: Duration;
  }): Promise<ethers.ContractTransaction> {
    const seenCreatorIds: Set<BigNumberish> = new Set();
    const initialCollectionFeeBips: BasisPoints = initialCollectionContent.reduce((acc: BasisPoints, currEntry: MerkleTreeNode) => {
      const feeBips = currEntry.currentFeeBips;
      const creatorId = currEntry.creatorId;
      // Verify that the fee BIPs is a positive number.
      if (feeBips <= 0) {
        throw new Error(`Fee bips cannot be ${feeBips} for creator ID ${creatorId} (must be positive).`);
      }

      // Verify that this creator ID is not a duplicate.
      if (seenCreatorIds.has(creatorId)) {
        throw new Error(`Creator ID ${creatorId} is duplicated in the collection.`);
      }

      // Mark this creator ID as seen.
      seenCreatorIds.add(creatorId);

      return acc + feeBips;
    }, 0);

    // Ensure the curator fee is not negative.
    const curatorFeeBigNum: BigNumber = parseUnits(curatorFee);
    if (curatorFeeBigNum.isNegative()) {
      throw new Error(`Curator fee cannot be negative (provided value was ${curatorFee})`);
    }

    // Validate that the epoch length is not less than the minimum epoch length.
    const epochLengthSeconds = epochLength.toMillis() / 1000;
    if (epochLengthSeconds < MIN_EPOCH_LENGTH_SECONDS) {
      throw new Error(`Epoch length in seconds of ${epochLengthSeconds} is less than minimum epoch length of ${MIN_EPOCH_LENGTH_SECONDS}`);
    }

    // Create the initial collection root from the provided content.
    const merkleTree: MerkleTree = this.createMerkleTreeFromCollectionContent(initialCollectionContent);
    const initialCollectionRoot: string = merkleTree.getHexRoot();

    return this.splendid.createCollection(curatorFeeBigNum, initialCollectionRoot, initialCollectionFeeBips, epochLengthSeconds);
  }

  public async settleCreatorEpochRewardsFromCollection({
    collectionOwner,
    collectionIndex,
    startingEpoch,
    startingEpochTreeNode,
    startingMerkleTreeLeaves,
    endingEpoch,
    endingEpochTreeNode,
    endingMerkleTreeLeaves,
  }: {
    collectionOwner: string;
    collectionIndex: number;
    startingEpoch: BigNumberish;
    startingEpochTreeNode: MerkleTreeNode;
    startingMerkleTreeLeaves: string[];
    endingEpoch: BigNumberish;
    endingEpochTreeNode: MerkleTreeNode;
    endingMerkleTreeLeaves: string[];
  }): Promise<ethers.ContractTransaction> {
    // Validate that the collection owner is a valid Ethereum address.
    if (!ethers.utils.isAddress(collectionOwner)) {
      throw new Error(`Collection owner ${collectionOwner} is not a valid Ethereum address.`);
    }

    // Validate the provided epochs are not negative.
    if (startingEpoch < 0 || endingEpoch < 0) {
      throw new Error(`Epochs cannot be negative`);
    }

    // Validate the starting epoch does not exceed the ending epoch.
    if (startingEpoch > endingEpoch) {
      throw new Error(`Starting epoch ${startingEpoch} cannot exceed ending epoch ${endingEpoch}`);
    }

    // Validate the creator ID for both merkle tree nodes matches.
    if (startingEpochTreeNode.creatorId !== endingEpochTreeNode.creatorId) {
      throw new Error(`Epoch tree nodes should be the same (starting: ${startingEpochTreeNode.creatorId}, ending: ${endingEpochTreeNode.creatorId})`);
    }

    // TODO: Any other validations that should be performed? Unfortunately might require reading on-chain state.

    // Create the merkle proofs for the starting and ending epoch tree nodes.
    const startingMerkleTree = this.createMerkleTree(startingMerkleTreeLeaves);
    const startingEpochMerkleProof = this.getMerkleProof(startingMerkleTree, startingEpochTreeNode);

    const endingMerkleTree = this.createMerkleTree(endingMerkleTreeLeaves);
    const endingEpochMerkleProof = this.getMerkleProof(endingMerkleTree, endingEpochTreeNode);

    return this.splendid.settleCreatorEpochRewardsFromCollection(
      collectionOwner,
      collectionIndex,
      startingEpoch,
      startingEpochTreeNode,
      startingEpochMerkleProof,
      endingEpoch,
      endingEpochTreeNode,
      endingEpochMerkleProof,
    );
  }

  public async claimCreatorRewards({ creatorId }: { creatorId: BigNumberish }): Promise<ethers.ContractTransaction> {
    // TODO: Validate that creatorId exists? Requires reading on-chain state.

    return this.splendid.claimCreatorRewards(creatorId);
  }

  public async claimCuratorRewards({ collectionIndex }: { collectionIndex: BigNumberish }): Promise<ethers.ContractTransaction> {
    // TODO: Validate that collectionIndx exists? Requires reading on-chain state.

    return this.splendid.claimCuratorRewards(collectionIndex);
  }

  public async subscribeToCollection({
    collectionOwner,
    collectionIndex,
    subscriber,
    payer,
  }: {
    collectionOwner: string;
    collectionIndex: BigNumberish;
    subscriber: string;
    payer: string;
  }): Promise<ethers.ContractTransaction> {
    // Validate that the collection owner is a valid Ethereum address.
    if (!ethers.utils.isAddress(collectionOwner)) {
      throw new Error(`Collection owner ${collectionOwner} is not a valid Ethereum address.`);
    }

    // Validate that the subscriber is a valid Ethereum address.
    if (!ethers.utils.isAddress(subscriber)) {
      throw new Error(`Subscriber ${subscriber} is not a valid Ethereum address.`);
    }

    // Validate that the payer is a valid Ethereum address.
    if (!ethers.utils.isAddress(payer)) {
      throw new Error(`Payer ${payer} is not a valid Ethereum address.`);
    }

    return this.splendid.subscribeToCollection(collectionOwner, collectionIndex, subscriber, payer);
  }

  public async parseEventLog(data: string, topics: string[]): Promise<ethers.utils.LogDescription> {
    return this.splendidInterface.parseLog({
      topics,
      data,
    });
  }

  private createMerkleTreeFromCollectionContent(collectionContent: MerkleTreeNode[]): MerkleTree {
    const leaves: string[] = collectionContent.map((content: MerkleTreeNode) => {
      // TODO: Verify the leaf hashes match with solidity implementation.
      return this.hashLeafNode(content);
    });

    return this.createMerkleTree(leaves);
  }

  private hashLeafNode(node: MerkleTreeNode): string {
    // TODO: Verify the leaf hashes match with solidity implementation.
    return ethers.utils.hexlify(
      Buffer.from(
        ethers.utils.solidityKeccak256(['uint256', `uint224`, `uint32`], [node.creatorId, node.lastUpdatedEpoch, node.lastUpdatedEpoch]).slice(2),
        'hex',
      ),
    );
  }

  private createMerkleTree(leaves: string[]): MerkleTree {
    const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true });
    return merkleTree;
  }

  private getMerkleProof(tree: MerkleTree, node: MerkleTreeNode): string[] {
    const leaf = this.hashLeafNode(node);
    return tree.getHexProof(leaf);
  }
}
