import * as Net from 'net'
import * as Rfc1928 from './rfc1928'
import * as shortID from 'shortid'
import * as Async from 'async'
import ImapCluster from './imapCluster'
import * as Compress from './compress'
import mainWrite from './mainWrite'
import * as HttpProxy from './httpProxy'
import * as fs from 'fs'
import * as hostList from './hostList'

//import TransfromFromServer from './transformServerBack'
import * as Stream from 'stream'

const res_NO_AUTHENTICATION_REQUIRED = new Buffer ( '0500', 'hex' )
const res_AUTHENTICATION_REQUIRED = new Buffer ( '0502', 'hex' )
const res_ENDSOCKET_REQUIRED = new Buffer ( '05ff', 'hex' )
const res_NO_ACCEPTABLE_METHODS = new Buffer ( '05ff', 'hex')
const respon_se = new Buffer ( '05000001000000000000', 'hex' )
const body_401 = '<!DOCTYPE html><html>Error user/password</html>'
const body_403 = '<!DOCTYPE html><html><p>這個域名被代理服務器列入黑名單</p><p>This domain in proxy blacklist.</p><p>このサイドはプロクシーの禁止リストにあります</p></html>'
const cachePath = '.cache/'

const HTTP_407 = `HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="Vpn.Email login"
Content-Length: 0
Connection: close
Proxy-Connection: close
Content-Type: text/html; charset=UTF-8
Cache-Control: private, max-age=0

`
const HTTP_401 = `HTTP/1.1 401 Unauthorized
Proxy-Authenticate: Basic realm="Vpn.Email user/password error!"
Content-Length: ${body_401.length}
Connection: close
Proxy-Connection: close

${body_401}

`

const HTTP_407_LOGIN_ERR = `HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="Vpn.Email login error"
Content-Length: 0
Connection: close
Proxy-Connection: close
Content-Type: text/html; charset=UTF-8
Cache-Control: private, max-age=0

`

const HTTP_PROXY_200 = `HTTP/1.1 200 Connection Established
Content-Type: text/html; charset=UTF-8

`

const HTTP_403 = `HTTP/1.1 403 Forbidden
Content-Type: text/html; charset=UTF-8
Connection: close
Proxy-Connection: close
Content-Length: 300

${body_403}

`
const IsSslConnect = ( buffer: Buffer ) => {
	
	const kk = buffer.toString ('hex', 0, 4)
	
	return /^1603(01|02|03|00)|^80..0103|^(14|15|17)03(00|01)/.test (kk)
}

/**
 * 		buffer format
 * 		bit [0] = 0 next is buffer
 * 		bit [0] = 1 COMMAND
 * 		bit [0] = 2 socket.end()
 * 		bit [32] = serial number  (must)
 * 		bit [5] = 36 bit uuid 'uft8
 * 		bit [41] = buffer
 * 
 */

export default class localProxy {

	private bufferPool: Map < number, Compress.packetBuffer > = new Map ()

	private save = ( data ) => {

		const _data: VE_IPptpStream = {
			type: 'proxyTcp',
			host: this.host,
			port: this.port,
			buffer: data.toString ( 'base64' ),
			ATYP: this.ATYP,
			cmd: this.cmd,
		}
		const _dataJson  = new Buffer ( JSON.stringify ( _data ), 'utf8' )
		//const uuu = Compress.encrypt ( _dataJson,  this.masterPassword )
		const buffer = Compress.packetBuffer ( 1, 0, this.id, _dataJson )
		this.socketPool.set ( this.id, this )
		return this.ImapCluster.pushMainData ( buffer )
	}


	private connectStat2_after ( retBuffer: Rfc1928.Requests, cmd: string ) {

		if ( this.keep ) {

			this.socket.once ( 'data', ( data: Buffer ) => {
				const header = new HttpProxy.httpProxy ( data )

				if ( this.hostList.putListSock5 ( this.host, IsSslConnect ? null : header ))
					return this.endConnect ( HTTP_403 )
				if (  header.cachePath && this.cacheKeepTime ) {
					return this.proxyCacheSave ( header, ( err, data1: Buffer ) => {
						if ( !data1 ) {
							this.mainWrite = new mainWrite ( this.ImapCluster, this.masterPassword, IsSslConnect ( data ), this.id, 0 )
							this.socket.pipe ( this.mainWrite )
							if ( this.savePath )
								return this.save ( header.BufferWithOutKeepAlife )
							return this.save ( data )
						}
						
						this.ending = true
						return this.endConnect ( data1 )

					})
				}

				this.mainWrite = new mainWrite ( this.ImapCluster, this.masterPassword, IsSslConnect ( data ), this.id, 0 )
				this.socket.pipe ( this.mainWrite )

				return this.save ( data )
			})

			return this.socket.write ( retBuffer.buffer )
			
		}

		this.socket.write ( retBuffer.buffer )
		console.log ( '====================> cmd is not Rfc1928.CMD.CONNECT close connect', cmd  )
		return this.socket.end ()
	}

