'use strict'

function next() {
  return new Promise((resolve) => setTimeout(resolve, 0))
}

function toArray(value) {
  return Array.isArray(value) ? value : [value]
}

const Wappalyzer = {
  technologies: [],
  categories: [],
  requires: [],

  slugify: (string) =>
    string
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, '-')
      .replace(/--+/g, '-')
      .replace(/(?:^-|-$)/g, ''),

  getTechnology: (name) =>
    [
      ...Wappalyzer.technologies,
      ...Wappalyzer.requires.map(({ technologies }) => technologies).flat(),
    ].find(({ name: _name }) => name === _name),

  getCategory: (id) => Wappalyzer.categories.find(({ id: _id }) => id === _id),

  /**
   * Resolve promises for implied technology.
   * @param {Array} detections
   */
  resolve(detections = []) {
    const resolved = detections.reduce((resolved, { technology, lastUrl }) => {
      if (
        resolved.findIndex(
          ({ technology: { name } }) => name === technology.name
        ) === -1
      ) {
        let version = ''
        let confidence = 0

        detections
          .filter(({ technology }) => technology)
          .forEach(
            ({ technology: { name }, pattern, version: _version = '' }) => {
              if (name === technology.name) {
                confidence = Math.min(100, confidence + pattern.confidence)
                version =
                  _version.length > version.length &&
                  _version.length <= 15 &&
                  (parseInt(_version, 10) || 0) < 10000 // Ignore long numeric strings like timestamps
                    ? _version
                    : version
              }
            }
          )

        resolved.push({ technology, confidence, version, lastUrl })
      }

      return resolved
    }, [])

    Wappalyzer.resolveExcludes(resolved)
    Wappalyzer.resolveImplies(resolved)

    const priority = ({ technology: { categories } }) =>
      categories.reduce(
        (max, id) => Math.max(max, Wappalyzer.getCategory(id).priority),
        0
      )

    return resolved
      .sort((a, b) => (priority(a) > priority(b) ? 1 : -1))
      .map(
        ({
          technology: { name, slug, categories, icon, website, cpe },
          confidence,
          version,
          lastUrl,
        }) => ({
          name,
          slug,
          categories: categories.map((id) => Wappalyzer.getCategory(id)),
          confidence,
          version,
          icon,
          website,
          cpe,
          lastUrl,
        })
      )
  },

  /**
   * Resolve promises for version of technology.
   * @param {Promise} resolved
   * @param match
   */
  resolveVersion({ version, regex }, match) {
    let resolved = version

    if (version) {
      const matches = regex.exec(match)

      if (matches) {
        matches.forEach((match, index) => {
          // Parse ternary operator
          const ternary = new RegExp(`\\\\${index}\\?([^:]+):(.*)$`).exec(
            version
          )

          if (ternary && ternary.length === 3) {
            resolved = version.replace(
              ternary[0],
              match ? ternary[1] : ternary[2]
            )
          }

          // Replace back references
          resolved = resolved
            .trim()
            .replace(new RegExp(`\\\\${index}`, 'g'), match || '')
        })
      }
    }

    return resolved
  },

  /**
   * Resolve promises for excluded technology.
   * @param {Promise} resolved
   */
  resolveExcludes(resolved) {
    resolved.forEach(({ technology }) => {
      technology.excludes.forEach(({ name }) => {
        const excluded = Wappalyzer.getTechnology(name)

        if (!excluded) {
          throw new Error(`Excluded technology does not exist: ${name}`)
        }

        let index

        do {
          index = resolved.findIndex(
            ({ technology: { name } }) => name === excluded.name
          )

          if (index !== -1) {
            resolved.splice(index, 1)
          }
        } while (index !== -1)
      })
    })
  },

  /**
   * Resolve promises for implied technology.
   * @param {Promise} resolved
   */
  resolveImplies(resolved) {
    let done = false

    do {
      done = true

      resolved.forEach(({ technology, confidence, lastUrl }) => {
        technology.implies.forEach(({ name, confidence: _confidence }) => {
          const implied = Wappalyzer.getTechnology(name)

          if (!implied) {
            throw new Error(`Implied technology does not exist: ${name}`)
          }

          if (
            resolved.findIndex(
              ({ technology: { name } }) => name === implied.name
            ) === -1
          ) {
            resolved.push({
              technology: implied,
              confidence: Math.min(confidence, _confidence),
              version: '',
              lastUrl,
            })

            done = false
          }
        })
      })
    } while (resolved.length && !done)
  },

  /**
   * Initialize analyzation.
   * @param {*} param0
   */
  async analyze(
    {
      url,
      xhr,
      html,
      css,
      robots,
      magento,
      meta,
      headers,
      dns,
      certIssuer,
      cookies,
      scripts,
    },
    technologies = Wappalyzer.technologies
  ) {
    const oo = Wappalyzer.analyzeOneToOne
    const om = Wappalyzer.analyzeOneToMany
    const mm = Wappalyzer.analyzeManyToMany

    const flatten = (array) => Array.prototype.concat.apply([], array)

    try {
      const detections = flatten(
        await Promise.all(
          technologies.map(async (technology) => {
            await next()

            return flatten([
              oo(technology, 'url', url),
              oo(technology, 'xhr', xhr),
              oo(technology, 'html', html),
              oo(technology, 'css', css),
              oo(technology, 'robots', robots),
              oo(technology, 'magento', magento),
              oo(technology, 'certIssuer', certIssuer),
              om(technology, 'scripts', scripts),
              mm(technology, 'cookies', cookies),
              mm(technology, 'meta', meta),
              mm(technology, 'headers', headers),
              mm(technology, 'dns', dns),
            ])
          })
        )
      ).filter((technology) => technology)

      return detections
    } catch (error) {
      throw new Error(error.message || error.toString())
    }
  },

  /**
   * Extract technologies from data collected.
   * @param {object} data
   */
  setTechnologies(data) {
    const transform = Wappalyzer.transformPatterns

    Wappalyzer.technologies = Object.keys(data).reduce((technologies, name) => {
      const {
        cats,
        url,
        xhr,
        dom,
        html,
        css,
        robots,
        magento,
        meta,
        headers,
        dns,
        certIssuer,
        cookies,
        scripts,
        js,
        implies,
        excludes,
        requires,
        icon,
        website,
        cpe,
      } = data[name]

      technologies.push({
        name,
        categories: cats || [],
        slug: Wappalyzer.slugify(name),
        url: transform(url),
        xhr: transform(xhr),
        headers: transform(headers),
        dns: transform(dns),
        cookies: transform(cookies),
        dom: transform(
          typeof dom === 'string' || Array.isArray(dom)
            ? toArray(dom).reduce(
                (dom, selector) => ({
                  ...dom,
                  [selector]: { exists: '' },
                }),
                {}
              )
            : dom,
          false,
          false
        ),
        html: transform(html),
        css: transform(css),
        certIssuer: transform(certIssuer),
        robots: transform(robots),
        magento: transform(magento),
        meta: transform(meta),
        scripts: transform(scripts),
        js: transform(js, true),
        implies: transform(implies).map(({ value, confidence }) => ({
          name: value,
          confidence,
        })),
        excludes: transform(excludes).map(({ value }) => ({
          name: value,
        })),
        requires: transform(requires).map(({ value }) => ({
          name: value,
        })),
        icon: icon || 'default.svg',
        website: website || null,
        cpe: cpe || null,
      })

      return technologies
    }, [])

    Wappalyzer.technologies
      .filter(({ requires }) => requires.length)
      .forEach((technology) =>
        technology.requires.forEach(({ name }) => {
          if (!Wappalyzer.getTechnology(name)) {
            throw new Error(`Required technology does not exist: ${name}`)
          }

          Wappalyzer.requires[name] = Wappalyzer.requires[name] || []

          Wappalyzer.requires[name].push(technology)
        })
      )

    Wappalyzer.requires = Object.keys(Wappalyzer.requires).map((name) => ({
      name,
      technologies: Wappalyzer.requires[name],
    }))

    Wappalyzer.technologies = Wappalyzer.technologies.filter(
      ({ requires }) => !requires.length
    )
  },

  /**
   * Assign categories for data.
   * @param {Object} data
   */
  setCategories(data) {
    Wappalyzer.categories = Object.keys(data)
      .reduce((categories, id) => {
        const category = data[id]

        categories.push({
          id: parseInt(id, 10),
          slug: Wappalyzer.slugify(category.name),
          ...category,
        })

        return categories
      }, [])
      .sort(({ priority: a }, { priority: b }) => (a > b ? -1 : 0))
  },

  /**
   * Transform patterns for internal use.
   * @param {string|array} patterns
   * @param {boolean} caseSensitive
   */
  transformPatterns(patterns, caseSensitive = false, isRegex = true) {
    if (!patterns) {
      return []
    }

    if (typeof patterns === 'string' || Array.isArray(patterns)) {
      patterns = { main: patterns }
    }

    const parsed = Object.keys(patterns).reduce((parsed, key) => {
      parsed[caseSensitive ? key : key.toLowerCase()] = toArray(
        patterns[key]
      ).map((pattern) => Wappalyzer.parsePattern(pattern, isRegex))

      return parsed
    }, {})

    return 'main' in parsed ? parsed.main : parsed
  },

  /**
   * Extract information from regex pattern.
   * @param {string|object} pattern
   */
  parsePattern(pattern, isRegex = true) {
    if (typeof pattern === 'object') {
      return Object.keys(pattern).reduce(
        (parsed, key) => ({
          ...parsed,
          [key]: Wappalyzer.parsePattern(pattern[key]),
        }),
        {}
      )
    } else {
      const { value, regex, confidence, version } = pattern
        .split('\\;')
        .reduce((attrs, attr, i) => {
          if (i) {
            // Key value pairs
            attr = attr.split(':')

            if (attr.length > 1) {
              attrs[attr.shift()] = attr.join(':')
            }
          } else {
            attrs.value = attr

            // Escape slashes in regular expression
            attrs.regex = new RegExp(
              isRegex ? attr.replace(/\//g, '\\/') : '',
              'i'
            )
          }

          return attrs
        }, {})

      return {
        value,
        regex,
        confidence: parseInt(confidence || 100, 10),
        version: version || '',
      }
    }
  },

  /**
   * @todo describe
   * @param {Object} technology
   * @param {String} type
   * @param {String} value
   */
  analyzeOneToOne(technology, type, value) {
    return technology[type].reduce((technologies, pattern) => {
      if (pattern.regex.test(value)) {
        technologies.push({
          technology,
          pattern,
          version: Wappalyzer.resolveVersion(pattern, value),
        })
      }

      return technologies
    }, [])
  },

  /**
   * @todo update
   * @param {Object} technology
   * @param {String} type
   * @param {Array} items
   */
  analyzeOneToMany(technology, type, items = []) {
    return items.reduce((technologies, value) => {
      const patterns = technology[type] || []

      patterns.forEach((pattern) => {
        if (pattern.regex.test(value)) {
          technologies.push({
            technology,
            pattern,
            version: Wappalyzer.resolveVersion(pattern, value),
          })
        }
      })

      return technologies
    }, [])
  },

  /**
   *
   * @param {Object} technology
   * @param {string} types
   * @param {Array} items
   */
  analyzeManyToMany(technology, types, items = {}) {
    const [type, ...subtypes] = types.split('.')

    return Object.keys(technology[type]).reduce((technologies, key) => {
      const patterns = technology[type][key] || []
      const values = items[key] || []

      patterns.forEach((_pattern) => {
        const pattern = (subtypes || []).reduce(
          (pattern, subtype) => pattern[subtype] || {},
          _pattern
        )

        values.forEach((value) => {
          if (pattern.regex.test(value)) {
            technologies.push({
              technology,
              pattern,
              version: Wappalyzer.resolveVersion(pattern, value),
            })
          }
        })
      })

      return technologies
    }, [])
  },
}

if (typeof module !== 'undefined') {
  module.exports = Wappalyzer
}
