import mustache from 'mustache'
import express, { response } from 'express'
import axios from 'axios'
import { SolrRecord } from './types/solrrecord'
import _ from 'lodash'
import bodyParser from 'body-parser'
import { v4 as uuidv4, v4 } from 'uuid'
import ss from 'simple-statistics'
import ip from 'ip'
import crypto from 'crypto'
import { spawn } from 'child_process'

// import fetch from 'node-fetch';

const stateMapping: Record<string, string> = {
    "Alabama": "AL",
    "Kentucky": "KY",
    "Ohio": "OH",
    "Alaska": "AK",
    "Louisiana": "LA",
    "Oklahoma": "OK",
    "Arizona": "AZ",
    "Maine": "ME",
    "Oregon": "OR",
    "Arkansas": "AR",
    "Maryland": "MD",
    "Pennsylvania": "PA",
    "American Samoa": "AS",
    "Massachusetts": "MA",
    "Puerto Rico": "PR",
    "California": "CA",
    "Michigan": "MI",
    "Rhode Island": "RI",
    "Colorado": "CO",
    "Minnesota": "MN",
    "South Carolina": "SC",
    "Connecticut": "CT",
    "Mississippi": "MS",
    "South Dakota": "SD",
    "Delaware": "DE",
    "Missouri": "MO",
    "Tennessee": "TN",
    "District of Columbia": "DC",
    "Montana": "MT",
    "Texas": "TX",
    "Florida": "FL",
    "Nebraska": "NE",
    "Trust Territories": "TT",
    "Georgia": "GA",
    "Nevada": "NV",
    "Utah": "UT",
    "Guam": "GU",
    "New Hampshire": "NH",
    "Vermont": "VT",
    "Hawaii": "HI",
    "New Jersey": "NJ",
    "Virginia": "VA",
    "Idaho": "ID",
    "New Mexico": "NM",
    "Virgin Islands": "VI",
    "Illinois": "IL",
    "New York": "NY",
    "Washington": "WA",
    "Indiana": "IN",
    "North Carolina": "NC",
    "West Virginia": "WV",
    "Iowa": "IA",
    "North Dakota": "ND",
    "Wisconsin": "WI",
    "Kansas": "KS",
    "Northern Mariana Islands": "MP",
    "Wyoming": "WY"
  }

const streetSuffixMapping: Record<string, string> = {
    "Alley": "aly",
    "Alleyway": "alyw",
    "Anex": "anx",
    "Annex": "anx",
    "Arcade": "arc",
    "Avenue": "ave",
    "Bayou": "byu",
    "Beach": "bch",
    "Bend": "bnd",
    "Bluff": "blf",
    "Bottom": "btm",
    "Boulevard": "blvd",
    "Branch": "br",
    "Bridge": "brg",
    "Brook": "brk",
    "Bypass": "byp",
    "Camp": "cp",
    "Canyon": "cyn",
    "Cape": "cpe",
    "Causeway": "cswy",
    "Center": "ctr",
    "Circle": "cir",
    "Cliff": "clf",
    "Club": "clb",
    "Common": "cmn",
    "Corner": "cor",
    "Course": "crse",
    "Court": "ct",
    "Courts": "cts",
    "Cove": "cv",
    "Creek": "crk",
    "Crescent": "cres",
    "Crest": "crst",
    "Crossing": "xing",
    "Crossroad": "xrd",
    "Curve": "curv",
    "Dale": "dl",
    "Dam": "dm",
    "Drive": "dr",
    "Estate": "est",
    "Expressway": "expy",
    "Extension": "ext",
    "Falls": "fls",
    "Ferry": "fry",
    "Field": "fld",
    "Flat": "flt",
    "Ford": "frd",
    "Forest": "frst",
    "Forge": "frg",
    "Fork": "frk",
    "Fort": "ft",
    "Freeway": "fwy",
    "Garden": "gdn",
    "Gateway": "gtwy",
    "Glen": "gln",
    "Green": "grn",
    "Grove": "grv",
    "Harbor": "hbr",
    "Haven": "hvn",
    "Heights": "hts",
    "Highway": "hwy",
    "Hill": "hl",
    "Hollow": "holw",
    "Inlet": "inlt",
    "Island": "is",
    "Junction": "jct",
    "Key": "ky",
    "Knoll": "knl",
    "Lake": "lk",
    "Landing": "lndg",
    "Lane": "ln",
    "Light": "lgt",
    "Loaf": "lf",
    "Lock": "lck",
    "Locks": "lcks",
    "Lodge": "ldg",
    "Loop": "loop",
    "Mall": "mall",
    "Manor": "mnr",
    "Meadow": "mdw",
    "Mews": "mews",
    "Mill": "ml",
    "Mission": "msn",
    "Motorway": "mtwy",
    "Mount": "mt",
    "Mountain": "mtn",
    "Neck": "nck",
    "Orchard": "orch",
    "Overpass": "opas",
    "Park": "park",
    "Parkway": "pkwy",
    "Pass": "pass",
    "Path": "path",
    "Pike": "pike",
    "Pine": "pne",
    "Place": "pl",
    "Plain": "pln",
    "Plaza": "plz",
    "Point": "pt",
    "Port": "prt",
    "Prairie": "pr",
    "Radial": "radl",
    "Ramp": "ramp",
    "Ranch": "rnch",
    "Rapid": "rpd",
    "Rest": "rst",
    "Ridge": "rdg",
    "River": "riv",
    "Road": "rd",
    "Route": "rte",
    "Row": "row",
    "Rue": "rue",
    "Run": "run",
    "Shore": "shr",
    "Skyway": "skwy",
    "Spring": "spg",
    "Springs": "spgs",
    "Spur": "spur",
    "Square": "sq",
    "Station": "sta",
    "Stravenue": "stra",
    "Stream": "strm",
    "Street": "st",
    "Summit": "smt",
    "Terrace": "ter",
    "Throughway": "trwy",
    "Trace": "trce",
    "Track": "trak",
    "Trafficway": "trfy",
    "Trail": "trl",
    "Trailer": "trlr",
    "Tunnel": "tunl",
    "Turnpike": "tpke",
    "Underpass": "upas",
    "Union": "un",
    "Valley": "vly",
    "Viaduct": "via",
    "View": "vw",
    "Village": "vlg",
    "Ville": "vl",
    "Vista": "vis",
    "Walk": "walk",
    "Way": "way",
    "Well": "wl",
    "Wells": "wls",
    "Wye": "wye"
  }

const app = express()
const port = 3000

const hlrToken = ''
const hlrSecret = ''
const basic = crypto.createHash('sha256').update(hlrToken + ':' + hlrSecret).digest('hex');

const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;

let handledCharges: string[] = []

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

// parse application/json
app.use(bodyParser.json())

const fs = require('fs')

const donations = JSON.parse(fs.readFileSync('templates/pages/donations.json', 'utf-8'))


