import {
	WebSocketProvider,
	Wallet,
	Contract,
	Interface,
	Fragment,
	JsonFragment,
	TransactionReceipt,
	TransactionRequest,
	TransactionResponse,
	TransactionDescription,
	BigNumberish,
	BytesLike,
	BaseContract,
	ContractInterface,
	ContractEventName,
	EventEmitterable,
	Log,
	Result,
	EventFragment,
	SigningKey,
	JsonRpcApiProvider,
} from "ethers";
import { EventEmitter } from "node:events";
import { ChildProcess } from "node:child_process";

type ImpersonatedWallet = {
	readonly address: string;
	readonly provider: JsonRpcApiProvider;
	getNonce(): Promise<number>;
	setNonce(nonce: BigNumberish): Promise<void>;
	sendTransaction(tx: TransactionRequest): Promise<TransactionResponse>;
};
type DevWallet = Omit<Wallet, "connect">;
type PatchedBaseContract = Omit<
	BaseContract,
	"target" | "connect" | "attach"
> & {
	target: string;
	connect(...args: Parameters<Contract["connect"]>): Contract;
	attach(...args: Parameters<Contract["attach"]>): Contract;
	waitForDeployment(): Promise<Contract>;
};
type FoundryContract = PatchedBaseContract &
	Omit<ContractInterface, keyof PatchedBaseContract> &
	EventEmitterable<ContractEventName> & {
		readonly __info: {
			readonly contract: string;
		};
	};
type DeployedContract = FoundryContract & {
	readonly __create2?: { salt: string; deployer: string };
	readonly __receipt: TransactionReceipt;
	readonly __info: {
		readonly origin: string;
		readonly bytecode: Uint8Array;
		readonly libs: { [cid: string]: string };
		readonly from: DevWallet | ImpersonatedWallet;
	};
};

type EventLike = string | EventFragment;
type InterfaceLike =
	| Interface
	| Contract
	| (Fragment | JsonFragment | string)[];

type ExternalLink = {
	file: string;
	contract: string;
	offsets: number[];
};

type PathLike = string | URL;
type WalletLike = string | DevWallet;
type CompiledArtifact = {
	readonly type: string;
	readonly contract: string;
	readonly origin: string;
	readonly abi: Interface;
	readonly bytecode: string;
	readonly links: ExternalLink[];
};
type CompiledFromSourceArtifact = CompiledArtifact & {
	readonly cid: string;
	readonly root: string;
	readonly compiler: string;
};
type FileArtifact = CompiledFromSourceArtifact & {
	readonly file: string;
};
type CodeArtifact = CompiledFromSourceArtifact & {
	readonly sol: string;
};
type Artifact = CompiledArtifact | FileArtifact | CodeArtifact;
type CompileOptions = {
	optimize?: boolean | number;
	solcVersion?: string;
	evmVersion?: string;
	viaIR?: boolean;
	autoHeader?: boolean;
	contract?: string;
};

export function compile(
	sol: string | string[],
	options?: { foundry?: Foundry } & CompileOptions
): Promise<CodeArtifact>;

type ArtifactLike =
	| { import: string; contract?: string }
	| { bytecode: string; abi?: InterfaceLike; contract?: string }
	| ({ sol: string } & CompileOptions)
	| { file: string; contract?: string };

type ToConsoleLog = boolean | PathLike | ((line: string) => any);
type WalletOptions = {
	ether?: BigNumberish;
};
type BuildInfo = {
	date: Date;
};

type ConfirmOptions = {
	silent?: boolean;
	confirms?: number;
};

type Backend = "ethereum" | "optimism";

type FoundryBaseOptions = {
	root?: PathLike; // default: ancestor w/foundry.toml
	profile?: string; // default: "default"
	forge?: string; // default: "forge" via PATH
};

type BuildEvent = {
	started: Date;
	root: string;
	cmd: string[];
	force: boolean;
	profile: string;
	mode: "project" | "shadow" | "compile";
};

type FoundryEventMap = {
	building: [event: BuildEvent];
	built: [event: BuildEvent & { sources: string[] }];
	shutdown: [];
	tx: [
		tx: TransactionResponse,
		receipt: TransactionReceipt,
		desc?: TransactionDescription
	];
	console: [line: string];
	deploy: [contract: DeployedContract];
};

export class FoundryBase extends EventEmitter {
	// <FoundryEventMap> {
	static profile(): string;
	static root(cwd?: PathLike): Promise<string>;
	static load(options?: FoundryBaseOptions): Promise<FoundryBase>;
	readonly root: string;
	readonly profile: string;
	readonly config: {
		src: string;
		test: string;
		out: string;
		libs: string[];
		remappings: string[];
	};
	readonly forge: string;
	readonly built?: BuildInfo;
	compiler(solcVersion: string): Promise<string>;
	version(): Promise<string>;

