import { ethers } from 'ethers';
import { V3_ABI_IDENTITIES, V3_WRITE_METHODS } from './abi';
import { V3_NETWORK_PROFILES, V3_ROUTE_METADATA } from './constants';
import { V3DegradedError, V3UnavailableError, V3WritesBlockedError } from './errors';
import { cloneAndDeepFreeze, DeepReadonly } from './freeze';
import {
  RuntimeDeploymentVerifier, verifyAuthorizedDeployment,
} from './internal/deployment';
import { V3_SUPPORTED_DEPLOYMENT_NAMES } from './supported';
import type {
  Address, BondReadModel, Bytes32, CapabilityReadModel, GovernanceReadModel,
  Hex, MandateReadModel, PullCreditReadModel, SolvencyReadModel,
  V3DeploymentName, V3NetworkProfile, V3ReadRoute,
  X402ChallengeReadModel, X402DiscoveryReadModel, X402ReceiptReadModel,
  X402StatusReadModel,
} from './types';

const DEPLOYMENTS: readonly V3DeploymentName[] = V3_SUPPORTED_DEPLOYMENT_NAMES;
const UINT256_MAX = (1n << 256n) - 1n;
const UINT96_MAX = (1n << 96n) - 1n;
const UINT64_MAX = (1n << 64n) - 1n;
const ZERO_BYTES32 = `0x${'00'.repeat(32)}` as Bytes32;

export interface V3ReadTransport {
  read(route: V3ReadRoute, query: Readonly<Record<string, unknown>>): Promise<unknown>;
}

/** Independent environment attestor. It must not be implemented by the transaction sender. */
export interface V3DeploymentVerifier extends RuntimeDeploymentVerifier {}

export interface V3WriteTransport {
  sendTransaction(transaction: Readonly<{
    to: Address;
    data: Hex;
    value: bigint;
  }>): Promise<Bytes32>;
}

export interface CapabilityEvidence {
  schema: 'vaultfire.v3.capability-evidence.v1';
  chainId: 8453 | 84532;
  contract: Address;
  capability: Bytes32;
  subject: Address;
  issuedAt: bigint;
  expiresAt: bigint;
  evidenceHash: Bytes32;
}

export interface V3CapabilityVerifier {
  verify(evidence: CapabilityEvidence): Promise<boolean>;
}

export interface V3WriteRequest {
  deployment: V3DeploymentName;
  capability: Bytes32;
  subject: Address;
  /** Non-empty calldata whose selector is allowlisted for the selected deployment. */
  data: Hex;
  value?: bigint;
  evidence: CapabilityEvidence;
}

export interface V3ClientConfig {
  profile: V3NetworkProfile;
  readTransport?: V3ReadTransport;
  writeTransport?: V3WriteTransport;
  deploymentVerifier?: V3DeploymentVerifier;
  capabilityVerifier?: V3CapabilityVerifier;
  now?: () => bigint;
}

const record = (value: unknown): value is Record<string, unknown> =>
  value !== null && typeof value === 'object' && !Array.isArray(value);
const validAddress = (value: unknown): value is Address =>
  typeof value === 'string' && ethers.isAddress(value) && value !== ethers.ZeroAddress;
const validAnyAddress = (value: unknown): value is Address =>
  typeof value === 'string' && ethers.isAddress(value);
const validBytes32 = (value: unknown): value is Bytes32 =>
  typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value);
const validUint = (value: unknown, maximum = UINT256_MAX): value is bigint =>
  typeof value === 'bigint' && value >= 0n && value <= maximum;
const validEnum = (value: unknown, maximum: number): boolean =>
  typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= maximum;
const sameHex = (left: string, right: string): boolean =>
  left.toLowerCase() === right.toLowerCase();

const routeDeployments: Record<V3ReadRoute, readonly V3DeploymentName[]> = {
  bonds: ['accountabilityBond', 'partnershipBond'],
  pullCredits: ['accountabilityBond', 'partnershipBond'],
  solvency: ['accountabilityBond', 'partnershipBond'],
  mandates: ['mandateRegistry'],
  capabilities: [],
  taskEscrow: [],
  governance: ['governance'],
  'x402.status': [],
  'x402.discovery': [],
  'x402.challenge': [],
  'x402.receipt': [],
};