// const mapTemplate = fs.readFileSync('templates/pages/map.html', 'utf-8')
const indexTemplate = fs.readFileSync('templates/pages/home.mustache', 'utf-8')
const recordByIdTemplate = fs.readFileSync('templates/pages/recordById.mustache', 'utf-8')
const recordsListingTemplate = fs.readFileSync('templates/pages/recordsListing.mustache', 'utf-8')
const donationsTemplate = fs.readFileSync('templates/pages/donations.html', 'utf-8')
const exportsTemplate = fs.readFileSync('templates/pages/exports.html', 'utf-8')

const visualizerTemplate = fs.readFileSync('templates/pages/visualizer.html', 'utf-8')


const servers = ["http://solr1:8983/solr/BigData/select"]


const blacklistedAutomatedIps: string[] = []
const whitelistedUserAgents: string[] = [
    // API Keys
]

// Stores that last time a query was made to a server for an IP address
const lastQueryTime: Record<string, number> = {}
const lastQueryTimes: Record<string, number[]> = {}
const numLookupsPerIP: Record<string, number> = {}
let hasRequestedWallet: string[] = []

function getRandServer() {
    var index = Math.floor(Math.random() * servers.length);
    return servers[index];
}

function getRandWalletsServer() {
    var index = Math.floor(Math.random() * servers.length);
    return servers[index].replace('BigData', 'Wallets');
}



async function queryForDocsSpatial(latLong: string, distanceKm: number) {
    const records: SolrRecord[] = []
    let numDocs = 0

    await axios.get(getRandServer(), {
        params: {
            q: "latLong:*",
            rows: 500,
            fq: "{!geofilt sfield=latLong}",
            pt: latLong,
            d: distanceKm
        }
    }).then(({ data }) => {
        records.push(...data.response.docs)
        numDocs += data.response.numFound
    })

    console.log(`Spatial query for ${latLong} returned ${numDocs} records.`)

    return {
        numDocs: numDocs,
        records: records
    }
}


async function getWalletBalance(wallet: string): Promise<number> {
    return axios.get(getRandWalletsServer(), {
        params: {
            q: `id:"${wallet}"`
        }
    })
        .then(({ data }) => {
            console.log(data.response.docs[0])
            if (data.response.docs[0]) {
                return data.response.docs[0].credits
            } else {
                return 0
            }
        }
        )
        .catch((err) => { console.log(err); return 0 })
}

async function addWalletBalance(wallet: string, credits: number) {
    if (typeof credits !== 'number') {
        console.log(`Credits is not a number: ${credits}`)
        throw new Error('Credits is not a number')
    }

    // Get the old balance
    const oldBalance = await getWalletBalance(wallet)

    console.log('old balance: ' + oldBalance + ' credits: ' + credits + ' wallet: ' + wallet + ' new balance: ' + (oldBalance + credits))
    // Add the new balance
    credits += oldBalance
    // Update the wallet
    return axios.post(getRandWalletsServer().replace('select', 'update'), {
        "add": {
            "doc": {
                // uuid v4
                "id": wallet,
                "credits": credits
            }
        }
    }, {
        params: {
            commit: true
        }
    }).then(() => {
        console.log(`Added ${credits} credits to wallet ${wallet}`)
        return true
    }) .catch ((err) => {
        console.log(err)
        return false
    })
}

async function removeWalletBalance(wallet: string, credits: number) {
    if (typeof credits !== 'number') {
        console.log(`Credits is not a number: ${credits}`)
        throw new Error('Credits is not a number')
    }

    // Get the old balance
    const oldBalance = await getWalletBalance(wallet)
    // Remove the new balance
    const newBalance = oldBalance - credits
    // Update the wallet
    return axios.post(getRandWalletsServer().replace('select', 'update'), {
        "add": {
            "doc": {
                // uuid v4
                "id": wallet,
                "credits": newBalance
            }
        }
    }, {
        params: {
            commit: true
        }
    }).then(() => {
        console.log(`Removed ${credits} credits to wallet ${wallet}`)
        return true
    })
}

async function queryForDocs(query: string, limit?: number, start?: number, sort?: string) {
    const records: SolrRecord[] = []
    let numDocs = 0

    await axios.get(getRandServer(), {
        params: {
            q: query,
            rows: limit || 100,
            sort: sort,
            start: start || 0
        }
    }).then(({ data }) => {
        records.push(...data.response.docs)
        numDocs += data.response.numFound
    })


    return {
        numDocs: numDocs,
        records: records
    }
}

async function queryForExportDocs(query: string, limit?: number, start?: number, sort?: string) {
    const records: SolrRecord[] = []
    let numDocs = 0

    await axios.get(getRandServer().replace('BigData', 'Exports'), {
        params: {
            q: query,
            rows: limit || 100,
            sort: sort,
            start: start || 0
        }
    }).then(({ data }) => {
        records.push(...data.response.docs)
        numDocs += data.response.numFound
    })

    return {
        numDocs: numDocs,
        records: records
    }
}