	build(force?: boolean): Promise<BuildInfo>;
	compile(
		sol: string | string[],
		options?: CompileOptions
	): Promise<CodeArtifact>;
	find(options: { file: string; contract?: string }): Promise<string>;
	resolveArtifact(artifact: ArtifactLike | string): Promise<Artifact>;

	on<E extends keyof FoundryEventMap>(
		name: E,
		fn: (...args: FoundryEventMap[E]) => any
	): this;
	once<E extends keyof FoundryEventMap>(
		name: E,
		fn: (...args: FoundryEventMap[E]) => any
	): this;
}

type SolidityStandardJSONInput = {
	language: string;
	sources: { [cid: string]: { content: string } };
	optimizer: {
		enabled: boolean;
		runs?: number;
	};
	settings: {
		remappings: string[];
		metadata: Record<string, any>;
		evmVersion: string;
		viaIR: boolean;
		libraries: { [cid: string]: { [contract: string]: string } };
	};
};

type VerifyEtherscanOptions = {
	apiKey?: string; // foundry.toml => ETHERSCAN_API_KEY
	pollMs?: number; // default: 5sec
	retry?: number; // default: 10
};

export type Deployable = {
	gas: bigint;
	gasPrice: bigint;
	maxFeePerGas: bigint;
	maxPriorityFeePerGas: bigint;
	wei: bigint;
	eth: string;
	root: string;
	cid: string;
	linked: (ExternalLink & { cid: string; address: string })[];
	compiler: string;
	decodedArgs: any[];
	encodedArgs: string;
	address?: string;
	deployArgs(injectPrivateKey?: boolean): string[];
	deploy(options?: {
		confirms?: number;
	}): Promise<{ contract: Contract; receipt: TransactionReceipt }>;
	json(): Promise<Readonly<SolidityStandardJSONInput>>;
	verifyEtherscan(
		options?: { address?: string } & VerifyEtherscanOptions
	): Promise<void>;
};

type FoundryDeployerOptions = FoundryBaseOptions & {
	infoLog?: ToConsoleLog; // default: true
};

export class FoundryDeployer extends FoundryBase {
	static etherscanChains(): Promise<Map<bigint, string>>;

	static load(
		options?: {
			provider?:
				| JsonRpcApiProvider
				| "mainnet"
				| "sepolia"
				| "holesky"
				| "hoodi"
				| "arb1"
				| "base"
				| "op"
				| "linea"
				| "polygon";
			privateKey?: string | SigningKey | undefined;
		} & FoundryDeployerOptions
	): Promise<FoundryDeployer>;

	readonly rpc: string;
	readonly chain: bigint;
	readonly provider: JsonRpcApiProvider;

	set etherscanApiKey(key: string | undefined);
	get etherscanApiKey(): string;

	set privateKey(key: SigningKey | string | undefined);
	get privateKey(): SigningKey | undefined;
	get address(): string | undefined;
	requireWallet(): Wallet;

	prepare(
		options:
			| string
			| ({
					args?: any[];
					libs?: { [cid: string]: string };
					confirms?: number;
			  } & ArtifactLike)
	): Promise<Readonly<Deployable>>;

	verifyEtherscan(
		options: {
			address: string; // 0x...
			json: SolidityStandardJSONInput;
			cid?: string; // "src/File.sol:Contract"
			encodedArg?: string | Uint8Array;
			compiler?: string; // can be semver
		} & VerifyEtherscanOptions
	): Promise<void>;
}

export class Foundry extends FoundryBase {
	static of(x: DevWallet | FoundryContract): Foundry;
	static launch(
		options?: {
			port?: number;
			chain?: number;
			anvil?: string;
			gasLimit?: number;
			blockSec?: number;
			accounts?: string[];
			autoClose?: boolean; // default: true
			infoLog?: ToConsoleLog; // default: true = console.log()
			procLog?: ToConsoleLog; // default: off
			fork?: PathLike;
			infiniteCallGas?: boolean; // default: false
			genesisTimestamp?: number; // default: now
			backend?: Backend; // default: foundry.toml => 'ethereum'
			hardfork?: string; // default: foundry.toml => latest
		} & FoundryBaseOptions
	): Promise<Foundry>;

	ensRegistry: string;

	readonly anvil: string;
	readonly proc: ChildProcess;
	readonly provider: WebSocketProvider;
	readonly wallets: Record<string, DevWallet>;
	readonly accounts: Map<string, DevWallet | FoundryContract>;
	readonly endpoint: string;
	readonly chain: number;
	readonly port: number;
	readonly automine: boolean;
	readonly backend: Backend;
	readonly hardfork: string;
	readonly started: Date;
	readonly fork: string | undefined;