const exactKeys = (value: Record<string, unknown>, expected: readonly string[]): boolean => {
  const prototype = Object.getPrototypeOf(value);
  if (prototype !== null && prototype !== Object.prototype) return false;
  const ownKeys = Reflect.ownKeys(value);
  if (ownKeys.some((key) => typeof key === 'symbol')) return false;
  const actual = (ownKeys as string[]).sort();
  const canonical = [...expected].sort();
  if (actual.length !== canonical.length ||
      !actual.every((key, index) => key === canonical[index])) return false;
  return actual.every((key) => {
    const descriptor = Object.getOwnPropertyDescriptor(value, key);
    return descriptor !== undefined && 'value' in descriptor && descriptor.enumerable === true;
  });
};
function assertShape(
  value: unknown,
  expected: readonly string[],
  label: string,
): asserts value is Record<string, unknown> {
  if (!record(value) || !exactKeys(value, expected)) {
    throw new TypeError(`${label} has a noncanonical shape`);
  }
}
const addressEquals = (left: unknown, right: Address | undefined): boolean =>
  right !== undefined && validAddress(left) && sameHex(left, right);
const oneOfAddresses = (value: unknown, addresses: readonly Address[]): boolean =>
  validAddress(value) && addresses.some((address) => sameHex(value, address));
const validNullable = (value: unknown, validator: (candidate: unknown) => boolean): boolean =>
  value === null || validator(value);

function assertAccountabilityTerms(value: unknown, chainId: number): void {
  assertShape(value, [
    'bondId', 'aiCompany', 'counterparty', 'companyStake', 'stakeAsset',
    'objectiveRuleHash', 'objectiveRuleClass', 'objectiveRuleSchemaVersion',
    'activePeriodStart', 'activePeriodEnd', 'challengeWindow', 'nonce',
    'chainId', 'protocolVersion',
  ], 'accountability terms');
  if (!validUint(value.bondId) || !validAddress(value.aiCompany) ||
      !validAddress(value.counterparty) || !validUint(value.companyStake) ||
      !validAnyAddress(value.stakeAsset) || !validBytes32(value.objectiveRuleHash) ||
      value.objectiveRuleClass !== 1 || value.objectiveRuleSchemaVersion !== 1 ||
      !validUint(value.activePeriodStart, UINT64_MAX) ||
      !validUint(value.activePeriodEnd, UINT64_MAX) ||
      !validUint(value.challengeWindow, UINT64_MAX) ||
      !validUint(value.nonce, UINT96_MAX) || value.chainId !== chainId ||
      value.protocolVersion !== '3.0.0') {
    throw new TypeError('accountability terms contain invalid values');
  }
}

function assertPartnershipTerms(value: unknown, chainId: number): void {
  assertShape(value, [
    'bondId', 'partyA', 'partyB', 'partyAStake', 'partyBStake', 'stakeAsset',
    'partnershipType', 'objectiveRuleClass', 'objectiveRuleSchemaVersion',
    'proposalDeadline', 'activePeriodStart', 'activePeriodEnd', 'challengeWindow',
    'nonce', 'chainId', 'protocolVersion',
  ], 'partnership terms');
  if (!validUint(value.bondId) || !validAddress(value.partyA) || !validAddress(value.partyB) ||
      !validUint(value.partyAStake) || !validUint(value.partyBStake) ||
      !validAnyAddress(value.stakeAsset) || !validBytes32(value.partnershipType) ||
      value.objectiveRuleClass !== 0 || value.objectiveRuleSchemaVersion !== 0 ||
      !validUint(value.proposalDeadline, UINT64_MAX) ||
      !validUint(value.activePeriodStart, UINT64_MAX) ||
      !validUint(value.activePeriodEnd, UINT64_MAX) ||
      !validUint(value.challengeWindow, UINT64_MAX) ||
      !validUint(value.nonce, UINT96_MAX) || value.chainId !== chainId ||
      value.protocolVersion !== '3.0.0') {
    throw new TypeError('partnership terms contain invalid values');
  }
}

