/**
 * @packageDocumentation
 *
 * This module exports various matchers that can be used to infer the type of a
 * passed multiaddr.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNS } from '@multiformats/multiaddr-matcher'
 *
 * const ma = multiaddr('/dnsaddr/example.org')
 *
 * DNS.matches(ma) // true - this is a multiaddr with a DNS address at the start
 * ```
 *
 * @example
 *
 * The default matching behaviour ignores any subsequent tuples in the multiaddr.
 * If you want stricter matching you can use `.exactMatch`:
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNS, Circuit } from '@multiformats/multiaddr-matcher'
 *
 * const ma = multiaddr('/dnsaddr/example.org/p2p/QmFoo/p2p-circuit/p2p/QmBar')
 *
 * DNS.exactMatch(ma) // false - this address has extra tuples after the DNS component
 * Circuit.matches(ma) // true
 * Circuit.exactMatch(ma) // true - the extra tuples are circuit relay related
 * ```
 */

import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
import { and, or, literal, string, peerId, optional, fmt, func, number, certhash } from './utils.js'
import type { Multiaddr } from '@multiformats/multiaddr'

/**
 * A matcher accepts multiaddr components and either fails to match and returns
 * false or returns a sublist of unmatched components
 */
export interface Matcher {
  match(parts: string[]): string[] | false
  pattern: string
}

/**
 * A MultiaddrMatcher allows interpreting a multiaddr as a certain type of
 * multiaddr
 */
export interface MultiaddrMatcher {
  /**
   * The matchers that make up this MultiaddrMatcher - useful if you want to
   * make your own custom matchers
   */
  matchers: Matcher[]

  /**
   * Returns true if the passed multiaddr can be treated as this type of
   * multiaddr
   */
  matches(ma: Multiaddr): boolean

  /**
   * Returns true if the passed multiaddr terminates as this type of
   * multiaddr
   */
  exactMatch(ma: Multiaddr): boolean
}

/**
 * Matches PeerId addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { PEER_ID } from '@multiformats/multiaddr-matcher'
 *
 * PEER_ID.matches(multiaddr('/p2p/Qmfoo')) // true
 * PEER_ID.matches(multiaddr('/ipfs/Qmfoo')) // true
 * ```
 */
const _PEER_ID = peerId()

export const PEER_ID = fmt(_PEER_ID)

/**
 * DNS matchers
 */
const _DNS4 = and(literal('dns4'), string())
const _DNS6 = and(literal('dns6'), string())
const _DNSADDR = and(literal('dnsaddr'), string())
const _DNS = and(literal('dns'), string())

/**
 * Matches dns4 addresses.
 *
 * Use {@link DNS DNS} instead to match any type of DNS address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNS4 } from '@multiformats/multiaddr-matcher'
 *
 * DNS4.matches(multiaddr('/dns4/example.org')) // true
 * ```
 */
export const DNS4 = fmt(_DNS4, optional(peerId()))

/**
 * Matches dns6 addresses.
 *
 * Use {@link DNS DNS} instead to match any type of DNS address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNS6 } from '@multiformats/multiaddr-matcher'
 *
 * DNS6.matches(multiaddr('/dns6/example.org')) // true
 * ```
 */
export const DNS6 = fmt(_DNS6, optional(peerId()))

/**
 * Matches dnsaddr addresses.
 *
 * Use {@link DNS DNS} instead to match any type of DNS address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNSADDR } from '@multiformats/multiaddr-matcher'
 *
 * DNSADDR.matches(multiaddr('/dnsaddr/example.org')) // true
 * DNSADDR.matches(multiaddr('/dnsaddr/example.org/p2p/Qmfoo')) // true
 * ```
 */
export const DNSADDR = fmt(_DNSADDR, optional(peerId()))

/**
 * Matches any dns address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { DNS } from '@multiformats/multiaddr-matcher'
 *
 * DNS.matches(multiaddr('/dnsaddr/example.org')) // true
 * DNS.matches(multiaddr('/dns4/example.org')) // true
 * DNS.matches(multiaddr('/dns6/example.org')) // true
 * DNS.matches(multiaddr('/dns6/example.org/p2p/Qmfoo')) // true
 * ```
 */
export const DNS = fmt(or(_DNS, _DNSADDR, _DNS4, _DNS6), optional(peerId()))

