import { EnsResolver, Provider, Web3Provider } from '@ethersproject/providers'
import { DIDDocument, DIDResolutionResult, DIDResolver, ParsedDID, Service, VerificationMethod } from 'did-resolver'
import { ConfigurationOptions, configureResolverWithNetworks } from './configuration'
import { Errors, identifierMatcher, isDefined } from './helpers'

export function getResolver(config?: ConfigurationOptions): Record<string, DIDResolver> {
  async function resolve(did: string, parsed: ParsedDID): Promise<DIDResolutionResult> {
    const networks = configureResolverWithNetworks(config)
    // check if identifier(parsed.id) contains a network code
    const fullId = parsed.id.match(identifierMatcher)
    if (!fullId) {
      return {
        didResolutionMetadata: {
          error: Errors.invalidDid,
          message: `Not a valid did:ens: ${parsed.id}`,
        },
        didDocumentMetadata: {},
        didDocument: null,
      }
    }
    const ensName = fullId[2]
    const networkCode = typeof fullId[1] === 'string' ? fullId[1].slice(0, -1) : ''

    // get provider for that network or the mainnet provider if none other is given
    const provider: Provider = networks[networkCode]
    if (!provider || typeof provider === 'undefined') {
      return {
        didResolutionMetadata: {
          error: Errors.unknownNetwork,
          message: `This resolver is not configured for the ${networkCode} network required by ${
            parsed.id
          }. Networks: ${JSON.stringify(Object.keys(networks))}`,
        },
        didDocumentMetadata: {},
        didDocument: null,
      }
    }
    const ensResolver: EnsResolver | null = await (provider as Web3Provider).getResolver(ensName)
    if (!ensResolver) {
      return {
        didResolutionMetadata: {
          error: Errors.unknownEnsResolver,
          message: `This network (${networkCode}), required by ${parsed.id}, does not have a known ENS resolver`,
        },
        didDocumentMetadata: {},
        didDocument: null,
      }
    }
    let err: string | null = null
    let address: string | null = null
    try {
      address = await ensResolver.getAddress()
    } catch (error) {
      err = `resolver_error: Cannot resolve ENS name: ${error}`
    }

    const didDocumentMetadata = {}
    let didDocument: DIDDocument | null = null

    if (address) {
      const chainId = (await provider.getNetwork()).chainId
      const blockchainAccountId = `${address}@eip155:${chainId}`
      const postfix = address

      // setup default did doc
      didDocument = {
        id: did,
        service: [
          {
            id: `${did}#Web3PublicProfile-${postfix}`,
            type: 'Web3PublicProfile',
            serviceEndpoint: ensName,
          },
        ],
        verificationMethod: [
          {
            id: `${did}#${postfix}`,
            type: 'EcdsaSecp256k1RecoveryMethod2020',
            controller: did,
            blockchainAccountId,
          },
        ],
        authentication: [`${did}#${postfix}`],
        capabilityDelegation: [`${did}#${postfix}`],
        capabilityInvocation: [`${did}#${postfix}`],
        assertionMethod: [`${did}#${postfix}`],
      }
    }

    const getEnsRecord = async <T>(ensResolver: EnsResolver, name: string): Promise<T | null> => {
      let parsedEntry: T | null = null
      const entry = await ensResolver.getText(name)
      if (entry) {
        try {
          parsedEntry = JSON.parse(unescape(entry))
        } catch (e) {
          return null
        }
      }
      return parsedEntry
    }

    const filterValidVerificationMethods = (
      did: string,
      current: (string | VerificationMethod)[],
      all: VerificationMethod[]
    ): (string | VerificationMethod)[] => {
      const methodLinks = (current.filter((entry) => typeof entry === 'string') as string[])
        .map((entry) => (entry.startsWith('#') ? `${did}${entry}` : entry))
        .filter((entry) => all?.some((b) => b.id === entry))

      const fullMethods = (
        current.filter(
          (entry) =>
            entry != null &&
            typeof entry === 'object' &&
            Object.keys(entry).includes('id') &&
            Object.keys(entry).includes('type') &&
            Object.keys(entry).some((k) => k.startsWith('publicKey'))
        ) as VerificationMethod[]
      ).map((entry: VerificationMethod) => {
        entry.controller = entry.controller || did
        if (entry.id.startsWith('#')) {
          entry.id = `${did}${entry}`
        }
        return entry
      })
      return [...methodLinks, ...fullMethods]
    }

    const services = (await getEnsRecord<Service[]>(ensResolver, 'org.w3c.did.service')) || []
    if (services) {
      if (didDocument) {
        didDocument.service = [...(didDocument.service || []), ...services].filter(isDefined)
      }
    }

    const verificationMethods =
      (await getEnsRecord<VerificationMethod[]>(ensResolver, 'org.w3c.did.verificationMethod')) || []

    if (verificationMethods) {
      verificationMethods.map((method) => {
        if (method.id.startsWith('#')) {
          method.id = `${did}${method.id}`
        }
        method.controller = method.controller || did
        return method
      })
      if (didDocument) {
        didDocument.verificationMethod = [...(didDocument.verificationMethod || []), ...verificationMethods].filter(
          isDefined
        )
      }
    }

    const relationships = [
      'keyAgreement',
      'assertionMethod',
      'authentication',
      'capabilityInvocation',
      'capabilityDelegation',
    ]
    await relationships.reduce(async (memo, relationship) => {
      await memo
      try {
        const verificationMethod =
          (await getEnsRecord<(string | VerificationMethod)[]>(ensResolver, `org.w3c.did.${relationship}`)) || []
        if (verificationMethod) {
          if (didDocument) {
            didDocument[relationship] = [
              ...(didDocument[relationship] || []),
              ...filterValidVerificationMethods(did, verificationMethod, verificationMethods),
            ]
          }
        }
      } catch (e) {
        // nop
      }
    }, Promise.resolve())

    const contentType =
      typeof didDocument?.['@context'] !== 'undefined' ? 'application/did+ld+json' : 'application/did+json'

    if (err) {
      return {
        didDocument,
        didDocumentMetadata,
        didResolutionMetadata: {
          error: Errors.notFound,
          message: err,
        },
      }
    } else {
      return {
        didDocument,
        didDocumentMetadata,
        didResolutionMetadata: { contentType },
      }
    }
  }

  return { ens: resolve }
}