	private connectStat2 ( data: Buffer ) {

		//this.socket.pause()
		
		const req = new Rfc1928.Requests ( data )
		this.ATYP = req.ATYP
		this.host = req.host
		this.port = req.port
		this.cmd = req.cmd
		let Continue = true
		const localIp = this.socket.localAddress.split (':')[3]
		let retBuffer = new Rfc1928.Requests ( respon_se )
		retBuffer.ATYP_IP4Address = localIp
		
		let cmd = ''
		
		switch ( this.cmd ) {
			case Rfc1928.CMD.CONNECT:
				/*
				if ( this.ATYP === Rfc1928.ATYP.DOMAINNAME ) 
					return this.checkDomain ( this.host, true, ( err, ip ) => {
						if ( err ) {

							retBuffer.REP = Rfc1928.Replies.HOST_UNREACHABLE
							Continue = false
						}
						console.log (`return a ip ${ip}, this.ATYP[${this.ATYP}]`)
						this.host = ip
						this.connectStat2_after ( Continue, retBuffer )
					})
				*/

			break
			case Rfc1928.CMD.BIND:
				cmd = 'Rfc1928.CMD.BIND'
				Continue = false;
				retBuffer.REP = Rfc1928.Replies.COMMAND_NOT_SUPPORTED_or_PROTOCOL_ERROR
			case Rfc1928.CMD.UDP_ASSOCIATE:
				cmd = 'Rfc1928.CMD.UDP_ASSOCIATE'
				Continue = false;
				retBuffer.REP = Rfc1928.Replies.COMMAND_NOT_SUPPORTED_or_PROTOCOL_ERROR
			break
			default:
				Continue = false;
				retBuffer.REP = Rfc1928.Replies.COMMAND_NOT_SUPPORTED_or_PROTOCOL_ERROR
			break
		}
		this.keep = Continue
		return this.connectStat2_after ( retBuffer, cmd )
		
	}

	private proxyCacheSave ( data: HttpProxy.httpProxy, CallBack ) {
		
		this.savePath = data.cachePath
		const kkkk = data.Url.host + data.Url.href
		if ( ! data.cachePath ) {
			
			return CallBack ()
		}
			
		const path = cachePath + '/' + this.savePath
		
		fs.stat ( path, ( err, stat ) => {
			if ( err ) {
				return CallBack ()
			}
			const now = new Date ().getTime()
			const birthtime = new Date ( stat.birthtime ).getTime()
			if ( now - birthtime > this.cacheKeepTime ) {
				return fs.unlink ( path, err1 => {
					if ( err1 ) {
						console.log ( 'file system got error, cache save stop', err.message )
					}
					
					return CallBack ()
				})
			}

			fs.readFile ( path, ( err2, data ) => {
				if ( err2 ) {

					fs.unlink ( path, err3 => {})
					return CallBack ()
				}

				return CallBack ( null, data )
			})
			
		})
		

		
	}


	private findIpInWhiteList ( ip: string ) {
		if ( ! this.whiteIpList || ! this.whiteIpList.length )
			return false
		return this.whiteIpList.find ( n => { return ip === n })
	}

	private connectToLoginSever () {

	}

	private httpProxy ( data: Buffer ) {

		const httpHead = new HttpProxy.httpProxy ( data )
		const ip = this.socket.remoteAddress
		let count = this.blackIpList.get ( ip ) || 1
			
		if ( ! httpHead.isHttpRequest ) {
			this.blackIpList.set ( ip, ++ count )
			console.log ( 'Bad request from : ', ip, count, httpHead.command )
			return this.endConnect ()
		}
		
		
		if ( this.hostList.putList ( httpHead ))
			return this.endConnect ( HTTP_403 )

		const connectremote = () => {
			this.host = httpHead.Url.hostname
			this.port = parseInt ( httpHead.Url.port ||  httpHead.isHttps ? '443' : '80')
			this.ATYP = Rfc1928.ATYP.IP_V4
			this.cmd = Rfc1928.CMD.CONNECT
			this.keep = true
			console.log ( `connect:[${httpHead.Url.protocol}]${httpHead.Url.hostname}${httpHead.Url.path}:${this.port}` )
			if ( httpHead.isConnect ) {
				
				this.socket.once ( 'data', ( data1: Buffer ) => {
					
					this.mainWrite = new mainWrite ( this.ImapCluster, this.masterPassword, true, this.id, 0 )
					this.socket.pipe ( this.mainWrite )

					return this.save ( data1 )
				})

				return this.socket.write( HTTP_PROXY_200 )
				
			}
			
			this.mainWrite = new mainWrite ( this.ImapCluster, this.masterPassword, false, this.id, 0 )
			this.socket.pipe ( this.mainWrite )
			/*
			if ( this.savePath && this.cacheKeepTime ) {
				
				return this.save ( httpHead.BufferWithOutKeepAlife )
			}
			*/
			return this.save ( data )
		}
		/*
		if ( httpHead.cachePath && this.cacheKeepTime ) {
			return this.proxyCacheSave ( httpHead, ( err, data: Buffer ) => {
				if ( !data ) {
					return connectremote ()
				}
				
				this.ending = true
				return this.endConnect ( data )

			})
		}
		*/
		return connectremote ()
		
	}

