/**
 * @module EthereumContractService
 *
 * Provides a unified service for interacting with Ethereum smart contracts using ethers.js.
 */
import AccountingArtifact from '../../assets/ethereum/ABI/liqEth/accounting.sol/Accounting.json';
import DepositManagerArtifact from '../../assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.json';
import LiqEthArtifact from '../../assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.json';
import StakingModuleArtifact from '../../assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.json';
import WithdrawalQueueArtifact from '../../assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.json';
import WithdrawalVaultArtifact from '../../assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.json';

// Outpost Artifacts
import DepositorArtifact from '../../assets/ethereum/ABI/outpost/Depositor.sol/Depositor.json';
import ReceiptNFTArtifact from '../../assets/ethereum/ABI/outpost/ReceiptNFT.sol/ReceiptNFT.json';
import OutpostManagerArtifact from '../../assets/ethereum/ABI/outpost/OutpostManager.sol/OutpostManager.json';
import BARArtifact from '../../assets/ethereum/ABI/outpost/BAR.sol/BAR.json';
import OPPArtifact from '../../assets/ethereum/ABI/outpost/OPP.sol/OPP.json';
import OPPCommonArtifact from '../../assets/ethereum/ABI/outpost/OPPCommon.sol/OPPCommon.json';
import OPPInboundArtifact from '../../assets/ethereum/ABI/outpost/OPPInbound.sol/OPPInbound.json';
import PretokenArtifact from '../../assets/ethereum/ABI/outpost/Pretoken.sol/Pretoken.json';
import AggregatorArtifact from '../../assets/ethereum/ABI/outpost/Aggregator.sol/Aggregator.json';
import EthUsdPriceConsumerArtifact from '../../assets/ethereum/ABI/outpost/EthUsdPriceConsumer.sol/EthUsdPriceConsumer.json';
import PoolArtifact from '../../assets/ethereum/ABI/outpost/Pool.sol/Pool.json';

import ERC20Artifact from '../../assets/ethereum/ABI/token/ERC20Token.sol/ERC20Token.json';
import ERC721Artifact from '../../assets/ethereum/ABI/token/ERC721Token.sol/ERC721Token.json';
import ERC1155Artifact from '../../assets/ethereum/ABI/token/ERC1155Token.sol/ERC1155Token.json';

import { AddressBook, ContractName } from './types';
import { Contract, ethers, providers, Signer } from 'ethers';
import { JsonFragment } from '@ethersproject/abi';
import { ErrorDescription } from '@ethersproject/abi/lib/interface';

export const ERC20Abi = ERC20Artifact.abi;
export const ERC721Abi = ERC721Artifact.abi;
export const ERC1155Abi = ERC1155Artifact.abi;

// -----------------------------------------------------------------------------
// Address book
// -----------------------------------------------------------------------------

// Make sure ContractName in ./types includes all of these keys.
export const ADDRESSES: AddressBook = {
    // LiqETH contracts
    Accounting: "0xd333A03a44D5d602A98c1B7bcd7ED1f042DD0dEd",
    DepositManager: "0x601eaA31e8d33D8725786f1733f4cE6cCEf740D4",
    LiqEth: "0x08252e1Dcbaa86A2887927b02536CD3E67a802c8", // AKA LiqEthToken
    StakingModule: "0x7Aac2fCfc83d7F2f15eA53a10C1677CA377AFcF4",
    WithdrawalQueue: "0x951E413FC81a2CE133078ABE8B88677F5296abE7",
    WithdrawalVault: "0x0D2bf834DD560839e986d42D06DeE268A17c2d13",

    //Outpost contracts
    Depositor: "0x69Aa53Ef02F124dB421AeDda509d6912341299Cc",
    ReceiptNFT: "0x13588fF41E2f47D047874162B1eD15AaF6818f5a",
    OutpostManager: "0x187f4fEDBCAD2f3150120B743b5d568F11dD3452",
    BAR: "0x27d4646abF638A13934c94f8bA9AC1Bda39F5dbb",
    OPP: "0x79CA2DE915C294593edc16b32CBA8B7866159205",
    OPPCommon: "0x8392ea945Cd8C03F5241d4964D20BB5A9c320c71",
    OPPInbound: "0xfDA9A2C7e395a9722d9fa506f5d67aba823298dd",
    Pretoken: "0xcf6A1209A7A391cc576174204386F4e5462323dC",
    EthUsdPriceConsumer: "0x1Ef180FF49313fCB8B5c0470268295d0d24CDE69",
    Aggregator: "0xFCfc3ddd4CBd9Ad3b3af3A374B8bdA1b66eE6FFF",
    Pool: "0x29DEf0fA009e02d108d9505018EAe0168F233e03",
};

export type Contracts<T extends string = ContractName> = Record<T, ContractConfig>;

export type ContractConfig = {
    address: string;
    abi: JsonFragment[];
};

// -----------------------------------------------------------------------------
// Contract config
// -----------------------------------------------------------------------------