	// note: these are silent fail on forks
	nextBlock(options?: { blocks?: number; sec?: number }): Promise<void>;

	setStorageValue(
		address: string | Contract,
		slot: BigNumberish,
		value: BigNumberish | Uint8Array | undefined
	): Promise<void>;
	getStorageBytesLength(
		address: string | Contract,
		slot: BigNumberish
	): Promise<bigint>;
	getStorageBytes(
		address: string | Contract,
		slot: BigNumberish,
		maxBytes?: number
	): Promise<Uint8Array>;
	setStorageBytes(
		address: string | Contract,
		slot: BigNumberish,
		value: BytesLike | undefined,
		zeroBytes?: boolean | number
	): Promise<void>;

	overrideENS(
		options: {
			owner?: string | FoundryContract | null;
			resolver?: string | FoundryContract | null;
			registry?: string | FoundryContract;
		} & ({ name: string } | { node: string })
	): Promise<void>;

	// require a wallet
	requireWallet(...wallets: (WalletLike | undefined)[]): DevWallet;
	randomWallet(
		options?: { prefix?: string } & WalletOptions
	): Promise<DevWallet>;
	ensureWallet(
		wallet: WalletLike,
		options?: WalletOptions
	): Promise<DevWallet>;
	impersonateWallet(address: string): Promise<ImpersonatedWallet>;

	attach(
		options: {
			to: string | FoundryContract;
			from?: WalletLike | ImpersonatedWallet;
			abis?: InterfaceLike[];
			parseAllErrors?: boolean;
		} & ArtifactLike
	): Promise<FoundryContract>;

	// compile and deploy a contract, returns Contract with ABI
	deploy(
		options:
			| string
			| ({
					from?: WalletLike | ImpersonatedWallet;
					args?: any[];
					libs?: { [cid: string]: string | FoundryContract };
					abis?: InterfaceLike[];
					parseAllErrors?: boolean;
					salt?: BigNumberish;
					create2Deployer?: string;
			  } & ConfirmOptions &
					ArtifactLike)
	): Promise<DeployedContract>;

	// send a transaction promise and get a pretty print console log
	confirm(
		call: TransactionResponse | Promise<TransactionResponse>,
		options?: ConfirmOptions & Record<string, any>
	): Promise<TransactionReceipt>;

	addABI(abi: Interface): void;
	parseAllErrors(iface: Interface): Interface;
	parseArtifacts(): Promise<void>;

	findEvent(event: EventLike): { abi: Interface; frag: EventFragment };
	getEventResults(
		logs: Log[] | TransactionReceipt | DeployedContract,
		event: EventLike
	): Result[];

	// kill anvil (this is a bound function)
	shutdown: () => Promise<void>;
}

export function mergeABI(...abis: InterfaceLike[]): Interface;

export class Node extends Map {
	static root(tag?: string): Node;
	static create(name: string | Node): Node;

	readonly label: string;
	readonly parent: Node;

	readonly namehash: string;
	readonly labelhash: string;
	get dns(): Uint8Array;

	get name(): string;
	get isETH2LD(): boolean;

	get depth(): number;
	get nodeCount(): number;
	get root(): Node;
	path(includeRoot?: boolean): Node[];

	find(name: string): Node | undefined;
	create(name: string): Node;
	child(label: string): Node;
	unique(prefix?: string): Node;

	scan(fn: (node: Node, level: number) => void, level?: number): void;
	flat(): Node[];
	print(): void;
}

type RecordQuery = {
	type: "addr" | "text" | "contenthash" | "pubkey" | "name";
	arg?: any;
};
type RecordResult = { rec: RecordQuery; res?: any; err?: Error };
type TORPrefix = "on" | "off" | undefined;
type RecordOptions = { multi?: boolean; ccip?: boolean; tor?: TORPrefix };

export class Resolver {
	static readonly ABI: Interface;
	static get(ens: Contract, node: Node): Promise<Resolver | undefined>;

	constructor(node: Node, contract: Contract);

	readonly node: Node;
	readonly contract: Contract;

	readonly base: Node;
	readonly drop: number;

	wild: boolean;
	tor: boolean;

	get address(): string;

	text(key: string, options?: RecordOptions): Promise<string>;
	addr(type?: number, options?: RecordOptions): Promise<string>;
	contenthash(options?: RecordOptions): Promise<string>;
	record(rec: RecordQuery, options?: RecordOptions): Promise<any>;
	records(
		rec: RecordQuery[],
		options?: RecordOptions
	): Promise<[records: RecordResult[], multicalled?: boolean]>;
	profile(
		rec?: RecordQuery[],
		options?: RecordOptions
	): Promise<{ [key: string]: any }>;
}

export function error_with(
	message: string,
	options: Object,
	cause?: any
): Error;
export function to_address(x: any): string;
export function is_address(x: any): boolean;