function buildQuery(requestedQuery: Record<string, string | string[]>, res: express.Response) {
    let query: string[] = []
    let orQuery: string[] = []
    let notQuery: string[] = []
    let additionalQuery: string[] = []


    let doAdditionalQuery = false


    if (requestedQuery.firstName != undefined) {
        if (typeof requestedQuery.firstName == 'string') {
            query.push(`firstName:${requestedQuery.firstName.replace(' ', '?')}`)
        }
    }
    if (requestedQuery.notfirstName != undefined) {
        if (typeof requestedQuery.notfirstName === 'string') {
            notQuery.push(`firstName:${requestedQuery.notfirstName.replace(' ', '?')}`)
        } else if (Array.isArray(requestedQuery.notfirstName)) {
            requestedQuery.notfirstName.forEach((notItem: any) => {
                notQuery.push(`firstName:${notItem.replace(' ', '?')}`)
            })
        }
    }

    if (requestedQuery.lastName != undefined) {
        if (typeof requestedQuery.lastName == 'string') {
            query.push(`lastName:${requestedQuery.lastName.replace(' ', '?')}`)
        }
    }
    if (requestedQuery.notlastName != undefined) {
        if (typeof requestedQuery.notlastName === 'string') {
            notQuery.push(`lastName:${requestedQuery.notlastName.replace(' ', '?')}`)
        } else if (Array.isArray(requestedQuery.notlastName)) {
            requestedQuery.notlastName.forEach((notItem: any) => {
                notQuery.push(`lastName:${notItem.replace(' ', '?')}`)
            })
        }
    }

    if (requestedQuery.birthYear != undefined) {
        if (typeof requestedQuery.birthYear == 'string') {
            query.push(`birthYear:${requestedQuery.birthYear}`)
        }
    }
    if (requestedQuery.notbirthYear != undefined) {
        if (typeof requestedQuery.notbirthYear === 'string') {
            notQuery.push(`birthYear:${requestedQuery.notbirthYear.replace(' ', '?')}`)
        } else if (Array.isArray(requestedQuery.notbirthYear)) {
            requestedQuery.notbirthYear.forEach((notItem: any) => {
                notQuery.push(`birthYear:${notItem.replace(' ', '?')}`)
            })
        }
    }

    if (requestedQuery.ips != undefined) {
        if (typeof requestedQuery.ips == 'string') {
            query.push(`ips:WRfKdFVogXnk82${requestedQuery.ips}WRfKdFVogXnk82`)
        }
    }
    if (requestedQuery.notips != undefined) {
        if (typeof requestedQuery.notips === 'string') {
            notQuery.push(`ips:WRfKdFVogXnk82${requestedQuery.notips}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notips)) {
            requestedQuery.notips.forEach((notItem: any) => {
                notQuery.push(`ips:WRfKdFVogXnk82${notItem}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.asn != undefined) {
        if (typeof requestedQuery.asn == 'string') {
            query.push(`asn:${requestedQuery.asn}`)
        }
    }
    if (requestedQuery.notasn != undefined) {
        if (typeof requestedQuery.notasn === 'string') {
            notQuery.push(`asn:${requestedQuery.notasn.replace(' ', '?')}`)
        } else if (Array.isArray(requestedQuery.notasn)) {
            requestedQuery.notasn.forEach((notItem: any) => {
                notQuery.push(`asn:${notItem.replace(' ', '?')}`)
            })
        }
    }

    if (requestedQuery.domain != undefined) {
        if (requestedQuery.exact) {
            if (typeof requestedQuery.domain == 'string') {
                query.push(`domain:WRfKdFVogXnk82${requestedQuery.domain.replace(' ', '?')}WRfKdFVogXnk82`)
            }
        } else {
            if (typeof requestedQuery.domain == 'string') {
                query.push(`domain:${requestedQuery.domain.replace(' ', '?')}`)
            }
        }

    }
    if (requestedQuery.notdomain != undefined) {
        if (typeof requestedQuery.notdomain === 'string') {
            notQuery.push(`domain:WRfKdFVogXnk82${requestedQuery.notdomain.replace(' ', '?')}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notdomain)) {
            requestedQuery.notdomain.forEach((notItem: any) => {
                notQuery.push(`domain:WRfKdFVogXnk82${notItem.replace(' ', '?')}WRfKdFVogXnk82`)
            })
        }
    }


    if (requestedQuery.asnOrg != undefined) {
        if (typeof requestedQuery.asnOrg == 'string') {
            query.push(`asnOrg:WRfKdFVogXnk82${requestedQuery.asnOrg}WRfKdFVogXnk82`)
        }
    }
    if (requestedQuery.notasnOrg != undefined) {
        if (typeof requestedQuery.notasnOrg === 'string') {
            notQuery.push(`asnOrg:${requestedQuery.notasnOrg.replace(' ', '?')}`)
        } else if (Array.isArray(requestedQuery.notasnOrg)) {
            requestedQuery.notasnOrg.forEach((notItem: any) => {
                notQuery.push(`asnOrg:WRfKdFVogXnk82${notItem.replace(' ', '?')}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.country != undefined) {
        if (typeof requestedQuery.country == 'string') {
            query.push(`country:WRfKdFVogXnk82${requestedQuery.country}WRfKdFVogXnk82`)
        }
    }
    if (requestedQuery.notcountry != undefined) {
        if (typeof requestedQuery.notcountry === 'string') {
            notQuery.push(`country:WRfKdFVogXnk82${requestedQuery.notcountry.replace(' ', '?')}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notcountry)) {
            requestedQuery.notcountry.forEach((notItem: any) => {
                notQuery.push(`country:WRfKdFVogXnk82${notItem.replace(' ', '?')}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.continent != undefined) {
        if (typeof requestedQuery.continent == 'string') {
            query.push(`continent:WRfKdFVogXnk82${requestedQuery.continent}WRfKdFVogXnk82`)
        }
    }
    if (requestedQuery.notcontinent != undefined) {
        if (typeof requestedQuery.notcontinent === 'string') {
            notQuery.push(`continent:WRfKdFVogXnk82${requestedQuery.notcontinent.replace(' ', '?')}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notcontinent)) {
            requestedQuery.notcontinent.forEach((notItem: any) => {
                notQuery.push(`continent:WRfKdFVogXnk82${notItem.replace(' ', '?')}WRfKdFVogXnk82`)
            })
        }
    }



    if (requestedQuery.firstName != undefined && requestedQuery.lastName != undefined && typeof requestedQuery.firstName == 'string' && typeof requestedQuery.lastName == 'string') {
        const firstName = requestedQuery.firstName.replace(' ', '?')
        const lastName = requestedQuery.lastName.replace(' ', '?')

        if (!requestedQuery.exact) {
            additionalQuery.push(`emails:${firstName}?${lastName}`)
            doAdditionalQuery = true
        }
    }

    if (requestedQuery.source != undefined) {
        if (typeof requestedQuery.source == 'string') {
            query.push(`source:WRfKdFVogXnk82${requestedQuery.source}WRfKdFVogXnk82`)
        }
    }
    if (requestedQuery.notsource != undefined) {
        if (typeof requestedQuery.notsource === 'string') {
            notQuery.push(`source:WRfKdFVogXnk82${requestedQuery.notsource.replace(' ', '?')}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notsource)) {
            requestedQuery.notsource.forEach((notItem: any) => {
                notQuery.push(`source:WRfKdFVogXnk82${notItem.replace(' ', '?')}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.emails != undefined) {
        if (typeof requestedQuery.emails == 'string') {

            // check if there's a * in the email
            if (requestedQuery.emails.indexOf('*') != -1) {
                query.push(`emails:${requestedQuery.emails.replace('@', '?')}`)
            }
            else {
                if (requestedQuery.exact) {
                    query.push(`emails:WRfKdFVogXnk82${requestedQuery.emails.replace('@', '?')}WRfKdFVogXnk82`)
                } else {
                    query.push(`emails:${requestedQuery.emails.replace('@', '?')}`)
                }

            }


            const mostCommonEmailDomains = [/\@gmail\..*/gi, /\@yahoo\..*/gi, /\@hotmail\..*/gi, /\@outlook\..*/gi, /\@aol\..*/gi, /\@icloud\..*/gi, /\@mail\..*/gi, /\@protonmail\..*/gi, /\@zoho\..*/gi, /\@msn\..*/gi, /\@yandex\..*/gi, /\@gmx\..*/gi, /\@live\..*/gi, /\@mail\.ru\..*/gi, /\@inbox\..*/gi, /\@ymail\..*/gi, /\@comcast\..*/gi, /\@verizon\..*/gi, /\@att\..*/gi, /\@sbcglobal\..*/gi, /\@cox\..*/gi, /\@earthlink\..*/gi, /\@charter\..*/gi, /\@optonline\..*/gi, /\@frontier\..*/gi, /\@windstream\..*/gi, /\@q\.com\..*/gi, /\@btinternet\..*/gi, /\@btconnect\..*/gi, /\@ntlworld\..*/gi, /\@bt\..*/gi, /\@virginmedia\..*/gi, /\@btopenworld\..*/gi, /\@talktalk\..*/gi, /\@sky\..*/gi, /\@orange\..*/gi, /\@bt\..*/gi, /\@virgin\..*/gi, /\@ntl\..*/gi, /\@freeserve\..*/gi, /\@blueyonder\..*/gi, /\@btinternet\..*/gi, /\@tiscali\..*/gi, /\@virgin\..*/gi, /\@tesco\..*/gi, /\@onetel\..*/gi, /\@bt\..*/gi, /\@virgin\..*/gi, /\@ntl\..*/gi, /\@freeserve\..*/gi, /\@blueyonder\..*/gi, /\@btinternet\..*/gi, /\@tiscali\..*/gi, /\@virgin\..*/gi, /\@tesco\..*/gi, /\@onetel\..*/gi, /\@bt\..*/gi, /\@virgin\..*/]
            const emailSplit = requestedQuery.emails.split('@')

            if (requestedQuery.emails.indexOf('*') == -1) {
                additionalQuery.push(`emails:WRfKdFVogXnk82${emailSplit[0]}WRfKdFVogXnk82`)
            }


            doAdditionalQuery = true;

            let isCommonDomain = false;

            for (const domain of mostCommonEmailDomains) {
                if (domain.test(requestedQuery.emails as string)) {
                    isCommonDomain = true
                }
            }

            if (!isCommonDomain) {
                if (emailSplit.length === 2) {
                    const domain = emailSplit[emailSplit.length - 1]
                    additionalQuery.push(`emails:${domain}`)
                    doAdditionalQuery = true
                }
            }
        }
    }

    if (requestedQuery.notemails != undefined) {
        if (typeof requestedQuery.notemails === 'string') {
            notQuery.push(`emails:WRfKdFVogXnk82${requestedQuery.notemails.replace('@', '?')}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notemails)) {
            requestedQuery.notemails.forEach((notItem: any) => {
                notQuery.push(`emails:WRfKdFVogXnk82${notItem.replace('@', '?')}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.VRN != undefined) {
        if (typeof requestedQuery.VRN == 'string') {
            query.push(`VRN:${requestedQuery.VRN.toLowerCase()}`)
        }
    }
    if (requestedQuery.notVRN != undefined) {
        if (typeof requestedQuery.notVRN === 'string') {
            notQuery.push(`VRN:${requestedQuery.notVRN.toLowerCase()}`)
        } else if (Array.isArray(requestedQuery.notVRN)) {
            requestedQuery.notVRN.forEach((notItem: any) => {
                notQuery.push(`VRN:${notItem.toLowerCase()}`)
            })
        }
    }

    if (requestedQuery.usernames != undefined) {
        if (requestedQuery.exact) {
            query.push(`usernames:WRfKdFVogXnk82${requestedQuery.usernames}WRfKdFVogXnk82`)
        } else {
            query.push(`usernames:${requestedQuery.usernames}`)
        }
    }

    if (requestedQuery.notusernames != undefined) {
        if (typeof requestedQuery.notusernames === 'string') {
            notQuery.push(`usernames:${requestedQuery.notusernames}`)
        } else if (Array.isArray(requestedQuery.notusernames)) {
            requestedQuery.notusernames.forEach((notItem: any) => {
                notQuery.push(`usernames:${notItem.toLowerCase()}`)
            })
        }
    }


    if (requestedQuery.address != undefined) {
        query.push(`address:WRfKdFVogXnk82${requestedQuery.address}WRfKdFVogXnk82`)
    }
    if (requestedQuery.notaddress != undefined) {
        if (typeof requestedQuery.notaddress === 'string') {
            notQuery.push(`address:WRfKdFVogXnk82${requestedQuery.notaddress}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notaddress)) {
            requestedQuery.notaddress.forEach((notItem: any) => {
                notQuery.push(`address:WRfKdFVogXnk82${notItem}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.city != undefined) {
        query.push(`city:${requestedQuery.city}`)
    }

    if (requestedQuery.notcity != undefined) {
        if (typeof requestedQuery.notcity === 'string') {
            notQuery.push(`city:WRfKdFVogXnk82${requestedQuery.notcity}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notcity)) {
            requestedQuery.notcity.forEach((notItem: any) => {
                notQuery.push(`city:WRfKdFVogXnk82${notItem}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.zipCode != undefined) {
        query.push(`zipCode:${requestedQuery.zipCode}`)
    }
    if (requestedQuery.notzipCode != undefined) {
        if (typeof requestedQuery.notzipCode === 'string') {
            notQuery.push(`address:WRfKdFVogXnk82${requestedQuery.notzipCode}WRfKdFVogXnk82`)
        } else if (Array.isArray(requestedQuery.notzipCode)) {
            requestedQuery.notzipCode.forEach((notItem: any) => {
                notQuery.push(`address:WRfKdFVogXnk82${notItem}WRfKdFVogXnk82`)
            })
        }
    }

    if (requestedQuery.state != undefined) {
        query.push(`state:${requestedQuery.state}`)
    }
    if (requestedQuery.notstate != undefined) {
        if (typeof requestedQuery.notstate === 'string') {
            notQuery.push(`state:${requestedQuery.notstate}`)
        } else if (Array.isArray(requestedQuery.notstate)) {
            requestedQuery.notstate.forEach((notItem: any) => {
                notQuery.push(`state:${notItem}`)
            })
        }
    }


    if (requestedQuery.phoneNumbers != undefined && typeof requestedQuery.phoneNumbers == 'string') {

        if (!requestedQuery.exact) {
            query.push(`phoneNumbers:WRfKdFVogXnk82${requestedQuery.phoneNumbers.replace(/\D+/g, "")}WRfKdFVogXnk82`)
        } else {
            // Replace all non-digits or non-question marks with 
            query.push(`phoneNumbers:WRfKdFVogXnk82${requestedQuery.phoneNumbers.replace(/\D+/g, "")}WRfKdFVogXnk82`)
            additionalQuery.push(`phoneNumbers:1${requestedQuery.phoneNumbers.replace(/\D+/g, "")}`)
            additionalQuery.push(`phoneNumbers:7${requestedQuery.phoneNumbers.replace(/\D+/g, "")}`)
            doAdditionalQuery = true;
        }

    }

    if (requestedQuery.notphoneNumbers != undefined) {
        if (typeof requestedQuery.notphoneNumbers === 'string') {
            notQuery.push(`phoneNumbers:${requestedQuery.notphoneNumbers}`)
        } else if (Array.isArray(requestedQuery.notphoneNumbers)) {
            requestedQuery.notphoneNumbers.forEach((notItem: any) => {
                notQuery.push(`phoneNumbers:${notItem}`)
            })
        }
    }

    if (requestedQuery.passwords != undefined && typeof requestedQuery.passwords === 'string') {
        query.push(`passwords:${requestedQuery.passwords}`)
    } else if (requestedQuery.passwords != undefined && Array.isArray(requestedQuery.passwords)) {
        requestedQuery.passwords.forEach((password) => {
            orQuery.push(`passwords:WRfKdFVogXnk82${password}WRfKdFVogXnk82`)
        })
    }

    if (requestedQuery.notpasswords != undefined) {
        if (typeof requestedQuery.notpasswords === 'string') {
            notQuery.push(`passwords:${requestedQuery.notpasswords}`)
        } else if (Array.isArray(requestedQuery.notpasswords)) {
            requestedQuery.notpasswords.forEach((notItem: any) => {
                notQuery.push(`passwords:${notItem}`)
            })
        }
    }

    if (requestedQuery.vin != undefined && typeof requestedQuery.vin === 'string') {
        query.push(`vin:${requestedQuery.vin}`)
    } else if (requestedQuery.vin != undefined && Array.isArray(requestedQuery.vin)) {
        requestedQuery.vin.forEach((vin) => {
            orQuery.push(`vin:WRfKdFVogXnk82${vin}WRfKdFVogXnk82`)
        })
    }



    let queryBuilt = sanitizeQuery(query.join(' AND '))

    if (orQuery.length > 0) {
        if (query.length > 0) {
            queryBuilt += ' OR ' + sanitizeQuery(orQuery.join(' OR '))
        } else {
            queryBuilt += sanitizeQuery(orQuery.join(' OR '))
        }
    }

    if (notQuery.length > 0) {
        if (query.length > 0) {
            queryBuilt += ' NOT ' + sanitizeQuery(notQuery.join(' NOT '))
        } else {
            return res.status(400).send("Error: you sent an negative query without a regular query! Silly goose.")
        }
    }

    return {
        query: queryBuilt,
        additionalQuery: additionalQuery,
        doAdditionalQuery: doAdditionalQuery
    }
}

async function getSimilarRecords(record: SolrRecord) {
    let responses: any[] = []

    const scoresList: Record<string, number> = {}

    const relatedDocIDs: string[] = []

    await Promise.all([
        ...(record.emails || []).map((email) => {
            return axios.get(getRandServer(), {
                params: {
                    q: `emails:"${email.replace(/\"/gi, '').replace(/\//gi, '')}"`,
                    rows: 20,
                    fl: 'id'
                }
            }).then(({ data }) => {
                data.response.docs.forEach((doc: SolrRecord) => {
                    if (doc.id === record.id) {
                        return
                    }
                    if (!(doc.id in scoresList)) {
                        scoresList[doc.id] = 15
                    } else {
                        scoresList[doc.id] += 5
                    }
                    relatedDocIDs.push(doc.id)
                })
            })
        }),
        ...(record.usernames || []).map((email) => {
            return axios.get(getRandServer(), {
                params: {
                    q: `usernames:"${email.replace(/\"/gi, '').replace(/\//gi, '')}"`,
                    rows: 20,
                    fl: 'id'
                }
            }).then(({ data }) => {
                data.response.docs.forEach((doc: SolrRecord) => {
                    if (doc.id === record.id) {
                        return
                    }
                    if (!(doc.id in scoresList)) {
                        scoresList[doc.id] = 15
                    } else {
                        scoresList[doc.id] += 5
                    }
                    relatedDocIDs.push(doc.id)
                })
            })
        }),
        axios.get(getRandServer(), {
            params: {
                q: `id:${record.id}`,
                mlt: true,
                'mlt.fl': 'emails',
                'mlt.mindf': 2,
                'mlt.mintf': 2,
            }
        }).then(({ data }) => {
            data.moreLikeThis.forEach((doc: any) => {
                if (typeof doc == 'object') {
                    doc.docs.forEach((relatedDoc: any) => {
                        if (scoresList[relatedDoc.id]) {
                            scoresList[relatedDoc.id] += relatedDoc.score
                        } else {
                            scoresList[relatedDoc.id] = relatedDoc.score

                        }
                    })
                }
            })
            responses.push(...data.moreLikeThis)
        }),
        axios.get(getRandServer(), {
            params: {
                q: `id:${record.id}`,
                mlt: true,
                'mlt.fl': 'usernames',
                'mlt.mindf': 1,
                'mlt.mintf': 1,
                'mlt.match.include': true,
            }
        }).then(({ data }) => {
            data.moreLikeThis.forEach((doc: any) => {
                if (typeof doc == 'object') {
                    doc.docs.forEach((relatedDoc: any) => {
                        if (scoresList[relatedDoc.id]) {
                            scoresList[relatedDoc.id] += relatedDoc.score
                        } else {
                            scoresList[relatedDoc.id] = relatedDoc.score

                        }
                    })
                }
            })
            responses.push(...data.moreLikeThis)
        }),
        axios.get(getRandServer(), {
            params: {
                q: `id:${record.id}`,
                mlt: true,
                'mlt.fl': 'phoneNumbers',
                'mlt.mindf': 1,
                'mlt.mintf': 1,
                'mlt.match.include': true,
            }
        }).then(({ data }) => {
            data.moreLikeThis.forEach((doc: any) => {
                if (typeof doc == 'object') {
                    doc.docs.forEach((relatedDoc: any) => {
                        if (scoresList[relatedDoc.id]) {
                            scoresList[relatedDoc.id] += relatedDoc.score
                        } else {
                            scoresList[relatedDoc.id] = relatedDoc.score

                        }
                    })
                }
            })
            responses.push(...data.moreLikeThis)
        }),
        axios.get(getRandServer(), {
            params: {
                q: `id:${record.id}`,
                mlt: true,
                'mlt.fl': 'address_search',
                'mlt.mindf': 1,
                'mlt.mintf': 1,
                'mlt.match.include': true,
            }
        }).then(({ data }) => {
            data.moreLikeThis.forEach((doc: any) => {
                if (typeof doc == 'object') {
                    doc.docs.forEach((relatedDoc: any) => {
                        if (scoresList[relatedDoc.id]) {
                            scoresList[relatedDoc.id] += relatedDoc.score
                        } else {
                            scoresList[relatedDoc.id] = relatedDoc.score

                        }
                    })
                }
            })
            responses.push(...data.moreLikeThis)
        }),
        axios.get(getRandServer(), {
            params: {
                q: `id:${record.id}`,
                mlt: true,
                'mlt.fl': 'firstName,lastName',
                'mlt.mindf': 1,
                'mlt.mintf': 1,
                'mlt.match.include': true,
            }
        }).then(({ data }) => {
            data.moreLikeThis.forEach((doc: any) => {
                if (typeof doc == 'object') {
                    doc.docs.forEach((relatedDoc: any) => {
                        if (scoresList[relatedDoc.id]) {
                            scoresList[relatedDoc.id] += relatedDoc.score
                        } else {
                            scoresList[relatedDoc.id] = relatedDoc.score
                        }
                    })
                }
            })
            responses.push(...data.moreLikeThis)
        }),
    ])


    responses.forEach((response) => {
        if (typeof response == 'object') {
            relatedDocIDs.push(...(response.docs.map((doc: any) => doc.id)))
        }
    })

    let docs: SolrRecord[] = []

    // Get unique doc IDs
    const uniqueDocIDs = [...new Set(relatedDocIDs)]



    await Promise.all(uniqueDocIDs.map((doc: any) => queryForDocs(`id:${doc}`))).then((results) => {
        results.forEach((result) => {
            docs.push(...result.records.map((result) => {
                return {
                    ...result,
                    'similarity score': scoresList[result.id] || 0,
                }
            }))
        })
    })

    // Sort the docs by score
    docs = docs.sort((a, b) => {
        if (scoresList[a.id] > scoresList[b.id]) {
            return -1
        } else if (scoresList[a.id] < scoresList[b.id]) {
            return 1
        } else {
            return 0
        }
    })

    return docs
}

function sanitizeQuery(query: string) {
    //return query;
    return query.replace(/[^\w\s\$\:\.\@\-\*\а-яА-ЯёЁ]/gi, '?').replace(/WRfKdFVogXnk82/g, '"')
}

app.get('/exports', async (req, res) => {
    const rendered = mustache.render(exportsTemplate, {})
    return res.send(rendered)
})

app.get('/exports/:walletid', async (req, res) => {
    try {
        const walletid = req.params.walletid
        // Ensure the wallet ID is valid
        if (!uuidRegex.test(walletid)) {
            return res.status(400).json({
                error: 'Invalid wallet ID.'
            })
        }
        const records = await queryForExportDocs(`wallet:"${walletid}"`)

        return res.json(records.records.map((record: any) => {
            return {
                ...record,
                jobid: record.id,
                exportCount: record.count,
            }
        }))
    } catch (e) {
        return res.status(500).json([
            {
                id: 'internal error',
                query: 'sorry bout that... try again?'
            }
        ])
    }
    
})

app.get('/', async (req, res) => {
    const rendered = mustache.render(indexTemplate, { count: "14,491,682,918" })

    return res.send(rendered)
})


app.get('/visualize', async (req, res) => {
    const rendered = mustache.render(visualizerTemplate, {})

    return res.send(rendered)
})

app.get('/map', async (req, res) => {
    res.sendFile(__dirname + '/static/map.html')
})

app.get('/favicon.ico', (req, res) => {
    // Read the favicon file and send it to the client
    res.sendFile(__dirname + '/static/favicon.ico')
})

app.get('/robots.txt', (req, res) => {
    // Read robots.txt and send it to the client
    res.sendFile(__dirname + '/static/robots.txt')
})

app.get('/dear_peter', (req, res) => {
    res.sendFile(__dirname + '/static/dear_peter.html')
})

app.get('/terms', (req, res) => {
    res.sendFile(__dirname + '/static/terms.html')
})

app.get('/faq', (req, res) => {
    res.sendFile(__dirname + '/static/faq.html')
})

app.get('/canary', (req, res) => {
    res.sendFile(__dirname + '/static/canary.html')
})

app.get('/donations', (req, res) => {
    // Render out the donations page tempate
    const rendered = mustache.render(donationsTemplate, {
        donations: donations.sort((a: any, b: any) => b.amount - a.amount)
    })

    return res.send(rendered)
})

app.post('/export', async (req, res) => {
    try {
        // Read body
        let body = req.body

        // Check if the body is valid
        if (!body || !body.walletId || !body.exportCount) {
            return res.status(400).send('Invalid body')
        }

        // Make sure export count is > 0
        if (typeof body.exportCount !== 'number') {
            // Set it to be a number
            body.exportCount = parseInt(body.exportCount, 10)
        }
        if (body.exportCount <= 0) {
            return res.status(400).send('Invalid export count')
        }

        // If the wallet ID is invalid uuid4
        if (!uuidRegex.test(body.walletId)) {
            return res.status(400).send('Invalid wallet ID')
        }

        // Get credits for the wallet
        const credits = await getWalletBalance(body.walletId)

        const cost = (body.exportCount - 100) / 10

        if (cost > credits) {
            return res.status(400).send('Insufficient credits')
        }

        // if cost < 0
        if (cost < 0) {
            return res.status(400).send('Invalid export count')
        }


        let requestedQuery: Record<string, any> = {}

        for (const [key, value] of Object.entries(req.query)) {
            if (key !== 'wt') {
                if (typeof value === 'string' || Array.isArray(value)) {
                    requestedQuery[key] = value
                } else {
                    console.log(`Invalid query: ${key} is not a string or array. ${value}`)
                }
            }
        }

        const queryBuilt = buildQuery(requestedQuery, res as any) as any

        const jobid = uuidv4()

        if (!queryBuilt.query || !queryBuilt.additionalQuery || typeof queryBuilt.doAdditionalQuery !== 'boolean') {
            return res.status(400).send('Invalid query')
        }

        let finalQuery = queryBuilt.query

        if (queryBuilt.additionalQuery.length > 0) {
            finalQuery = `(${finalQuery}) OR ${sanitizeQuery(queryBuilt.additionalQuery.join(' OR '))}`
        }

        const payload = {
            "status": "started",
            "jobid": jobid,
            "cost": cost,
            "query": finalQuery,
            "additionalQuery": queryBuilt.additionalQuery,
            "doAdditionalQuery": queryBuilt.doAdditionalQuery,
            "exportCount": body.exportCount,
            "success": true,
            "walletId": body.walletId
        }

        // Return to client that the job has been started
        res.json(payload)

        // End the response
        res.end()


        // Deduct clients from wallet
        await removeWalletBalance(body.walletId, cost)

        // Post to wallet DB
        await axios.post(getRandServer().replace('BigData', 'Exports').replace('select', 'update'), {
            "add": {
                "doc": {
                    // uuid v4
                    "id": payload.jobid,
                    "cost": payload.cost,
                    "wallet": payload.walletId,
                    "query": payload.query,
                    "status": payload.status,
                    'count': payload.exportCount,
                }
            }
        }, {
            params: {
                commit: true
            }
        })

        // Base64 encode payload
        const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64')

        // asyncronously run python3 exports/doExport.py <encodedPayload>
        const pythonProcess = spawn('python3', ['exports/doExport.py', encodedPayload])

        // Listen for data from the python process
        pythonProcess.stdout.on('data', (data) => {
            console.log(`Export Job ${jobid}: ${data}`);
        })

        // Listen for errors from the python process
        pythonProcess.stderr.on('data', (data) => {
            console.error(`Export Job ${jobid}: ${data}`);
        })
    } catch (e) {
        console.error(e)
        res.status(400).send('stoooop')
    }
})

app.post('/exports/callbacks/a-unique-id/exportCb', async (req, res) => {
    // Read body
    const body = req.body


    if (body.status === 'failed') {
        // Re-credit wallet
        await addWalletBalance(body.walletId, body.cost)
    }

    // Post to db
    await axios.post(getRandServer().replace('BigData', 'Exports').replace('select', 'update'), {
        "add": {
            "doc": {
                // uuid v4
                "id": body.jobid,
                "cost": body.cost,
                "wallet": body.walletId,
                "query": body.query,
                "status": body.status,
                'count': body.exportCount,
                "link": body.link
            }
        }
    }, {
        params: {
            commit: true
        }
    })

    return res.status(200)
})


app.get('/wallet/:walletId', async (req, res) => {
    try {
        const walletId = req.params.walletId


        if (!uuidRegex.test(walletId)) {
            console.log('bad wallet id regex')
            return res.status(400).json({ "error": "bad wallet id" })
        }

        if (typeof walletId !== 'string') {
            return res.status(400).json({ "error": "bad request" })
        }

        if (walletId.length !== 36) {
            console.log('bad wallet id length')
            return res.status(400).json({ "error": "bad wallet id" })
        }

        let balance = await getWalletBalance(walletId)

        return res.json({ "credits": balance })
    } catch {
        return res.status(400).json({ "error": "stooppp" })
    }
})


function isAutomated(req: any) {
    // Check if ip is in blacklist
    const connectingIp = req.headers['cf-connecting-ip'] || req.connection.remoteAddress;
    if (blacklistedAutomatedIps.includes(connectingIp)) {
        console.log(`BLACKLISTED IP ${connectingIp}`)
        return true
    }


    // Check if user agent has axios in it
    const userAgent = req.headers['user-agent']

    if (whitelistedUserAgents.some((whitelistedAgent) => userAgent.includes(whitelistedAgent))) {
        return false
    }

    if (userAgent != undefined) {
        if (typeof userAgent == 'string') {
            if (Object.keys(req.query).length === 1 && req.query.emails) {
                // Check if the user has gotten their wallet
                const connectingIp = req.headers['cf-connecting-ip']

                if (typeof connectingIp === 'string') {
                    if (hasRequestedWallet.includes(connectingIp)) {
                        return false
                    }
                }
            }
        }
    }
    return false
}

async function checkIPAutomatedSTDDEV(req: any) {
    const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress;

    const userAgent = req.headers['user-agent']

    if (whitelistedUserAgents.some((whitelistedAgent) => userAgent.includes(whitelistedAgent))) {
        return false
    }

    if (ip === '0.0.0.0' || !ip) {
        // Hello, KT :3c
        console.log('wat r u doin')
        return true
    } else if (typeof ip === 'string') {

        if (numLookupsPerIP[ip] === undefined) {
            numLookupsPerIP[ip] = 0
        }

        // If no last query time
        if (lastQueryTime[ip] === undefined) {
            lastQueryTime[ip] = Date.now()
            return false
        } else {
            // Get the time between the last query and now
            const timeSinceLastQuery = (Date.now() - lastQueryTime[ip])
            console.log(`Time since last query for ${ip}: ${timeSinceLastQuery}`)

            numLookupsPerIP[ip] += 1

            console.log(ip + ' has ' + numLookupsPerIP[ip] + ' lookups.')

            if (numLookupsPerIP[ip] > 200) {
                blacklistedAutomatedIps.push(ip)
                return true
            }


            // Add current request to lookup table
            if (lastQueryTimes[ip] === undefined) {
                lastQueryTimes[ip] = []
            }

            // If there's less than 40 values in the array, add the current time
            if (lastQueryTimes[ip].length < 40) {
                lastQueryTimes[ip].push(timeSinceLastQuery)
            }
            // If there's more than 40 values in the array, remove the first value and add the current time
            else if (lastQueryTimes[ip].length >= 40) {
                lastQueryTimes[ip].shift()
                lastQueryTimes[ip].push(timeSinceLastQuery)
            }

            // If there's more than 20 values in the array, calculate the standard deviation between the values
            if (lastQueryTimes[ip].length >= 20) {
                const standardDeviation = ss.standardDeviation(lastQueryTimes[ip])
                console.log(`Standard deviation for ${ip}: ${standardDeviation}`)
                // If the standard deviation is less than 1000, the user is probably a bot
                if (standardDeviation < 1800) {
                    console.log('erm haht the duence')
                    blacklistedAutomatedIps.push(ip)
                    return true
                }
            }

            // If all of the requests have less than 2000 ms of delay:
            if (lastQueryTimes[ip].length >= 20) {
                if (lastQueryTimes[ip].every((request) => request < 5000)) {
                    blacklistedAutomatedIps.push(ip)
                    return true
                }
            }


            lastQueryTime[ip] = Date.now()
            return false;
        }

    } else {
        console.log('erm haht the duence')
        return true
    }
    return false
}

async function doAutomatedRes(res: any, json?: boolean) {
    if (json) {
        return res.json({
            resultCount: 69420,
            count: 69420,
            records: [
                {
                    id: uuidv4(),
                    firstName: '[[EXTREMELY LOUD INCORRECT BUZZER]]',
                    lastName: 'Automated scraping detected. Please contact miyakoyakota@riseup.com to be whitelisted.',
                    email: 'This data comes from https://search.illicit.services and it is free to use. Do not pay for access to this data.',
                }
            ]
        })
    }
    else {

        const rendered = mustache.render(recordsListingTemplate, {
            resultCount: 69420,
            count: 69420,
            records: [
                {
                    id: uuidv4(),
                    firstName: '[[EXTREMELY LOUD INCORRECT BUZZER]]',
                    fields: [
                        "firstName: [[EXTREMELY LOUD INCORRECT BUZZER]]",
                        "lastName: Automated scraping detected. Please contact miyakoyakota@riseup.com to be whitelisted. If you would like to use this site as an API, you may add ?wt=json to return JSON.",
                        "email: This data comes from https://search.illicit.services and it is free to use. Do not pay for access to this data.",
                    ]
                }
            ]
        })

        return res.send(rendered)
    }


}

app.get('/documents/by_id/:id', async (req, res) => {
    if (isAutomated(req)) {
        return doAutomatedRes(res, req.query.wt === 'json')
    }

    if (await checkIPAutomatedSTDDEV(req)) {
        return doAutomatedRes(res, req.query.wt === 'json')
    }

    const records = await queryForDocs(`id:${req.params.id}`)
    const record = records.records[0]

    if (record === undefined) {
        return res.status(404).send('No record found.')
    }

    // If wt=json
    if (req.query.wt == 'json') {
        // check moreLikeThis 

        if (req.query.moreLikeThis == 'true') {
            const similarRecords = await getSimilarRecords(record).catch((err) => {
                console.log(err)
                return []
            })

            return res.json({
                record: record,
                related: similarRecords
            })
        } else {
            return res.json({
                record: record
            })
        }
    }

    const similarRecords = await getSimilarRecords(record).catch((err) => {
        return []
    })



    const rendered = mustache.render(recordByIdTemplate, {
        id: record.id,
        record: Object.entries(_.omit(record, ['id', '_version_'])).map(([key, value]) => {
            return {
                key: key,
                value: value
            }
        }),
        related: similarRecords.map((record) => {
            return {
                id: record.id,
                record: Object.entries(_.omit(record, ['id', '_version_'])).map(([key, value]) => {
                    return {
                        key: key,
                        value: value
                    }
                })
            }
        })
    })

    return res.send(rendered)
})

function makeid(length: number) {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    let counter = 0;
    while (counter < length) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
        counter += 1;
    }
    return result;
}

app.get('/spatial', async (req, res) => {
    if (req.query.latLong != undefined && typeof req.query.latLong === 'string') {
        // Validate latLong using regex
        const latLongRegex = /^-?\d{1,3}(?:\.\d{1,20})?,-?\d{1,3}(?:\.\d{1,20})?$/;

        // Validate d within 0.1 and 1000
        if (typeof req.query.d !== 'string' && req.query.d !== undefined) {
            return res.send({
                error: true,
                errorMessage: 'Please provide a valid d between 0.1 and 1000'
            })
        }
        const d = parseFloat(req.query.d || '0.2')

        if (d < 0.1 || d > 1000) {
            return res.send({
                error: true,
                errorMessage: 'Please provide a valid d between 0.1 and 1000'
            })
        }

        if (latLongRegex.test(req.query.latLong)) {
            let recordsResponse = await queryForDocsSpatial(req.query.latLong, d).catch((err) => {
                return {
                    numDocs: 0,
                    records: [] as SolrRecord[]
                }
            })

            res.json(recordsResponse)

        } else {
            // latLong is not valid
            return res.send({
                error: true,
                errorMessage: 'Please only send coordinates as lat,long'
            })
        }
    } else {
        res.json({
            error: true,
            errorMessage: "Provide a latLong! Dumbo."
        })
    }
})

app.get('/records', async (req, res) => {
    try {
        const userAgent = req.headers['user-agent']

        if (!(userAgent === 'yeayeyayaeyayeayeyaeyeayyaeyeyaeyae')) {
            if (isAutomated(req)) {
                return doAutomatedRes(res, req.query.wt === 'json')
            }
        }

        if (req.query.wt === 'json') {
            // Check if the user-agent is in the list of allowed user-agents
            const apiKey = req.query.apikey || req.headers['user-agent']

            if (typeof apiKey === 'string') {
                if (whitelistedUserAgents.includes(apiKey)) {
                    // User agent is allowed
                    // Continue
                } else {
                    console.log(`IP ${req.headers['cf-connecting-ip'] || req.connection.remoteAddress} tried to access the API with user-agent ${apiKey}`)
                    // User agent is not allowed
                    res.status(403)
                    return res.json({
                        error: true,
                        message: 'API Access requires a free API token. Please contact miyakoyakota@riseup.com to get one.'
                    })
                }
            } else {
                return res.json({ error: true, message: 'API Access requires a free API token. Please contact miyakoyakota@riseup.com to get one.' })
            }
        }

        let requestedQuery: Record<string, any> = {}

        for (const [key, value] of Object.entries(req.query)) {
            if (key !== 'wt') {
                if (typeof value === 'string' || Array.isArray(value)) {
                    requestedQuery[key] = value
                } else {
                    console.log(`Invalid query: ${key} is not a string or array. ${value}`)
                }
            }
        }

        const queryBuilt = buildQuery(requestedQuery, res as any) as any

        if (!Boolean(queryBuilt.query)) {
            return res.status(400).send('no query!!!')
        }

        // Check if type is express.Response
        if (queryBuilt === undefined) {
            return res.status(400).send('no query!!!')
        }

        if (!queryBuilt.query || !queryBuilt.additionalQuery || typeof queryBuilt.doAdditionalQuery !== 'boolean') {
            return res.status(400).send('no query!!!')
        }

        let totalRecordCount = 0
        let recordsResponse: any = null

        recordsResponse = await queryForDocs(queryBuilt.query).catch((err) => {
            console.log(err)
            return {
                numDocs: 0,
                records: [] as SolrRecord[]
            }
        })

        totalRecordCount += recordsResponse.numDocs

        let records = recordsResponse.records

        if (queryBuilt.doAdditionalQuery && !req.query.exact && req.query.exact != 'true' && req.query.exact != '1' && req.query.exact != 'yes' && req.query.exact != 'y' && req.query.exact != 'on' && req.query.exact != 't' && req.query.exact != 'sure' && req.query.exact != 'please' && req.query.exact != 'True') {
            const additionalQueryBuilt = sanitizeQuery(queryBuilt.additionalQuery.join(' OR '))

            let additionalRecordsResponse: any = null

            if (typeof req.query.sofreshandsoclean === 'string') {
                additionalRecordsResponse = await queryForDocs(additionalQueryBuilt, 10000).catch((err) => {
                    return {
                        numDocs: 0,
                        records: [] as SolrRecord[]
                    }
                })
            } else {
                additionalRecordsResponse = await queryForDocs(additionalQueryBuilt).catch((err) => {
                    return {
                        numDocs: 0,
                        records: [] as SolrRecord[]
                    }
                })
            }

            totalRecordCount += additionalRecordsResponse.numDocs

            const additionalRecords = additionalRecordsResponse.records

            records.push(...additionalRecords)
        }

        let recordsFinal = records.map((record: any) => {
            return {
                ...record,
                canMap: Boolean(record.address) || Boolean(record.latLong),
                fields: Object.keys(_.omit(record, ['id', '_version_'])).map((key) => {
                    return `${key}: ${(record as any)[key]}`
                })
            }
        })

        // Remove duplicates
        recordsFinal = _.uniqBy(recordsFinal, 'id')

        // If wt=json is set return as json
        if (req.query.wt == 'json') {
            return res.json({
                resultCount: totalRecordCount,
                count: records.length,
                records: recordsFinal
            })
        } else {
            const rendered = mustache.render(recordsListingTemplate, {
                resultCount: totalRecordCount,
                count: records.length,
                records: recordsFinal,
                finalquery: queryBuilt.query
            })

            return res.send(rendered)
        }
    } catch (e) {
        res.status(500)
    }
})

app.listen(port, '0.0.0.0', () => {
    console.log(`Example app listening on port ${port}`)
}) 