const _IP4 = and(literal('ip4'), func(isIPv4))
const _IP6 = and(literal('ip6'), func(isIPv6))
const _IP = or(_IP4, _IP6)

const _IP_OR_DOMAIN = or(_IP, _DNS, _DNS4, _DNS6, _DNSADDR)

/**
 * A matcher for addresses that start with IP or DNS tuples.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { IP_OR_DOMAIN } from '@multiformats/multiaddr-matcher'
 *
 * IP_OR_DOMAIN.matches(multiaddr('/ip4/123.123.123.123')) // true
 * IP_OR_DOMAIN.matches(multiaddr('/ip4/123.123.123.123/p2p/QmFoo')) // true
 * IP_OR_DOMAIN.matches(multiaddr('/dns/example.com/p2p/QmFoo')) // true
 * IP_OR_DOMAIN.matches(multiaddr('/p2p/QmFoo')) // false
 * ```
 */
export const IP_OR_DOMAIN = fmt(or(_IP, and(or(_DNS, _DNSADDR, _DNS4, _DNS6), optional(peerId()))))

/**
 * Matches ip4 addresses.
 *
 * Use {@link IP IP} instead to match any ip4/ip6 address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { IP4 } from '@multiformats/multiaddr-matcher'
 *
 * const ma = multiaddr('/ip4/123.123.123.123')
 *
 * IP4.matches(ma) // true
 * ```
 */
export const IP4 = fmt(_IP4)

/**
 * Matches ip6 addresses.
 *
 * Use {@link IP IP} instead to match any ip4/ip6 address.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { IP6 } from '@multiformats/multiaddr-matcher'
 *
 * const ma = multiaddr('/ip6/fe80::1cc1:a3b8:322f:cf22')
 *
 * IP6.matches(ma) // true
 * ```
 */
export const IP6 = fmt(_IP6)

/**
 * Matches ip4 or ip6 addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { IP } from '@multiformats/multiaddr-matcher'
 *
 * IP.matches(multiaddr('/ip4/123.123.123.123')) // true
 * IP.matches(multiaddr('/ip6/fe80::1cc1:a3b8:322f:cf22')) // true
 * ```
 */
export const IP = fmt(_IP)

const _TCP = and(_IP_OR_DOMAIN, literal('tcp'), number())
const _UDP = and(_IP_OR_DOMAIN, literal('udp'), number())

/**
 * Matches TCP addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { TCP } from '@multiformats/multiaddr-matcher'
 *
 * TCP.matches(multiaddr('/ip4/123.123.123.123/tcp/1234')) // true
 * ```
 */
export const TCP = fmt(and(_TCP, optional(peerId())))

/**
 * Matches UDP addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { UDP } from '@multiformats/multiaddr-matcher'
 *
 * UDP.matches(multiaddr('/ip4/123.123.123.123/udp/1234')) // true
 * ```
 */
export const UDP = fmt(_UDP)

const _QUIC = and(_UDP, literal('quic'), optional(peerId()))
const _QUICV1 = and(_UDP, literal('quic-v1'), optional(peerId()))

const QUIC_V0_OR_V1 = or(_QUIC, _QUICV1)

/**
 * Matches QUIC addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { QUIC } from '@multiformats/multiaddr-matcher'
 *
 * QUIC.matches(multiaddr('/ip4/123.123.123.123/udp/1234/quic')) // true
 * ```
 */
export const QUIC = fmt(_QUIC)

/**
 * Matches QUICv1 addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { QUICV1 } from '@multiformats/multiaddr-matcher'
 *
 * QUICV1.matches(multiaddr('/ip4/123.123.123.123/udp/1234/quic-v1')) // true
 * ```
 */
export const QUICV1 = fmt(_QUICV1)

const _WEB = or(
  _IP_OR_DOMAIN,
  _TCP,
  _UDP,
  _QUIC,
  _QUICV1
)

const _WebSockets = or(
  and(_WEB, literal('ws'), optional(peerId()))
)

/**
 * Matches WebSocket addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { WebSockets } from '@multiformats/multiaddr-matcher'
 *
 * WebSockets.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/ws')) // true
 * ```
 */
export const WebSockets = fmt(_WebSockets)