function assertEnforcement(value: unknown): void {
  assertShape(value, [
    'canonicalSource', 'canonicalSourceCodehash', 'canonicalSourceId', 'obligationId',
    'requiredBy', 'responseWindow', 'challengeWindow', 'decisionWindow', 'maximumSlash',
    'evidenceSchemaHash', 'privacyExclusionsHash', 'attesterSetHash',
    'adjudicatorSetHash', 'attesterThreshold', 'adjudicatorThreshold',
  ], 'enforcement');
  if (!validAddress(value.canonicalSource) || !validBytes32(value.canonicalSourceCodehash) ||
      !validBytes32(value.canonicalSourceId) || !validBytes32(value.obligationId) ||
      !validUint(value.requiredBy, UINT64_MAX) || !validUint(value.responseWindow, UINT64_MAX) ||
      !validUint(value.challengeWindow, UINT64_MAX) || !validUint(value.decisionWindow, UINT64_MAX) ||
      !validUint(value.maximumSlash) || !validBytes32(value.evidenceSchemaHash) ||
      !validBytes32(value.privacyExclusionsHash) || !validBytes32(value.attesterSetHash) ||
      !validBytes32(value.adjudicatorSetHash) ||
      !sameHex(value.attesterSetHash, ZERO_BYTES32) ||
      !sameHex(value.adjudicatorSetHash, ZERO_BYTES32) ||
      value.attesterThreshold !== 0 ||
      value.adjudicatorThreshold !== 0) {
    throw new TypeError('enforcement contains invalid values');
  }
}

function assertEnforcementState(value: unknown): void {
  assertShape(value, [
    'evidenceHash', 'challengeHash', 'requestedAt', 'responseDeadline', 'respondedAt',
    'challengeDeadline', 'challengedAt', 'decisionDeadline', 'resolved',
  ], 'enforcement state');
  if (!validBytes32(value.evidenceHash) || !validBytes32(value.challengeHash) ||
      !['requestedAt', 'responseDeadline', 'respondedAt', 'challengeDeadline',
        'challengedAt', 'decisionDeadline'].every((field) => validUint(value[field], UINT64_MAX)) ||
      typeof value.resolved !== 'boolean') {
    throw new TypeError('enforcement state contains invalid values');
  }
}

