const fs = require('node:fs')
const path = require('node:path')
const log = require('../../../../utils/util.log.server')

function createPacClient (pacFilePath) {
  const __PROXY__ = 'PROXY 127.0.0.1:1080;'

  function readFile (location) {
    try {
      log.info('pac root dir:', path.resolve('./'))
      log.info('pac location:', location)
      const filePath = path.resolve(location)
      log.info('read pac path:', filePath)
      return fs.readFileSync(location).toString()
    } catch (e) {
      log.error('读取pac失败:', e)
      return ''
    }
  }

  const getRules = function (pacFilePath) {
    let text = readFile(pacFilePath)
    if (!text.includes('!---------------------EOF')) {
      text = Buffer.from(text, 'base64').toString()
    }
    const rules = []
    const arr = text.split('\n')
    for (const line of arr) {
      const row = line.trim()
      if (row === '' || row.indexOf('!') === 0 || row.indexOf('[') === 0) {
        continue
      }
      rules.push(row)
    }
    return rules
  }
  const __RULES__ = getRules(pacFilePath)

  /* eslint-disable */
  // Was generated by gfwlist2pac in precise mode
  // https://github.com/clowwindy/gfwlist2pac

  // 2019-10-06: More 'javascript' way to interaction with main program
  // 2019-02-08: Updated to support shadowsocks-windows user rules.

  const proxy = __PROXY__
  const rules = []

  // convert to abp grammar
  for (let i = 0; i < __RULES__.length; i++) {
    let s = __RULES__[i]
    if (s.substring(0, 2) === "||") s += "^"
    rules.push(s)
  }

  /*
  * This file is part of Adblock Plus <http://adblockplus.org/>,
  * Copyright (C) 2006-2014 Eyeo GmbH
  *
  * Adblock Plus is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 3 as
  * published by the Free Software Foundation.
  *
  * Adblock Plus is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
  */

  function createDict () {
    const result = {}
    result.__proto__ = null
    return result
  }

  function getOwnPropertyDescriptor (obj, key) {
    if (obj.hasOwnProperty(key)) {
      return obj[key]
    }
    return null
  }

  function extend (subClass, superClass, definition) {
    if (Object.__proto__) {
      definition.__proto__ = superClass.prototype
      subClass.prototype = definition
    } else {
      const tmpClass = function () {}
      tmpClass.prototype = superClass.prototype
      subClass.prototype = new tmpClass()
      subClass.prototype.constructor = superClass
      for (const key in definition) {
        if (definition.hasOwnProperty(key)) {
          subClass.prototype[key] = definition[key]
        }
      }
    }
  }

  function Filter (text) {
    this.text = text
    this.subscriptions = []
  }

  Filter.prototype = {
    text: null,
    subscriptions: null,
    toString: function () {
      return this.text
    }
  }
  Filter.knownFilters = createDict()
  Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/
  Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/
  Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/
  Filter.fromText = function (text) {
    if (text in Filter.knownFilters) {
      return Filter.knownFilters[text]
    }
    let ret
    if (text.charAt(0) === "!") {
      ret = new CommentFilter(text)
    } else {
      ret = RegExpFilter.fromText(text)
    }
    Filter.knownFilters[ret.text] = ret
    return ret
  }

  function InvalidFilter (text, reason) {
    Filter.call(this, text)
    this.reason = reason
  }

  extend(InvalidFilter, Filter, {
    reason: null
  })

  function CommentFilter (text) {
    Filter.call(this, text)
  }

  extend(CommentFilter, Filter, {})

  function ActiveFilter (text, domains) {
    Filter.call(this, text)
    this.domainSource = domains
  }

  extend(ActiveFilter, Filter, {
    domainSource: null,
    domainSeparator: null,
    ignoreTrailingDot: true,
    domainSourceIsUpperCase: false,
    getDomains: function () {
      const prop = getOwnPropertyDescriptor(this, "domains")
      if (prop) {
        return prop
      }
      let domains = null
      if (this.domainSource) {
        let source = this.domainSource
        if (!this.domainSourceIsUpperCase) {
          source = source.toUpperCase()
        }
        const list = source.split(this.domainSeparator)
        if (list.length === 1 && (list[0]).charAt(0) !== "~") {
          domains = createDict()
          domains[""] = false
          if (this.ignoreTrailingDot) {
            list[0] = list[0].replace(/\.+$/, "")
          }
          domains[list[0]] = true
        } else {
          let hasIncludes = false
          for (let i = 0; i < list.length; i++) {
            let domain = list[i]
            if (this.ignoreTrailingDot) {
              domain = domain.replace(/\.+$/, "")
            }
            if (domain === "") {
              continue
            }
            let include
            if (domain.charAt(0) === "~") {
              include = false
              domain = domain.substr(1)
            } else {
              include = true
              hasIncludes = true
            }
            if (!domains) {
              domains = createDict()
            }
            domains[domain] = include
          }
          domains[""] = !hasIncludes
        }
        this.domainSource = null
      }
      return this.domains
    },
    siteKeys: null,
    isActiveOnDomain: function (docDomain, siteKey) {
      if (this.getSiteKeys() && (!siteKey || this.getSiteKeys().indexOf(siteKey.toUpperCase()) < 0)) {
        return false
      }
      if (!this.getDomains()) {
        return true
      }
      if (!docDomain) {
        return this.getDomains()[""]
      }
      if (this.ignoreTrailingDot) {
        docDomain = docDomain.replace(/\.+$/, "")
      }
      docDomain = docDomain.toUpperCase()
      while (true) {
        if (docDomain in this.getDomains()) {
          return this.domains[docDomain]
        }
        const nextDot = docDomain.indexOf(".")
        if (nextDot < 0) {
          break
        }
        docDomain = docDomain.substr(nextDot + 1)
      }
      return this.domains[""]
    }/*,
    isActiveOnlyOnDomain: function (docDomain) {
      if (!docDomain || !this.getDomains() || this.getDomains()[""]) {
        return false
      }
      if (this.ignoreTrailingDot) {
        docDomain = docDomain.replace(/\.+$/, "")
      }
      docDomain = docDomain.toUpperCase()
      for (const domain in this.getDomains()) {
        if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) {
          return false
        }
      }
      return true
    }*/
  })

  function RegExpFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {
    ActiveFilter.call(this, text, domains, siteKeys)
    if (contentType != null) {
      this.contentType = contentType
    }
    if (matchCase) {
      this.matchCase = matchCase
    }
    if (thirdParty != null) {
      this.thirdParty = thirdParty
    }
    if (siteKeys != null) {
      this.siteKeySource = siteKeys
    }
    if (regexpSource.length >= 2 && regexpSource.charAt(0) === "/" && regexpSource.charAt(regexpSource.length - 1) === "/") {
      this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i")
    } else {
      this.regexpSource = regexpSource
    }
  }

  extend(RegExpFilter, ActiveFilter, {
    domainSourceIsUpperCase: true,
    length: 1,
    domainSeparator: "|",
    regexpSource: null,
    getRegexp: function () {
      const prop = getOwnPropertyDescriptor(this, "regexp")
      if (prop) {
        return prop
      }
      const source = this.regexpSource.replace(/\*+/g, "*").replace(/\^\|$/, "^").replace(/\W/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)").replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?").replace(/^\\\|/, "^").replace(/\\\|$/, "$").replace(/^(\.\*)/, "").replace(/(\.\*)$/, "")
      const regexp = new RegExp(source, this.matchCase ? "" : "i")
      this.regexp = regexp
      return regexp
    },
    contentType: 2147483647,
    matchCase: false,
    thirdParty: null,
    siteKeySource: null,
    getSiteKeys: function () {
      const prop = getOwnPropertyDescriptor(this, "siteKeys")
      if (prop) {
        return prop
      }
      let siteKeys = null
      if (this.siteKeySource) {
        siteKeys = this.siteKeySource.split("|")
        this.siteKeySource = null
      }
      this.siteKeys = siteKeys
      return this.siteKeys
    },
    matches: function (location, contentType, docDomain, thirdParty, siteKey) {
      return !!(this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, siteKey))
    }
  })
  RegExpFilter.prototype["0"] = "#this"
  RegExpFilter.fromText = function (text) {
    let blocking = true
    const origText = text
    if (text.indexOf("@@") === 0) {
      blocking = false
      text = text.substr(2)
    }
    let contentType = null
    let matchCase = null
    let domains = null
    let siteKeys = null
    let thirdParty = null
    let collapse = null
    let options
    const match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null
    if (match) {
      options = match[1].toUpperCase().split(",")
      text = match.input.substr(0, match.index)
      for (let _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) {
        let option = options[_loopIndex6]
        let value = null
        const separatorIndex = option.indexOf("=")
        if (separatorIndex >= 0) {
          value = option.substr(separatorIndex + 1)
          option = option.substr(0, separatorIndex)
        }
        option = option.replace(/-/, "_")
        if (option in RegExpFilter.typeMap) {
          if (contentType == null) {
            contentType = 0
          }
          contentType |= RegExpFilter.typeMap[option]
        } else if (option.charAt(0) === "~" && option.substr(1) in RegExpFilter.typeMap) {
          if (contentType == null) {
            contentType = RegExpFilter.prototype.contentType
          }
          contentType &= ~RegExpFilter.typeMap[option.substr(1)]
        } else if (option === "MATCH_CASE") {
          matchCase = true
        } else if (option === "~MATCH_CASE") {
          matchCase = false
        } else if (option === "DOMAIN" && typeof value != "undefined") {
          domains = value
        } else if (option === "THIRD_PARTY") {
          thirdParty = true
        } else if (option === "~THIRD_PARTY") {
          thirdParty = false
        } else if (option === "COLLAPSE") {
          collapse = true
        } else if (option === "~COLLAPSE") {
          collapse = false
        } else if (option === "SITEKEY" && typeof value != "undefined") {
          siteKeys = value
        } else {
          return new InvalidFilter(origText, "Unknown option " + option.toLowerCase())
        }
      }
    }
    if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) {
      if (contentType == null) {
        contentType = RegExpFilter.prototype.contentType
      }
      contentType &= ~RegExpFilter.typeMap.DOCUMENT
    }
    try {
      if (blocking) {
        return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys, collapse)
      } else {
        return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys)
      }
    } catch (e) {
      return new InvalidFilter(origText, e)
    }
  }
  RegExpFilter.typeMap = {
    OTHER: 1,
    SCRIPT: 2,
    IMAGE: 4,
    STYLESHEET: 8,
    OBJECT: 16,
    SUBDOCUMENT: 32,
    DOCUMENT: 64,
    XBL: 1,
    PING: 1,
    XMLHTTPREQUEST: 2048,
    OBJECT_SUBREQUEST: 4096,
    DTD: 1,
    MEDIA: 16384,
    FONT: 32768,
    BACKGROUND: 4,
    POPUP: 268435456,
    ELEMHIDE: 1073741824
  }
  RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP)

  function BlockingFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys, collapse) {
    RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)
    this.collapse = collapse
  }

  extend(BlockingFilter, RegExpFilter, {
    collapse: null
  })

  function WhitelistFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {
    RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)
  }

  extend(WhitelistFilter, RegExpFilter, {})

  function Matcher () {
    this.clear()
  }

  Matcher.prototype = {
    filterByKeyword: null,
    keywordByFilter: null,
    clear: function () {
      this.filterByKeyword = createDict()
      this.keywordByFilter = createDict()
    },
    add: function (filter) {
      if (filter.text in this.keywordByFilter) {
        return
      }
      const keyword = this.findKeyword(filter)
      const oldEntry = this.filterByKeyword[keyword]
      if (typeof oldEntry == "undefined") {
        this.filterByKeyword[keyword] = filter
      } else if (oldEntry.length === 1) {
        this.filterByKeyword[keyword] = [oldEntry, filter]
      } else {
        oldEntry.push(filter)
      }
      this.keywordByFilter[filter.text] = keyword
    },
    remove: function (filter) {
      if (!(filter.text in this.keywordByFilter)) {
        return
      }
      const keyword = this.keywordByFilter[filter.text]
      const list = this.filterByKeyword[keyword]
      if (list.length <= 1) {
        delete this.filterByKeyword[keyword]
      } else {
        const index = list.indexOf(filter)
        if (index >= 0) {
          list.splice(index, 1)
          if (list.length === 1) {
            this.filterByKeyword[keyword] = list[0]
          }
        }
      }
      delete this.keywordByFilter[filter.text]
    },
    findKeyword: function (filter) {
      let result = ""
      let text = filter.text
      if (Filter.regexpRegExp.test(text)) {
        return result
      }
      const match = Filter.optionsRegExp.exec(text)
      if (match) {
        text = match.input.substr(0, match.index)
      }
      if (text.substr(0, 2) === "@@") {
        text = text.substr(2)
      }
      const candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g)
      if (!candidates) {
        return result
      }
      const hash = this.filterByKeyword
      let resultCount = 16777215
      let resultLength = 0
      for (let i = 0, l = candidates.length; i < l; i++) {
        const candidate = candidates[i].substr(1)
        const count = candidate in hash ? hash[candidate].length : 0
        if (count < resultCount || count === resultCount && candidate.length > resultLength) {
          result = candidate
          resultCount = count
          resultLength = candidate.length
        }
      }
      return result
    },
    hasFilter: function (filter) {
      return filter.text in this.keywordByFilter
    },
    getKeywordForFilter: function (filter) {
      if (filter.text in this.keywordByFilter) {
        return this.keywordByFilter[filter.text]
      } else {
        return null
      }
    },
    _checkEntryMatch: function (keyword, location, contentType, docDomain, thirdParty, siteKey) {
      const list = this.filterByKeyword[keyword]
      for (let i = 0; i < list.length; i++) {
        let filter = list[i]
        if (filter === "#this") {
          filter = list
        }
        if (filter.matches(location, contentType, docDomain, thirdParty, siteKey)) {
          return filter
        }
      }
      return null
    }/*,
    matchesAny: function (location, contentType, docDomain, thirdParty, siteKey) {
      let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)
      if (candidates === null) {
        candidates = []
      }
      candidates.push("")
      for (let i = 0, l = candidates.length; i < l; i++) {
        const substr = candidates[i]
        if (substr in this.filterByKeyword) {
          const result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
          if (result) {
            return result
          }
        }
      }
      return null
    }*/
  }

  function CombinedMatcher () {
    this.blacklist = new Matcher()
    this.whitelist = new Matcher()
    this.resultCache = createDict()
  }

  CombinedMatcher.maxCacheEntries = 1000
  CombinedMatcher.prototype = {
    blacklist: null,
    whitelist: null,
    resultCache: null,
    cacheEntries: 0,
    clear: function () {
      this.blacklist.clear()
      this.whitelist.clear()
      this.resultCache = createDict()
      this.cacheEntries = 0
    },
    add: function (filter) {
      if (filter instanceof WhitelistFilter) {
        this.whitelist.add(filter)
      } else {
        this.blacklist.add(filter)
      }
      if (this.cacheEntries > 0) {
        this.resultCache = createDict()
        this.cacheEntries = 0
      }
    },
    remove: function (filter) {
      if (filter instanceof WhitelistFilter) {
        this.whitelist.remove(filter)
      } else {
        this.blacklist.remove(filter)
      }
      if (this.cacheEntries > 0) {
        this.resultCache = createDict()
        this.cacheEntries = 0
      }
    },
    findKeyword: function (filter) {
      if (filter instanceof WhitelistFilter) {
        return this.whitelist.findKeyword(filter)
      } else {
        return this.blacklist.findKeyword(filter)
      }
    },
    hasFilter: function (filter) {
      if (filter instanceof WhitelistFilter) {
        return this.whitelist.hasFilter(filter)
      } else {
        return this.blacklist.hasFilter(filter)
      }
    },
    getKeywordForFilter: function (filter) {
      if (filter instanceof WhitelistFilter) {
        return this.whitelist.getKeywordForFilter(filter)
      } else {
        return this.blacklist.getKeywordForFilter(filter)
      }
    },
    /*isSlowFilter: function (filter) {
      const matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist
      if (matcher.hasFilter(filter)) {
        return !matcher.getKeywordForFilter(filter)
      } else {
        return !matcher.findKeyword(filter)
      }
    },*/
    matchesAnyInternal: function (location, contentType, docDomain, thirdParty, siteKey) {
      let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)
      if (candidates === null) {
        candidates = []
      }
      candidates.push("")
      let blacklistHit = null
      for (let i = 0, l = candidates.length; i < l; i++) {
        const substr = candidates[i]
        if (substr in this.whitelist.filterByKeyword) {
          const result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
          if (result) {
            return result
          }
        }
        if (substr in this.blacklist.filterByKeyword && blacklistHit === null) {
          blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
        }
      }
      return blacklistHit
    },
    matchesAny: function (location, docDomain) {
      const key = location + " " + docDomain + " "
      if (key in this.resultCache) {
        return this.resultCache[key]
      }
      const result = this.matchesAnyInternal(location, 0, docDomain, null, null)
      if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) {
        this.resultCache = createDict()
        this.cacheEntries = 0
      }
      this.resultCache[key] = result
      this.cacheEntries++
      return result
    }
  }

  const userrulesMatcher = new CombinedMatcher()
  const defaultMatcher = new CombinedMatcher()

  const direct = 'DIRECT;'

  for (let i = 0; i < rules.length; i++) {
    defaultMatcher.add(Filter.fromText(rules[i]))
  }

  function FindProxyForURL (url, host) {
    let matchedResult = userrulesMatcher.matchesAny(url, host)
    if (matchedResult instanceof BlockingFilter) {
      return proxy
    } else if (matchedResult instanceof WhitelistFilter) {
      return direct
    }

    // Hack for Geosite, it provides a whitelist...
    matchedResult = defaultMatcher.matchesAny(url, host)
    if (matchedResult instanceof BlockingFilter) {
      return proxy
    } else if (matchedResult instanceof WhitelistFilter) {
      return direct
    }

    return direct
  }


  return {
    FindProxyForURL,
    proxyUrl: __PROXY__
  }
}

module.exports = {
  createPacClient
}