	private connectStat1 ( data: Buffer ) {

		switch ( data.readUInt8 (0) ) {
			
			case 0x4:
				return 
			case 0x5:
				const ip = this.socket.remoteAddress
				if ( ! this.findIpInWhiteList ( ip )) {
					return this.socket.end ( res_ENDSOCKET_REQUIRED )
				}
				
				this.socket.once ( 'data', ( chunk: Buffer ) => {
					
					return this.connectStat2 ( chunk )
				})

				return this.socket.write ( res_NO_AUTHENTICATION_REQUIRED )
				

			default:
				return this.httpProxy ( data )
		}
		
	}

	private saveEndToServer () {
		if ( !this.keep || this.ending )
			return this.ending = true
		this.ending = true
		if ( this.mainWrite && this.mainWrite.sendEnd )
			this.mainWrite.sendEnd ()
	}

	private endConnect ( data: string|Buffer = null ) {
		
		this.saveEndToServer ()
		if ( this.socket && this.socket.writable ) {
			data ? this.socket.end ( data ) : this.socket.end ()
		}
			
		return this.endCallBack ()
	}

	public host: string;
	public ATYP: number;
	public port: number;
	public cmd: number;
	private ending = false
	private count1 = 0
	private serial = 0
	private mainWrite: mainWrite
	public id = shortID.generate()
	//			cache 
	private savePath = null

	private keep = false


	private checkBufferPool () {
		if ( this.bufferPool.size == 0 )
			return
		const data = this.bufferPool.get ( this.serial )
		if ( ! data )
			return
		this.bufferPool.delete ( this.serial )
		return this.getData ( data )
	}

	constructor ( private masterPassword: string, public socket: Net.Socket, public ImapCluster: ImapCluster, private whiteIpList: string[], private cacheKeepTime: number,
	 private hostLocalIp: string, private socketPool: Map < string, localProxy >, private blackIpList: Map < string, number >, private hostList: hostList.hostList, private endCallBack: () => void ) {
		

		socket.once ( 'data', ( data: Buffer ) => {

			return this.connectStat1 ( data )
		})

		socket.once ( 'end', () => {
			
			if ( ! this.ending )
				return this.endConnect ()
		})

		socket.once ( 'error', err => {
			console.log ( this.id, 'localProxyServer socket.once error:', err )
			this.endConnect ()
		})
	}

	public getData ( data: Compress.packetBuffer  ) {
		console.log (`<===== uuid[${this.id}],serial[${data.serial}],buffer[${data.buffer.length}]`)
		if ( data.serial > this.serial ) {
			//console.log ( `============-----------------》[${ this.serial }] < [${ data.serial }]`)
			return this.bufferPool.set ( data.serial, data )
		}
			
		if ( data.serial < this.serial ) {
			console.log ( `============》[${ this.id }], this.count[${ this.serial }] > count[${ data.serial }], buffer[${ data.buffer.length }]` )
			return 
		}
			
			
		if ( data.command == 2 ) {
			
			return this.endConnect ()

		}


		
		this.serial ++
		
		if ( data.buffer && data.buffer.length ) {
			
			if ( data.command === 5 ) {
				if ( ! this.socket || ! this.socket.writable ) {
					//console.log (`[${this.id}] socker is closed!`)
					return this.endConnect ()
				}
				this.socket.write ( data.buffer )
				return this.checkBufferPool ()
			}

			return Compress.decrypt ( data.buffer, this.masterPassword, ( err, _data ) => {

				if ( err )
					return console.log ( 'Compress.decrypt error', data, data.buffer.toString ('hex')) 
				const cacheData = new Buffer ( _data, 'base64' )
				
				this.bufferToCache ( cacheData )
				if ( ! this.socket || ! this.socket.writable ) {
					//console.log (`[${this.id}] socker is closed!`)
					return this.endConnect ()
				}

				this.socket.write ( cacheData )
				this.socket.resume ()
				return this.checkBufferPool ()
			})
		
		}
		
		return this.checkBufferPool ()
		
	}

	public bufferToCache ( data: Buffer ) {
		if ( ! this.savePath ) {
			
			return
		}
		const path = cachePath + '/' + this.savePath
		const openFs = fs.appendFile ( path, data, err => {})
	}

}