function assertReadModel(
  route: V3ReadRoute,
  value: unknown,
  chainId: number,
  deployments: Partial<Record<V3DeploymentName, Address>>,
): void {
  if (!record(value)) throw new TypeError('read result must be a plain record');
  if (route === 'bonds') {
    const accountability = value.kind === 'accountability';
    const partnership = value.kind === 'partnership';
    const expected = accountability ? deployments.accountabilityBond :
      partnership ? deployments.partnershipBond : undefined;
    assertShape(value, accountability ? [
      'kind', 'address', 'terms', 'termsHash', 'state', 'fundedAt', 'settlementRequestedAt',
      'fundedAmount', 'upheldBreachRecorded', 'slashedAmount', 'settlementSnapshot',
      'challengeDisputeId', 'challengeChallenger', 'challengeSettlementSnapshot',
      'challengeSettlementRequestedAt', 'challengeFiledAt', 'enforcement', 'enforcementState',
    ] : [
      'kind', 'address', 'terms', 'termsHash', 'state', 'fundedAt', 'settlementRequestedAt',
      'partyAFunded', 'partyBFunded', 'settlementHumanBps', 'settlementAiBps',
      'settlementPartnershipBps', 'settlementSnapshot', 'challengeDisputeId',
      'challengeChallenger', 'challengeSettlementSnapshot',
      'challengeSettlementRequestedAt', 'challengeFiledAt',
    ], 'bond');
    if (value.kind !== 'accountability' && value.kind !== 'partnership') {
      throw new TypeError('read result has invalid bond kind');
    }
    if (!addressEquals(value.address, expected) || !validBytes32(value.termsHash) ||
        !validEnum(value.state, 12) || !validUint(value.fundedAt, UINT64_MAX) ||
        !validUint(value.settlementRequestedAt, UINT64_MAX) ||
        !validBytes32(value.settlementSnapshot) || !validUint(value.challengeDisputeId) ||
        !validAnyAddress(value.challengeChallenger) ||
        !validBytes32(value.challengeSettlementSnapshot) ||
        !validUint(value.challengeSettlementRequestedAt, UINT64_MAX) ||
        !validUint(value.challengeFiledAt, UINT64_MAX)) {
      throw new TypeError('read result has invalid bond identity or state');
    }
    if (accountability) {
      assertAccountabilityTerms(value.terms, chainId);
      assertEnforcement(value.enforcement);
      assertEnforcementState(value.enforcementState);
      if (!validUint(value.fundedAmount) || typeof value.upheldBreachRecorded !== 'boolean' ||
          !validUint(value.slashedAmount)) {
        throw new TypeError('read result has invalid accountability funding');
      }
    } else {
      assertPartnershipTerms(value.terms, chainId);
      if (!validUint(value.partyAFunded) || !validUint(value.partyBFunded) ||
          !validEnum(value.settlementHumanBps, 10_000) ||
          !validEnum(value.settlementAiBps, 10_000) ||
          !validEnum(value.settlementPartnershipBps, 10_000)) {
        throw new TypeError('read result has invalid partnership funding');
      }
    }
  } else if (route === 'governance') {
    assertShape(value, [
      'governance', 'signers', 'threshold', 'proposalCount', 'timelockSeconds',
      'minimumTimelockSeconds', 'proposalLifetime', 'governanceConfigVersion',
      'canonicalPolicySha256', 'governancePolicyCommitment',
      'guardianRotationDelay', 'pendingGuardian', 'pendingGuardianAcceptAfter',
      'guardianPauseTargetCount', 'policyVersion', 'policyCommitment',
      'proposalPolicyCommitment', 'callPolicy', 'guardian',
    ], 'governance');
    if (!Array.isArray(value.signers) || !value.signers.every(validAddress) ||
        !addressEquals(value.governance, deployments.governance) || !validAddress(value.guardian) ||
        typeof value.threshold !== 'number' || !Number.isSafeInteger(value.threshold) ||
        value.threshold < 1 || value.threshold > value.signers.length ||
        !validUint(value.proposalCount) || !validUint(value.timelockSeconds) ||
        !validUint(value.minimumTimelockSeconds) || !validUint(value.proposalLifetime) ||
        !validUint(value.governanceConfigVersion) ||
        !validBytes32(value.canonicalPolicySha256) ||
        !validBytes32(value.governancePolicyCommitment) ||
        !sameHex(value.canonicalPolicySha256, value.governancePolicyCommitment) ||
        !validUint(value.guardianRotationDelay) ||
        !validAnyAddress(value.pendingGuardian) ||
        !validUint(value.pendingGuardianAcceptAfter, UINT64_MAX) ||
        !validUint(value.guardianPauseTargetCount) || !validUint(value.policyVersion) ||
        !validBytes32(value.policyCommitment) ||
        !validNullable(value.proposalPolicyCommitment, validBytes32) ||
        !validNullable(value.callPolicy, (candidate) => {
          if (!record(candidate) ||
              !exactKeys(candidate, ['target', 'selector', 'allowed', 'maxValue'])) return false;
          return validAddress(candidate.target) &&
            typeof candidate.selector === 'string' && /^0x[0-9a-fA-F]{8}$/.test(candidate.selector) &&
            typeof candidate.allowed === 'boolean' && validUint(candidate.maxValue, UINT96_MAX);
        })) {
      throw new TypeError('read result has invalid governance state');
    }
  } else if (route === 'mandates') {
    assertShape(value, [
      'contract', 'mandateHash', 'registered', 'revoked', 'registeredAt', 'notBefore',
      'notAfter', 'nonce', 'spentSoFar', 'chainId', 'identityRegistryChainId',
      'principalAgentId', 'operatorAgentId', 'principal', 'operator', 'beneficiary',
      'identityRegistry', 'identityRegistryCodehash', 'protocolVersionHash',
    ], 'mandate');
    if (!addressEquals(value.contract, deployments.mandateRegistry) ||
        !validBytes32(value.mandateHash) || !validUint(value.chainId) ||
        value.chainId !== BigInt(chainId) || typeof value.registered !== 'boolean' ||
        typeof value.revoked !== 'boolean' || !validAddress(value.principal) ||
        !validAddress(value.operator) || !validAddress(value.beneficiary) ||
        !validAddress(value.identityRegistry) || !validBytes32(value.identityRegistryCodehash) ||
        !validBytes32(value.protocolVersionHash) ||
        !['registeredAt', 'notBefore', 'notAfter'].every((field) => validUint(value[field], UINT64_MAX)) ||
        !validUint(value.nonce, UINT96_MAX) || !validUint(value.spentSoFar) ||
        !validUint(value.identityRegistryChainId) || !validUint(value.principalAgentId) ||
        !validUint(value.operatorAgentId)) {
      throw new TypeError('read result has invalid mandate state');
    }
  } else if (route === 'pullCredits') {
    assertShape(value, ['contract', 'asset', 'holder', 'amount', 'liability'], 'pull credit');
    const eligible = ['accountabilityBond', 'partnershipBond']
      .map((name) => deployments[name as V3DeploymentName]).filter(validAddress);
    if (!oneOfAddresses(value.contract, eligible) || !validAnyAddress(value.asset) ||
        !validAddress(value.holder) || !validUint(value.amount) || !validUint(value.liability)) {
      throw new TypeError('read result has invalid credit values');
    }
  } else if (route === 'solvency') {
    assertShape(value, [
      'contract', 'asset', 'assets', 'creditLiability', 'lockedPrincipalLiability',
      'totalLiability', 'solvent', 'observedAt',
    ], 'solvency');
    const eligible = ['accountabilityBond', 'partnershipBond']
      .map((name) => deployments[name as V3DeploymentName]).filter(validAddress);
    if (!oneOfAddresses(value.contract, eligible) || !validAnyAddress(value.asset)) {
      throw new TypeError('read result has invalid solvency deployment');
    }
    for (const field of ['assets', 'creditLiability', 'lockedPrincipalLiability', 'totalLiability', 'observedAt']) {
      if (!validUint(value[field])) throw new TypeError(`read result has invalid ${field}`);
    }
    if (typeof value.solvent !== 'boolean') throw new TypeError('read result has invalid solvency flag');
  } else if (route === 'capabilities') {
    assertShape(value, [
      'subject', 'target', 'capability', 'allowed', 'mandateId', 'validUntil', 'evidenceHash',
    ], 'capability');
    if (!validAddress(value.subject) || !validAddress(value.target) ||
        !validBytes32(value.capability) || typeof value.allowed !== 'boolean' ||
        !validNullable(value.mandateId, validBytes32) ||
        !validNullable(value.validUntil, (candidate) => validUint(candidate, UINT64_MAX)) ||
        !validNullable(value.evidenceHash, validBytes32)) {
      throw new TypeError('read result has invalid capability state');
    }
  }
}

