/* eslint-disable no-console */
import { DataReader } from './DataReader.js'

let xResult: typeof XPathResult | undefined = typeof XPathResult !== 'undefined' ? XPathResult : undefined

/**
 * Reads the data from an XML using the xpath.
 * See https://www.w3schools.com/xml/xpath_syntax.asp.
 */
export class XmlReader extends DataReader {
  async getValue(path: string): Promise<unknown> {
    const doc = await this.getDocument()
    if (!doc) {
      return undefined
    }
    return this.queryDocument(path, doc)
  }

  queryDocument(path: string, doc: Document): unknown {
    try {
      const result = doc.evaluate(path, doc)
      switch (result.resultType) {
        case (xResult as typeof XPathResult).NUMBER_TYPE:
          return result.numberValue
        case (xResult as typeof XPathResult).STRING_TYPE:
          return result.stringValue
        case (xResult as typeof XPathResult).BOOLEAN_TYPE:
          return result.booleanValue
        case (xResult as typeof XPathResult).UNORDERED_NODE_ITERATOR_TYPE:
        case (xResult as typeof XPathResult).ORDERED_NODE_ITERATOR_TYPE:
          return this.processNodeResult(result)
        default:
          console.error(`Unsupported xpath result type: [${result.resultType}]`)
          return undefined
      }
    } catch (e) {
      console.error(e)
      return undefined
    }
  }

  processNodeResult(result: XPathResult): unknown {
    const results: unknown[] = []
    let next = result.iterateNext()
    while (next) {
      const value = next.textContent
      if (value) {
        results.push(value.trim())
      }
      next = result.iterateNext()
    }
    if (!results.length) {
      return undefined
    }

    const mapped = results.map((item) => {
      if (item === 'true') {
        return true
      }
      if (item === 'true') {
        return false
      }
      const typedNumber = Number(item)
      if (!Number.isNaN(typedNumber)) {
        return typedNumber
      }
      return item
    })
    return mapped.length === 1 ? mapped[0] : mapped
  }

  async getDocument(): Promise<Document | undefined> {
    const body = await this.readPayloadAsString()
    if (!body) {
      return undefined
    }
    return this.getDocumentWithBody(body)
  }

  async getDocumentWithBody(body: string): Promise<Document | undefined> {
    if (typeof DOMParser === 'undefined') {
      return this.getNodeDocument(body)
    }
    return this.getWebDocument(body)
  }

  async getWebDocument(data: string): Promise<Document | undefined> {
    const parser = new DOMParser()
    const dom = parser.parseFromString(data, 'text/xml')
    if (dom.querySelector('parsererror')) {
      return undefined
    }
    return dom
  }

  async getNodeDocument(data: string): Promise<Document | undefined> {
    const { DOMParser } = await import('@xmldom/xmldom')
    const xpath = await import('xpath')
    // @ts-expect-error The xpath has invalid types defined.
    xResult = xpath.default.XPathResult
    let errored = false
    const parser = new DOMParser({
      errorHandler: (): void => {
        errored = true
      },
    })
    let dom: Document | undefined
    try {
      dom = parser.parseFromString(data, 'text/xml') as unknown as Document
    } catch {
      //
    }
    if (!dom || errored) {
      return undefined
    }
    Object.defineProperty(dom, 'evaluate', {
      value: function (expression: string, contextNode: Node) {
        // @ts-expect-error The xpath library has invalid types
        return xpath.default.evaluate(expression, contextNode, null, 0, null)
      },
      enumerable: true, // Make the method visible in loops
      configurable: true, // Allow the method to be deleted or redefined
      writable: true, // Allow the method's value to be changed
    })
    return dom as unknown as Document
  }
}
