text: const cache = require('./cache')
  const parse = require('url').parse
  const crypto = require('./crypto')
  const request = require('./request')
  const match = require('./provider/match')
  const querystring = require('querystring')
  
  const hook = {
  	request: {
  		before: () => {},
  		after: () => {},
  	},
  	connect: {
  		before: () => {}
  	},
  	negotiate: {
  		before: () => {}
  	},
  	target: {
  		host: new Set(),
  		path: new Set()
  	}
  }
  
  hook.target.host = new Set([
  	'music.163.com',
  	'interface.music.163.com',
  	'interface3.music.163.com',
  	'apm.music.163.com',
  	'apm3.music.163.com',
  	// 'mam.netease.com',
  	// 'api.iplay.163.com', // look living
  	// 'ac.dun.163yun.com',
  	// 'crash.163.com',
  	// 'clientlog.music.163.com',
  	// 'clientlog3.music.163.com'
  ])
  
  hook.target.path = new Set([
  	'/api/v3/playlist/detail',
  	'/api/v3/song/detail',
  	'/api/v6/playlist/detail',
  	'/api/album/play',
  	'/api/artist/privilege',
  	'/api/album/privilege',
  	'/api/v1/artist',
  	'/api/v1/artist/songs',
  	'/api/artist/top/song',
  	'/api/v1/album',
  	'/api/album/v3/detail',
  	'/api/playlist/privilege',
  	'/api/song/enhance/player/url',
  	'/api/song/enhance/player/url/v1',
  	'/api/song/enhance/download/url',
  	'/api/song/enhance/privilege',
  	'/batch',
  	'/api/batch',
  	'/api/v1/search/get',
  	'/api/v1/search/song/get',
  	'/api/cloudsearch/pc',
  	'/api/v1/playlist/manipulate/tracks',
  	'/api/song/like',
  	'/api/v1/play/record',
  	'/api/playlist/v4/detail',
  	'/api/v1/radio/get',
  	'/api/v1/discovery/recommend/songs'
  ])
  
  hook.request.before = ctx => {
  	const {req} = ctx
  	req.url = (req.url.startsWith('http://') ? '' : (req.socket.encrypted ? 'https:' : 'http:') + '//' + (['music.163.com', 'music.126.net'].some(domain => (req.headers.host || '').endsWith(domain)) ? req.headers.host : null)) + req.url
  	const url = parse(req.url)
  	if ([url.hostname, req.headers.host].some(host => host.includes('music.163.com'))) ctx.decision = 'proxy'
  	if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host)) && req.method == 'POST' && (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/'))) {
  		return request.read(req)
  		.then(body => req.body = body)
  		.then(body => {
  			if ('x-napm-retry' in req.headers) delete req.headers['x-napm-retry']
  			req.headers['X-Real-IP'] = '118.88.88.88'
  			if (req.url.includes('stream')) return // look living eapi can not be decrypted
  			if (body) {
  				let data = null
  				const netease = {}
  				netease.pad = (body.match(/%0+$/) || [''])[0]
  				netease.forward = (url.path == '/api/linux/forward')
  				if (netease.forward) {
  					data = JSON.parse(crypto.linuxapi.decrypt(Buffer.from(body.slice(8, body.length - netease.pad.length), 'hex')).toString())
  					netease.path = parse(data.url).path
  					netease.param = data.params
  				}
  				else {
  					data = crypto.eapi.decrypt(Buffer.from(body.slice(7, body.length - netease.pad.length), 'hex')).toString().split('-36cd479b6b5-')
  					netease.path = data[0]
  					netease.param = JSON.parse(data[1])
  				}
  				netease.path = netease.path.replace(/\/\d*$/, '')
  				ctx.netease = netease
  				// console.log(netease.path, netease.param)
  
  				if (netease.path == '/api/song/enhance/download/url')
  					return pretendPlay(ctx)
  			}
  		})
  		.catch(error => console.log(error, req.url))
  	}
  	else if ((hook.target.host.has(url.hostname)) && (url.path.startsWith('/weapi/') || url.path.startsWith('/api/'))) {
  		req.headers['X-Real-IP'] = '118.88.88.88'
  		ctx.netease = {web: true, path: url.path.replace(/^\/weapi\//, '/api/').replace(/\?.+$/, '').replace(/\/\d*$/, '')}
  	}
  	else if (req.url.includes('package')) {
  		try {
  			const data = req.url.split('package/').pop().split('/')
  			const url = parse(crypto.base64.decode(data[0]))
  			const id = data[1].replace(/\.\w+/, '')
  			req.url = url.href
  			req.headers['host'] = url.hostname
  			req.headers['cookie'] = null
  			ctx.package = {id}
  			ctx.decision = 'proxy'
  			// if (url.href.includes('google'))
  			// 	return request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080'))
  			// 	.then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res)))
  		}
  		catch(error) {
  			ctx.error = error
  			ctx.decision = 'close'
  		}
  	}
  }
  
  hook.request.after = ctx => {
  	const {req, proxyRes, netease, package} = ctx
  	if (req.headers.host === 'tyst.migu.cn' && proxyRes.headers['content-range'] && proxyRes.statusCode === 200) proxyRes.statusCode = 206
  	if (netease && hook.target.path.has(netease.path) && proxyRes.statusCode == 200) {
  		return request.read(proxyRes, true)
  		.then(buffer => buffer.length ? proxyRes.body = buffer : Promise.reject())
  		.then(buffer => {
  			const patch = string => string.replace(/([^\\]"\s*:\s*)(\d{16,})(\s*[}|,])/g, '$1"$2L"$3') // for js precision
  			try {
  				netease.encrypted = false
  				netease.jsonBody = JSON.parse(patch(buffer.toString()))
  			}
  			catch(error) {
  				netease.encrypted = true
  				netease.jsonBody = JSON.parse(patch(crypto.eapi.decrypt(buffer).toString()))
  			}
  
  			if (new Set([401, 512]).has(netease.jsonBody.code) && !netease.web) {
  				if (netease.path.includes('manipulate')) return tryCollect(ctx)
  				else if (netease.path == '/api/song/like') return tryLike(ctx)
  			}
  			else if (netease.path.includes('url')) return tryMatch(ctx)
  		})
  		.then(() => {
  			['transfer-encoding', 'content-encoding', 'content-length'].filter(key => key in proxyRes.headers).forEach(key => delete proxyRes.headers[key])
  
  			const inject = (key, value) => {
  				if (typeof(value) === 'object' && value != null) {
  					if ('fee' in value) value['fee'] = 0
  					if ('st' in value && 'pl' in value && 'dl' in value && 'subp' in value) { // batch modify
  						value['st'] = 0
  						value['subp'] = 1
  						value['pl'] = (value['pl'] == 0) ? 320000 : value['pl']
  						value['dl'] = (value['dl'] == 0) ? 320000 : value['dl']
  					}
  				}
  				return value
  			}
  
  			let body = JSON.stringify(netease.jsonBody, inject)
  			body = body.replace(/([^\\]"\s*:\s*)"(\d{16,})L"(\s*[}|,])/g, '$1$2$3') // for js precision
  			proxyRes.body = (netease.encrypted ? crypto.eapi.encrypt(Buffer.from(body)) : body)
  		})
  		.catch(error => error ? console.log(error, req.url) : null)
  	}
  	else if (package) {
  		if (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) {
  			return request(req.method, parse(req.url).resolve(proxyRes.headers.location), req.headers)
  			.then(response => ctx.proxyRes = response)
  		}
  		else if (/p\d+c*.music.126.net/.test(req.url)) {
  			proxyRes.headers['content-type'] = 'audio/*'
  		}
  	}
  }
  
  hook.connect.before = ctx => {
  	const {req} = ctx
  	const url = parse('https://' + req.url)
  	if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host))) {
  		if (url.port == 80) {
  			req.url = `${global.address || 'localhost'}:${global.port[0]}`
  			req.local = true
  		}
  		else if (global.port[1]) {
  			req.url = `${global.address || 'localhost'}:${global.port[1]}`
  			req.local = true
  		}
  		else {
  			ctx.decision = 'blank'
  		}
  	}
  	else if (url.href.includes(global.endpoint)) ctx.decision = 'proxy'
  }
  
  hook.negotiate.before = ctx => {
  	const {req, socket, decision} = ctx
  	const url = parse('https://' + req.url)
  	const target = hook.target.host
  	if (req.local || decision) return
  	if (target.has(socket.sni) && !target.has(url.hostname)) {
  		target.add(url.hostname)
  		ctx.decision = 'blank'
  	}
  }
  
  const pretendPlay = ctx => {
  	const {req, netease} = ctx
  	const turn = 'http://music.163.com/api/song/enhance/player/url'
  	let query = null
  	if (netease.forward) {
  		const {id, br} = netease.param
  		netease.param = {ids: `["${id}"]`, br}
  		query = crypto.linuxapi.encryptRequest(turn, netease.param)
  	}
  	else {
  		const {id, br, e_r, header} = netease.param
  		netease.param = {ids: `["${id}"]`, br, e_r, header}
  		query = crypto.eapi.encryptRequest(turn, netease.param)
  	}
  	req.url = query.url
  	req.body = query.body + netease.pad
  }
  
  const tryCollect = ctx => {
  	const {req, netease} = ctx
  	const {trackIds, pid, op} = netease.param
  	const trackId = (Array.isArray(trackIds) ? trackIds : JSON.parse(trackIds))[0]
  	return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=${op}`).then(response => response.json())
  	.then(jsonBody => {
  		netease.jsonBody = jsonBody
  	})
  	.catch(() => {})
  }
  
  const tryLike = ctx => {
  	const {req, netease} = ctx
  	const {trackId} = netease.param
  	let pid = 0, userId = 0
  	return request('GET', 'http://music.163.com/api/v1/user/info', req.headers).then(response => response.json())
  	.then(jsonBody => {
  		userId = jsonBody.userPoint.userId
  		return request('GET', `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, req.headers).then(response => response.json())
  	})
  	.then(jsonBody => {
  		pid = jsonBody.playlist[0].id
  		return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add`).then(response => response.json())
  	})
  	.then(jsonBody => {
  		if (new Set([200, 502]).has(jsonBody.code)) {
  			netease.jsonBody = {code: 200, playlistId: pid}
  		}
  	})
  	.catch(() => {})
  }
  
  const computeHash = task => request('GET', task.url).then(response => crypto.md5.pipe(response))
  
  const tryMatch = ctx => {
  	const {req, netease} = ctx
  	const {jsonBody} = netease
  	let tasks = [], target = 0
  
  	const inject = item => {
  		item.flag = 0
  		if ((item.code != 200 || item.freeTrialInfo) && (target == 0 || item.id == target)) {
  			return match(item.id)
  			.then(song => {
  				item.type = song.br === 999000 ? 'flac' : 'mp3'
  				item.url = global.endpoint ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${item.id}.${item.type}` : song.url
  				item.md5 = song.md5 || crypto.md5.digest(song.url)
  				item.br = song.br || 128000
  				item.size = song.size
  				item.code = 200
  				item.freeTrialInfo = null
  				return song
  			})
  			.then(song => {
  				if (!netease.path.includes('download') || song.md5) return
  				const newer = (base, target) => {
  					const difference =
  						Array.from([base, target])
  						.map(version => version.split('.').slice(0, 3).map(number => parseInt(number) || 0))
  						.reduce((aggregation, current) => !aggregation.length ? current.map(element => [element]) : aggregation.map((element, index) => element.concat(current[index])), [])
  						.filter(pair => pair[0] != pair[1])[0]
  					return !difference || difference[0] <= difference[1]
  				}
  				const limit = {android: '0.0.0', osx: '0.0.0'}
  				const task = {key: song.url.replace(/\?.*$/, '').replace(/(?<=kugou\.com\/)\w+\/\w+\//, '').replace(/(?<=kuwo\.cn\/)\w+\/\w+\/resource\//, ''), url: song.url}
  				try {
  					let {header} = netease.param
  					header = typeof(header) === 'string' ? JSON.parse(header) : header
  					const cookie = querystring.parse(req.headers.cookie.replace(/\s/g, ''), ';')
  					const os = header.os || cookie.os, version = header.appver || cookie.appver
  					if (os in limit && newer(limit[os], version))
  						return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value)
  				}
  				catch(e) {}
  			})
  			.catch(() => {})
  		}
  		else if (item.code == 200 && netease.web) {
  			item.url = item.url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net')
  		}
  	}
  
  	if (!Array.isArray(jsonBody.data)) {
  		tasks = [inject(jsonBody.data)]
  	}
  	else if (netease.path.includes('download')) {
  		jsonBody.data = jsonBody.data[0]
  		tasks = [inject(jsonBody.data)]
  	}
  	else {
  		target = netease.web ? 0 : parseInt(((Array.isArray(netease.param.ids) ? netease.param.ids : JSON.parse(netease.param.ids))[0] || 0).toString().replace('_0', '')) // reduce time cost
  		tasks = jsonBody.data.map(item => inject(item))
  	}
  	return Promise.all(tasks).catch(() => {})
  }
  
  module.exports = hook