export class V3Client {
  readonly profile: DeepReadonly<V3NetworkProfile>;
  private readonly readTransport?: V3ReadTransport;
  private readonly writeTransport?: V3WriteTransport;
  private readonly deploymentVerifier?: V3DeploymentVerifier;
  private readonly capabilityVerifier?: V3CapabilityVerifier;
  private readonly now: () => bigint;

  constructor(config: V3ClientConfig) {
    if (!record(config) || !record(config.profile) ||
        (config.profile.chainId !== 8453 && config.profile.chainId !== 84532) ||
        (config.profile.chainId === 8453 && config.profile.name !== 'Base') ||
        (config.profile.chainId === 84532 && config.profile.name !== 'Base Sepolia') ||
        !['configured', 'unconfigured', 'degraded'].includes(config.profile.status) ||
        !record(config.profile.deployments) ||
        !exactKeys(config.profile.deployments, DEPLOYMENTS) ||
        !DEPLOYMENTS.every((name) => {
          const deployment = config.profile.deployments[name];
          return record(deployment) &&
            exactKeys(deployment, ['address', 'abiIdentity', 'runtimeCodeHash']) &&
            (deployment.address === null || validAnyAddress(deployment.address)) &&
            (deployment.abiIdentity === null || validBytes32(deployment.abiIdentity)) &&
            (deployment.runtimeCodeHash === null || validBytes32(deployment.runtimeCodeHash));
        })) {
      throw new V3UnavailableError('V3 supports only explicit Base profiles');
    }
    try {
      this.profile = cloneAndDeepFreeze(config.profile);
    } catch (cause) {
      throw new V3UnavailableError('V3 profile is not immutable plain data', {}, cause);
    }
    this.readTransport = config.readTransport;
    this.writeTransport = config.writeTransport;
    this.deploymentVerifier = config.deploymentVerifier;
    this.capabilityVerifier = config.capabilityVerifier;
    this.now = config.now ?? (() => BigInt(Math.floor(Date.now() / 1000)));
    if (this.deploymentVerifier &&
        (Object.is(this.writeTransport, this.deploymentVerifier) ||
         Object.is(this.readTransport, this.deploymentVerifier))) {
      throw new V3UnavailableError('Transport and deployment verifier must be independent');
    }
  }

