import { ethers } from 'ethers';
import {
  ACCOUNTABILITY_RULE_SCHEMA_HASH,
  ACCOUNTABILITY_TERMS_TYPEHASH,
  EIP712_DOMAIN_TYPEHASH,
  MUTUAL_SETTLEMENT_TYPEHASH,
  OBJECTIVE_EVIDENCE_TYPEHASH,
  PARTNERSHIP_TERMS_TYPEHASH,
  TYPED_RESOLUTION_TYPEHASH,
} from './constants';
import type {
  AccountabilityBondTerms, Address, Bytes32, EnforcementConfig,
  ObjectiveEvidence, PartnershipBondTerms, PartnershipMutualSettlement,
  SignerConfig, SignerIndependenceContext, TypedResolution, V3ChainId,
} from './types';

const coder = ethers.AbiCoder.defaultAbiCoder();
const ZERO_BYTES32 = `0x${'00'.repeat(32)}` as Bytes32;

const encodedHash = (types: readonly string[], values: readonly unknown[]): Bytes32 =>
  ethers.keccak256(coder.encode(types, values)) as Bytes32;

export function hashSignerSet(signers: readonly Address[]): Bytes32 {
  return encodedHash(['address[]'], [signers]);
}

export function hashEnforcementConfig(config: EnforcementConfig): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'address', 'bytes32', 'bytes32', 'bytes32', 'uint64', 'uint64',
      'uint64', 'uint64', 'uint256', 'bytes32', 'bytes32', 'bytes32',
      'bytes32', 'uint8', 'uint8',
    ],
    [
      ACCOUNTABILITY_RULE_SCHEMA_HASH, config.canonicalSource,
      config.canonicalSourceCodehash, config.canonicalSourceId,
      config.obligationId, config.requiredBy,
      config.responseWindow, config.challengeWindow, config.decisionWindow,
      config.maximumSlash, config.evidenceSchemaHash, config.privacyExclusionsHash,
      config.attesterSetHash, config.adjudicatorSetHash,
      config.attesterThreshold, config.adjudicatorThreshold,
    ],
  );
}

export function hashAccountabilityTerms(terms: AccountabilityBondTerms): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'uint256', 'address', 'address', 'uint256', 'address',
      'bytes32', 'bytes32', 'uint8', 'uint16', 'bytes32', 'uint256',
      'uint64', 'uint64', 'uint64', 'uint96', 'uint256', 'bytes32',
    ],
    [
      ACCOUNTABILITY_TERMS_TYPEHASH, terms.bondId, terms.aiCompany,
      terms.counterparty, terms.companyStake, terms.stakeAsset, ZERO_BYTES32,
      terms.objectiveRuleHash, terms.objectiveRuleClass,
      terms.objectiveRuleSchemaVersion, ZERO_BYTES32, 0n,
      terms.activePeriodStart, terms.activePeriodEnd, terms.challengeWindow,
      terms.nonce, terms.chainId, ethers.keccak256(ethers.toUtf8Bytes(terms.protocolVersion)),
    ],
  );
}

export function hashObjectiveEvidence(evidence: ObjectiveEvidence): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'address', 'uint256', 'uint256', 'bytes32', 'bytes32',
      'address', 'bytes32', 'bytes32', 'uint64', 'uint64', 'uint64',
      'uint96', 'uint64',
    ],
    [
      OBJECTIVE_EVIDENCE_TYPEHASH, evidence.bond, evidence.chainId,
      evidence.bondId, evidence.termsHash, evidence.ruleHash,
      evidence.canonicalSource, evidence.canonicalSourceId,
      evidence.obligationId, evidence.requiredBy, evidence.recordedAt,
      evidence.observedAt, evidence.nonce, evidence.signatureDeadline,
    ],
  );
}

export function hashTypedResolution(resolution: TypedResolution): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'address', 'uint256', 'uint256', 'bytes32', 'bytes32',
      'bytes32', 'bytes32', 'bool', 'uint96', 'uint64',
    ],
    [
      TYPED_RESOLUTION_TYPEHASH, resolution.bond, resolution.chainId,
      resolution.bondId, resolution.termsHash, resolution.ruleHash,
      resolution.evidenceHash, resolution.challengeHash,
      resolution.evidenceValid, resolution.nonce, resolution.deadline,
    ],
  );
}

export function hashPartnershipTerms(terms: PartnershipBondTerms): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'uint256', 'address', 'address', 'uint256', 'uint256',
      'address', 'bytes32', 'bytes32', 'uint8', 'uint16', 'bytes32',
      'uint256', 'uint64', 'uint64', 'uint64', 'uint64', 'uint96',
      'uint256', 'bytes32',
    ],
    [
      PARTNERSHIP_TERMS_TYPEHASH, terms.bondId, terms.partyA, terms.partyB,
      terms.partyAStake, terms.partyBStake, terms.stakeAsset,
      terms.partnershipType, ZERO_BYTES32, terms.objectiveRuleClass,
      terms.objectiveRuleSchemaVersion, ZERO_BYTES32, 0n,
      terms.proposalDeadline, terms.activePeriodStart, terms.activePeriodEnd,
      terms.challengeWindow, terms.nonce, terms.chainId,
      ethers.keccak256(ethers.toUtf8Bytes(terms.protocolVersion)),
    ],
  );
}

export function hashPartnershipMutualSettlement(
  settlement: PartnershipMutualSettlement,
): Bytes32 {
  return encodedHash(
    [
      'bytes32', 'address', 'uint256', 'uint256', 'bytes32', 'uint256',
      'uint256', 'uint96', 'uint64',
    ],
    [
      MUTUAL_SETTLEMENT_TYPEHASH, settlement.bond, settlement.chainId,
      settlement.bondId, settlement.termsHash, settlement.partyAAmount,
      settlement.partyBAmount, settlement.nonce, settlement.deadline,
    ],
  );
}

export function domainSeparator(
  chainId: V3ChainId,
  verifyingContract: Address,
): Bytes32 {
  return encodedHash(
    ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
    [
      EIP712_DOMAIN_TYPEHASH,
      ethers.keccak256(ethers.toUtf8Bytes('Vaultfire')),
      ethers.keccak256(ethers.toUtf8Bytes('3.0.0')),
      chainId,
      verifyingContract,
    ],
  );
}

export function typedDataDigest(domain: Bytes32, structHash: Bytes32): Bytes32 {
  return ethers.keccak256(ethers.concat(['0x1901', domain, structHash])) as Bytes32;
}

export function validateSignerConfig(
  config: SignerConfig,
  enforcement: EnforcementConfig,
  _context: SignerIndependenceContext,
): void {
  if (!Array.isArray(config.attesters) || !Array.isArray(config.adjudicators) ||
      config.attesters.length !== 0 || config.adjudicators.length !== 0 ||
      enforcement.attesterThreshold !== 0 || enforcement.adjudicatorThreshold !== 0 ||
      enforcement.attesterSetHash !== ZERO_BYTES32 ||
      enforcement.adjudicatorSetHash !== ZERO_BYTES32) {
    throw new TypeError(
      'AccountabilityBondV3Safe requires empty signer sets, zero thresholds, and zero set hashes',
    );
  }
}