const _WebSocketsSecure = or(
  and(_WEB, literal('wss'), optional(peerId())),
  and(_WEB, literal('tls'), optional(and(literal('sni'), string())), literal('ws'), optional(peerId()))
)

/**
 * Matches secure WebSocket addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { WebSocketsSecure } from '@multiformats/multiaddr-matcher'
 *
 * WebSocketsSecure.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/wss')) // true
 * ```
 */
export const WebSocketsSecure = fmt(_WebSocketsSecure)

const _WebRTCDirect = and(_UDP, literal('webrtc-direct'), optional(certhash()), optional(certhash()), optional(peerId()))

/**
 * Matches WebRTC-direct addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
 *
 * WebRTCDirect.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/p2p/QmFoo/webrtc-direct/certhash/u....')) // true
 * ```
 */
export const WebRTCDirect = fmt(_WebRTCDirect)

const _WebTransport = and(_QUICV1, literal('webtransport'), optional(certhash()), optional(certhash()), optional(peerId()))

/**
 * Matches WebTransport addresses.
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
 *
 * WebRTCDirect.matches(multiaddr('/ip4/123.123.123.123/udp/1234/quic-v1/webtransport/certhash/u..../certhash/u..../p2p/QmFoo')) // true
 * ```
 */
export const WebTransport = fmt(_WebTransport)

const _P2P = or(
  _WebSockets,
  _WebSocketsSecure,
  and(_TCP, optional(peerId())),
  and(QUIC_V0_OR_V1, optional(peerId())),
  and(_IP_OR_DOMAIN, optional(peerId())),
  _WebRTCDirect,
  _WebTransport,
  peerId()
)

/**
 * Matches peer addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { P2P } from '@multiformats/multiaddr-matcher'
 *
 * P2P.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/p2p/QmFoo')) // true
 * ```
 */
export const P2P = fmt(_P2P)

const _Circuit = and(_P2P, literal('p2p-circuit'), peerId())

/**
 * Matches circuit relay addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { Circuit } from '@multiformats/multiaddr-matcher'
 *
 * Circuit.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/p2p/QmRelay/p2p-circuit/p2p/QmTarget')) // true
 * ```
 */
export const Circuit = fmt(_Circuit)

const _WebRTC = or(
  and(_P2P, literal('p2p-circuit'), literal('webrtc'), optional(peerId())),
  and(_P2P, literal('webrtc'), optional(peerId())),
  and(literal('webrtc'), optional(peerId()))
)

/**
 * Matches WebRTC addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { WebRTC } from '@multiformats/multiaddr-matcher'
 *
 * WebRTC.matches(multiaddr('/ip4/123.123.123.123/tcp/1234/p2p/QmRelay/p2p-circuit/webrtc/p2p/QmTarget')) // true
 * ```
 */
export const WebRTC = fmt(_WebRTC)

const _HTTP = or(
  and(_IP_OR_DOMAIN, literal('tcp'), number(), literal('http'), optional(peerId())),
  and(_IP_OR_DOMAIN, literal('http'), optional(peerId()))
)

/**
 * Matches HTTP addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { HTTP } from '@multiformats/multiaddr-matcher'
 *
 * HTTP.matches(multiaddr('/dns/example.org/http')) // true
 * ```
 */
export const HTTP = fmt(_HTTP)

const _HTTPS = or(
  and(_IP_OR_DOMAIN, literal('tcp'), or(
    and(literal('443'), literal('http')),
    and(number(), literal('https'))
  ), optional(peerId())),
  and(_IP_OR_DOMAIN, literal('tls'), literal('http'), optional(peerId())),
  and(_IP_OR_DOMAIN, literal('https'), optional(peerId()))
)

/**
 * Matches HTTPS addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { HTTP } from '@multiformats/multiaddr-matcher'
 *
 * HTTP.matches(multiaddr('/dns/example.org/tls/http')) // true
 * ```
 */
export const HTTPS = fmt(_HTTPS)

const _Memory = or(
  and(literal('memory'), string(), optional(peerId()))
)

/**
 * Matches Memory addresses
 *
 * @example
 *
 * ```ts
 * import { multiaddr } from '@multiformats/multiaddr'
 * import { Memory } from '@multiformats/multiaddr-matcher'
 *
 * Memory.matches(multiaddr('/memory/0xDEADBEEF')) // true
 * ```
 */
export const Memory = fmt(_Memory)