  private async read<T>(
    route: V3ReadRoute,
    query: Readonly<Record<string, unknown>>,
  ): Promise<DeepReadonly<T>> {
    const disposition = V3_ROUTE_METADATA[route];
    if (!disposition || disposition.disposition === 'unavailable' ||
        disposition.disposition === 'retired') {
      throw new V3UnavailableError(`V3 route ${route} is unavailable`, { route });
    }
    if (disposition.disposition === 'degraded') {
      throw new V3DegradedError(`V3 route ${route} is degraded`, { route });
    }
    if (this.profile.status !== 'configured') {
      throw new V3UnavailableError('V3 profile is not configured', { route });
    }
    const configuredNames = routeDeployments[route];
    if (configuredNames.length === 0 || !this.readTransport || !this.deploymentVerifier) {
      throw new V3UnavailableError('V3 route has no canonical configured deployment', { route });
    }
    try {
      const verifiedDeployments: Partial<Record<V3DeploymentName, Address>> = {};
      for (const name of configuredNames) {
        const canonicalIdentity = V3_ABI_IDENTITIES[name];
        verifiedDeployments[name] = await verifyAuthorizedDeployment(
          this.profile as V3NetworkProfile,
          V3_NETWORK_PROFILES[this.profile.chainId] as V3NetworkProfile,
          name,
          canonicalIdentity,
          this.deploymentVerifier,
        );
      }
      const result = await this.readTransport.read(route, cloneAndDeepFreeze(query));
      assertReadModel(route, result, this.profile.chainId, verifiedDeployments);
      const immutableResult = cloneAndDeepFreeze(result);
      assertReadModel(route, immutableResult, this.profile.chainId, verifiedDeployments);
      return immutableResult as DeepReadonly<T>;
    } catch (cause) {
      if (cause instanceof V3UnavailableError || cause instanceof V3DegradedError) throw cause;
      throw new V3UnavailableError('V3 read transport returned invalid or unavailable data', { route }, cause);
    }
  }