export const CONTRACTS: Contracts<ContractName> = {
    Accounting: {
        address: ADDRESSES.Accounting,
        abi: AccountingArtifact.abi as JsonFragment[],
    },
    LiqEth: {
        address: ADDRESSES.LiqEth,
        abi: LiqEthArtifact.abi as JsonFragment[],
    },
    DepositManager: {
        address: ADDRESSES.DepositManager,
        abi: DepositManagerArtifact.abi as JsonFragment[],
    },
    StakingModule: {
        address: ADDRESSES.StakingModule,
        abi: StakingModuleArtifact.abi as JsonFragment[],
    },
    WithdrawalQueue: {
        address: ADDRESSES.WithdrawalQueue,
        abi: WithdrawalQueueArtifact.abi as JsonFragment[],
    },
    WithdrawalVault: {
        address: ADDRESSES.WithdrawalVault,
        abi: WithdrawalVaultArtifact.abi as JsonFragment[],
    },

    // Outpost Contracts
    Depositor: {
        address: ADDRESSES.Depositor,
        abi: DepositorArtifact.abi as JsonFragment[],
    },
    ReceiptNFT: {
        address: ADDRESSES.ReceiptNFT,
        abi: ReceiptNFTArtifact.abi as JsonFragment[],
    },
    OutpostManager: {
        address: ADDRESSES.OutpostManager,
        abi: OutpostManagerArtifact.abi as JsonFragment[],
    },
    BAR: {
        address: ADDRESSES.BAR,
        abi: BARArtifact.abi as JsonFragment[],
    },
    OPP: {
        address: ADDRESSES.OPP,
        abi: OPPArtifact.abi as JsonFragment[],
    },
    OPPCommon: {
        address: ADDRESSES.OPPCommon,
        abi: OPPCommonArtifact.abi as JsonFragment[],
    },
    OPPInbound: {
        address: ADDRESSES.OPPInbound,
        abi: OPPInboundArtifact.abi as JsonFragment[],
    },
    Pretoken: {
        address: ADDRESSES.Pretoken,
        abi: PretokenArtifact.abi as JsonFragment[],
    },
    Aggregator: {
        address: ADDRESSES.Aggregator,
        abi: AggregatorArtifact.abi as JsonFragment[],
    },
    EthUsdPriceConsumer: {
        address: ADDRESSES.EthUsdPriceConsumer,
        abi: EthUsdPriceConsumerArtifact.abi as JsonFragment[],
    },
    Pool: {
        address: ADDRESSES.Pool,
        abi: PoolArtifact.abi as JsonFragment[],
    }
};

export interface ContractOptions {
    /** RPC endpoint or injected EIP-1193 provider */
    provider?: ethers.providers.Provider;
    /** Only required if you need to send txns */
    signer?: Signer;
}

export class EthereumContractService {
    private provider?: providers.Provider;
    private signer?: Signer;

    public readonly contract: { [N in ContractName]: ethers.Contract } = {} as any;
    public readonly iface: { [N in ContractName]: ethers.utils.Interface } = {} as any;

    constructor(opts: ContractOptions) {
        this.provider = opts.provider;
        this.signer = opts.signer;

        (Object.keys(CONTRACTS) as ContractName[]).forEach((name) => {
            const { address, abi } = CONTRACTS[name];
            this.iface[name] = new ethers.utils.Interface(abi);
            this.contract[name] = new ethers.Contract(
                address,
                abi,
                this.signer ?? this.provider
            );
        });
    }

    /** Get raw address for a contract */
    public getAddress(name: ContractName): string {
        return CONTRACTS[name].address;
    }

    /** Utility: get a read-only handle even if signer is present */
    public getReadOnly(name: ContractName): Contract {
        const { address, abi } = CONTRACTS[name];
        return new Contract(address, abi, this.provider);
    }

    /** Utility: get a write-enabled handle (throws if no signer) */
    public getWrite(name: ContractName): Contract {
        if (!this.signer) throw new Error('No signer available for write calls');
        const { address, abi } = CONTRACTS[name];
        return new Contract(address, abi, this.signer);
    }

    /** Expose network info */
    async getNetwork() {
        return this.provider?.getNetwork();
    }

    /** List all known contract names */
    public listContractNames(): ContractName[] {
        return Object.keys(CONTRACTS) as ContractName[];
    }

    /** Get Interface instance for a contract */
    public getInterface(name: ContractName): ethers.utils.Interface {
        const iface = this.iface[name];
        if (!iface) throw new Error(`Interface not initialized for ${name}`);
        return iface;
    }

    /** Get live Contract instance */
    public getContract(name: ContractName): ethers.Contract {
        const ctr = this.contract[name];
        if (!ctr) throw new Error(`Contract not initialized for ${name}`);
        return ctr;
    }

    /** A unified Interface containing all ABIs to parse custom errors or events. */
    public get omniInterface(): ethers.utils.Interface {
        const all: any[] = [];

        const allArtifacts = [
            ERC20Artifact,
            ERC721Artifact,
            ERC1155Artifact,

            // liqETH suite
            AccountingArtifact,
            DepositManagerArtifact,
            LiqEthArtifact,
            // LiqEthBurnArtifact,
            // LiqEthMintArtifact,
            // RewardsERC20Artifact,
            StakingModuleArtifact,
            // ValidatorBalanceArtifact,
            WithdrawalQueueArtifact,
            WithdrawalVaultArtifact,
            // YieldArtifact,
        ];

        allArtifacts.forEach(artifact => {
            if (artifact && artifact.abi) {
                all.push(...artifact.abi);
            }
        });

        const seen = new Set<string>();
        const dedup: any[] = [];
        for (const item of all) {
            try {
                const frag = ethers.utils.Fragment.from(item);
                const key = `${frag.type}:${frag.format(ethers.utils.FormatTypes.full)}`.toLowerCase();
                if (!seen.has(key)) {
                    seen.add(key);
                    dedup.push(item);
                }
            } catch {
                // ignore bad fragments
            }
        }

        return new ethers.utils.Interface(dedup);
    }

    /** Decode a revert or custom error payload */
    public parseError(revertData: string): ErrorDescription {
        try {
            return this.omniInterface.parseError(revertData);
        } catch {
            throw new Error(`Unable to parse error data: ${revertData}`);
        }
    }
}