  readBond(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<BondReadModel>> {
    return this.read('bonds', query);
  }
  readPullCredit(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<PullCreditReadModel>> {
    return this.read('pullCredits', query);
  }
  readSolvency(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<SolvencyReadModel>> {
    return this.read('solvency', query);
  }
  readMandate(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<MandateReadModel>> {
    return this.read('mandates', query);
  }
  readCapability(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<CapabilityReadModel>> {
    return this.read('capabilities', query);
  }
  readGovernance(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<GovernanceReadModel>> {
    return this.read('governance', query);
  }
  readX402Status(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<X402StatusReadModel>> {
    return this.read('x402.status', query);
  }
  readX402Discovery(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<X402DiscoveryReadModel>> {
    return this.read('x402.discovery', query);
  }
  readX402Challenge(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<X402ChallengeReadModel>> {
    return this.read('x402.challenge', query);
  }
  readX402Receipt(query: Readonly<Record<string, unknown>>): Promise<DeepReadonly<X402ReceiptReadModel>> {
    return this.read('x402.receipt', query);
  }

  async write(request: V3WriteRequest): Promise<Bytes32> {
    if (this.profile.status !== 'configured') {
      throw new V3WritesBlockedError('V3 profile is not configured');
    }
    if (!record(request) || typeof request.deployment !== 'string' ||
        !DEPLOYMENTS.includes(request.deployment as V3DeploymentName)) {
      throw new V3WritesBlockedError('Unknown V3 deployment');
    }
    const deploymentName = request.deployment as V3DeploymentName;
    const deployment = this.profile.deployments[deploymentName];
    const canonicalIdentity = V3_ABI_IDENTITIES[deploymentName];
    if (!deployment || !validAddress(deployment.address)) {
      throw new V3WritesBlockedError('V3 deployment address is not configured', {
        deployment: deploymentName,
      });
    }
    if (!validBytes32(deployment.abiIdentity) ||
        !sameHex(deployment.abiIdentity, canonicalIdentity)) {
      throw new V3WritesBlockedError('Profile does not contain the canonical ABI identity', {
        deployment: deploymentName,
      });
    }
    if (!validBytes32(request.capability) || !validAddress(request.subject) ||
        typeof request.data !== 'string' || !/^0x[0-9a-fA-F]{8}(?:[0-9a-fA-F]{2})*$/.test(request.data)) {
      throw new V3WritesBlockedError('Write request has malformed capability, subject, or calldata');
    }
    const method = V3_WRITE_METHODS[deploymentName].find(
      (candidate) => sameHex(candidate.selector, request.data.slice(0, 10)),
    );
    if (!method || !sameHex(method.capability, request.capability)) {
      throw new V3WritesBlockedError('Method is unsupported or does not match the capability', {
        deployment: deploymentName,
        selector: request.data.slice(0, 10),
      });
    }
    try {
      const iface = new ethers.Interface([`function ${method.signature}`]);
      const decoded = iface.decodeFunctionData(method.signature, request.data);
      if (!sameHex(iface.encodeFunctionData(method.signature, decoded), request.data)) {
        throw new TypeError('non-canonical calldata');
      }
    } catch (cause) {
      throw new V3WritesBlockedError('Calldata does not match the selected canonical method', {
        deployment: deploymentName,
        selector: request.data.slice(0, 10),
      }, cause);
    }
    const value = request.value ?? 0n;
    if (!validUint(value)) {
      throw new V3WritesBlockedError('Transaction value must be a uint256');
    }
    if (value > 0n && !method.payable) {
      throw new V3WritesBlockedError('Transaction value is not allowed for this method');
    }
    if (!record(request.evidence) || !exactKeys(request.evidence, [
      'schema', 'chainId', 'contract', 'capability', 'subject',
      'issuedAt', 'expiresAt', 'evidenceHash',
    ])) {
      throw new V3WritesBlockedError('Explicit capability evidence is required');
    }
    const evidence = request.evidence as unknown as Record<string, unknown>;
    let now: bigint;
    try {
      now = this.now();
    } catch (cause) {
      throw new V3WritesBlockedError('Capability evidence clock failed', {}, cause);
    }
    if (!validUint(now) ||
        evidence.schema !== 'vaultfire.v3.capability-evidence.v1' ||
        evidence.chainId !== this.profile.chainId ||
        !validAddress(evidence.contract) || !sameHex(evidence.contract, deployment.address) ||
        !validBytes32(evidence.capability) || !sameHex(evidence.capability, request.capability) ||
        !validAddress(evidence.subject) || !sameHex(evidence.subject, request.subject) ||
        !validUint(evidence.issuedAt, (1n << 64n) - 1n) ||
        !validUint(evidence.expiresAt, (1n << 64n) - 1n) ||
        evidence.issuedAt > now || evidence.expiresAt <= now ||
        !validBytes32(evidence.evidenceHash)) {
      throw new V3WritesBlockedError('Explicit capability evidence is malformed, mismatched, or expired', {
        deployment: deploymentName,
      });
    }
    if (!this.writeTransport || !this.deploymentVerifier || !this.capabilityVerifier) {
      throw new V3WritesBlockedError('Write transport and independent verifiers are required');
    }

    let verifiedAddress: Address;
    try {
      verifiedAddress = await verifyAuthorizedDeployment(
        this.profile as V3NetworkProfile,
        V3_NETWORK_PROFILES[this.profile.chainId] as V3NetworkProfile,
        deploymentName,
        canonicalIdentity,
        this.deploymentVerifier,
      );
    } catch (cause) {
      throw new V3WritesBlockedError('Canonical deployed runtime verification failed', {
        deployment: deploymentName,
      }, cause);
    }
    let capabilityVerified: boolean;
    try {
      capabilityVerified = await this.capabilityVerifier.verify(
        cloneAndDeepFreeze(request.evidence) as CapabilityEvidence,
      );
    } catch (cause) {
      throw new V3WritesBlockedError('Capability evidence verifier failed', {}, cause);
    }
    if (capabilityVerified !== true) {
      throw new V3WritesBlockedError('Capability evidence is untrusted', {
        deployment: deploymentName,
      });
    }
    try {
      const transactionHash = await this.writeTransport.sendTransaction(cloneAndDeepFreeze({
        to: verifiedAddress,
        data: request.data,
        value,
      }));
      if (!validBytes32(transactionHash)) {
        throw new TypeError('sender returned a malformed transaction hash');
      }
      return transactionHash;
    } catch (cause) {
      throw new V3DegradedError('V3 transaction submission failed after verification', {
        deployment: deploymentName,
        attempted: true,
      }, cause);
    }
  }
}

export function createV3Client(config: V3ClientConfig): V3Client {
  return new V3Client(config);
}
