import net from 'net'
import zlib from 'zlib'
import http from 'http'
import { parse } from 'url'
import { promisify } from 'util'
import { readdir, readFile } from 'fs'
import { createInterface } from 'readline'
import { createHash, createCipheriv, createDecipheriv } from 'crypto'

const FSreadFile = promisify(readFile)
const FSreaddir = promisify(readdir)
// 自定义的升级信息
const infoXML = `<?xml version="1.0" encoding="UTF-8"?><InformationFile LastUpdate="2019-05-01T00:00:00Z" Noop="false" Version="1.0">
<ControlConditions DefaultServiceStatus="open" DefaultVariance="0"/>
<ApplyConditions>
    <ApplyCondition ApplyOrder="1" Force="false">
        <Rules>
            <Rule Type="System" Key="Model" Value="CUSTOM" Operator="NotEqual"/>
            <Rule Type="System" Key="SerialNo" Value="0" Operator="GreaterThanEqual"/>
            <Rule Type="System" Key="FirmwareVersion" Value="0" Operator="NotEqual"/>
        </Rules>
        <Distributions>
            <Distribution ID="FW" InstallParams="" InstallType="binary" MAC="{FWsha1}" Size="{FWlength}" Type="" URI="http://info.update.sony.net/custom_fw.bin" Version="1"/>
            <Distribution ID="Disclaimer" InstallParams="" InstallType="notice" MAC="{Disclaimersha1}" Size="{Disclaimerlength}" Type="" URI="http://info.update.sony.net/custom_disclaimer.xml" Version="1"/>
        </Distributions>
        <Descriptions DefaultLang="Chinese(Simplified)">
            <Description Lang="Chinese(Simplified)" Title="CS"><![CDATA[请勿插入音频线或USB线。
否则可能会导致本设备发生故障。]]></Description>
        </Descriptions>
    </ApplyCondition>
</ApplyConditions>
</InformationFile>`
// 自定义的免责声明
const disclaimerXML = Buffer.from(`<?xml version="1.0" encoding="UTF-8"?> 

<NoticeFile Version="1.0" DefaultLocale="China">
    <Notice Locale="China">
        <Text><![CDATA[
※ 请在蓝牙连接最稳定的环境下进行更新。
* 请注意，在乘坐火车等车辆时，或者在Wi-Fi、微波炉、无绳电话以及其他许多无线电波等2.4GHz频带的无线电波混杂的环境中，不要更新。

※ Sony | Headphones Connect ※
更新主机之前，请更新至最新版本

[警告]
0) 本人不对因使用此软件造成的任何损失负责，使用前请确保为正品行货，做好需要保修的心理准备
1) 软件更新需要大约34分钟（Android）和44分钟（iOS）
更新下载、数据传输和更新期间切勿"关闭电源"。
否则，该装置可能会变得无法使用。
2) 请在确认这些耳机和Android设备（或iOS设备）有足够的电池寿命后更新。
3) 如果Bluetooth Low Energy设备（佩戴式终端，智能手表等）连接到Android设备（或iOS设备），则可能无法更新。
更新之前，请断开所有蓝牙设备和Android设备（或iOS设备）的连接。

]]></Text>
    </Notice>
</NoticeFile>`)

start()

/**
 * 也可以使用匿名函数
 *
 */
async function start() {
  const select0 = await choose0()
  switch (select0) {
    case '1':
      startProxy('1')
      break
    case '2':
      const select02 = await choose02()
      switch (select02) {
        case '1':
          startProxy('21')
          break
        case '2':
          startProxy('22')
          break
        case '3':
          startProxy('23')
          break
      }
      break
    case '3':
      const select03 = await choose03()
      startProxy('3', select03)
      break
  }
}
/**
 * 简单的cli交互
 *
 * @returns {Promise<string>}
 */
function choose(): Promise<string> {
  return new Promise(resolve => {
    const rl = createInterface({
      input: process.stdin,
      output: process.stdout
    })
    rl.prompt()
    rl.once('line', line => {
      rl.close()
      resolve(line)
    })
  })
}
async function choose0(): Promise<string> {
  console.log(`Please select function:
1. Force firmware upgrade
2. Force region change
3. Force flash custom firmware(DANGER!DANGER!!DANGER!!!)`)
  const select = await choose()
  if (['1', '2', '3'].includes(select)) return select
  else return choose0()
}
async function choose02(): Promise<string> {
  console.log(`Select region:
1. 00(Int ver)
2. 01(JP ver?)
3. 02(CN ver)`)
  const select = await choose()
  if (['1', '2', '3'].includes(select)) return select
  else return choose02()
}
async function choose03(): Promise<Buffer> {
  const files = await FSreaddir(`${__dirname}/custom/`)
  console.log('Please select firmware:')
  files.forEach((name, id) => console.log(`${id + 1}. ${name}`))
  const select = await choose()
  const file = files[+select - 1]
  if (file !== undefined) return await FSreadFile(`${__dirname}/custom/${file}`)
  else return choose03()
}
/**
 * 
 * 代理实现来源于 Jerry Qu
 * https://imququ.com/post/web-proxy.html
 * https://imququ.com/post/web-proxy-2.html
 * 
 * @param {string} mode
 * @param {Buffer} [fw]
 */
function startProxy(mode: string, fw?: Buffer) {
  http.createServer()
    .on('request', async (cReq: http.IncomingMessage, cRes: http.ServerResponse) => {
      const u = parse(<string>cReq.url)
      if (u.hostname === 'info.update.sony.net') {
        console.log('Catched', cReq.url)
        // 拦截 info.xml
        if ((<string>u.pathname).endsWith('info.xml')) {
          // 提取 categoryID 和 serviceID
          const pathSplit = (<string>u.pathname).match(/\/(?<categoryID>\w{5})\/(?<serviceID>\w{11})\//)
          if (pathSplit === null) nothing()
          else {
            const { categoryID, serviceID } = <{ [key: string]: string }>pathSplit.groups
            if (mode === '1' || mode[0] === '2') {
              // 切换区域
              const newServiceID = mode === '1' ? serviceID : `${serviceID.slice(0, -1)}${Number.parseInt(mode[1]) - 1}`
              const XML = await decryptedXML(categoryID, newServiceID)
              if (XML === undefined) nothing()
              else {
                // 替换升级检查条件, 实现强制升级
                const editedXML = XML.replace(/<Rule Type="System" Key="FirmwareVersion" Value="[\d\.]+" Operator="Equal"\/>/g,
                  '<Rule Type="System" Key="FirmwareVersion" Value="0" Operator="NotEqual"/>')
                const myXML = await encryptedXML(categoryID, serviceID, editedXML)
                end(zlib.gzipSync(myXML), { 'Content-Type': 'application/xml' })
              }
            }
            else if (mode === '3') {
              // 构建 info.xml
              const editedXML = infoXML
                .replace('{FWsha1}', getHash('sha1', <Buffer>fw))
                .replace('{FWlength}', (<Buffer>fw).length.toString())
                .replace('{Disclaimersha1}', getHash('sha1', disclaimerXML))
                .replace('{Disclaimerlength}', disclaimerXML.length.toString())
              const myXML = await encryptedXML(categoryID, serviceID, editedXML)
              end(zlib.gzipSync(myXML), { 'Content-Type': 'application/xml' })
            }
            else nothing()
          }
        } else if (u.pathname === '/custom_fw.bin') {
          end(zlib.gzipSync(<Buffer>fw), { 'Content-Type': 'application/octet-stream' })
        } else if (u.pathname === '/custom_disclaimer.xml') {
          end(zlib.gzipSync(disclaimerXML), { 'Content-Type': 'application/xml' })
        } else nothing()
      }
      else nothing()
      /**
       * 传输自定义文件
       *
       * @param {Buffer} data
       * @param {http.OutgoingHttpHeaders} [headers]
       */
      function end(data: Buffer, headers?: http.OutgoingHttpHeaders) {
        cRes.writeHead(200, Object.assign({
          'Accept-Ranges': 'bytes',
          'Content-Encoding': 'gzip',
          'Content-Length': data.length,
        },
          headers))
        cRes.write(data, 'binary')
        cRes.end()
      }
      /**
       * 简单代理
       *
       */
      function nothing() {
        const options = {
          hostname: u.hostname,
          port: u.port || 80,
          path: u.path,
          method: cReq.method,
          headers: cReq.headers
        }

        const pReq = http.request(options, pRes => {
          cRes.writeHead(<number>pRes.statusCode, pRes.headers)
          pRes.pipe(cRes)
        }).on('error', () => cRes.end())
        cRes.on('error', () => cReq.destroy())

        cReq.pipe(pReq)
      }
    })
    .on('connect', (cReq: http.IncomingMessage, cSock: net.Socket) => {
      const u = parse('http://' + cReq.url)

      const pSock = net.connect(Number.parseInt(<string>u.port), u.hostname, () => {
        cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n')
        pSock.pipe(cSock)
      }).on('error', () => cSock.end())
      cSock.on('error', () => pSock.end())

      cSock.pipe(pSock)
    })
    .listen(8848, '0.0.0.0', () => { console.log('Proxy service started, Port: 8848') })
}
/**
 * 解密 info.xml
 *
 * @param {string} categoryID
 * @param {string} serviceID
 * @returns {(Promise<string | undefined>)}
 */
function decryptedXML(categoryID: string, serviceID: string): Promise<string | undefined> {
  return new Promise(resolve => {
    http.get(`http://info.update.sony.net/${categoryID}/${serviceID}/info/info.xml`, {
      headers: {
        'User-Agent': 'Dalvik/2.1.0',
        'Accept-Encoding': 'gzip, deflate'
      }
    },
      res => {
        let cRes: http.IncomingMessage | zlib.Gunzip | zlib.Inflate
        let rawData: Buffer[] = []
        switch (res.headers['content-encoding']) {
          case 'gzip':
            cRes = res.pipe(zlib.createGunzip())
            break
          case 'deflate':
            cRes = res.pipe(zlib.createInflate())
            break
          default:
            cRes = res
            break
        }
        cRes
          .on('data', (chunk: Buffer) => rawData.push(chunk))
          .on('end', () => {
            const data = Buffer.concat(rawData)
            if (res.statusCode !== 200) {
              console.error(res.statusCode, data.toString())
              resolve(undefined)
            }
            else {
              // 分割数据
              const headerLength = data.indexOf('\n\n')
              // 头部数据
              const header = data.slice(0, headerLength).toString()
              // 解析头部
              const headerSplit = header.match(/eaid:(?<eaid>.*)\ndaid:(?<daid>.*)\ndigest:(?<digest>.*)/)
              if (headerSplit === null) {
                console.log(header)
                return resolve(undefined)
              }
              const { eaid, daid, digest } = <{ [key: string]: string }>headerSplit.groups
              let enc = ''
              switch (eaid) {
                case 'ENC0001':
                  enc = 'none'
                  break
                case 'ENC0002':
                  enc = 'des-ede3'
                  break
                case 'ENC0003':
                  enc = 'aes-128-ecb'
                  break
                default:
                  break
              }
              let has = ''
              switch (daid) {
                case 'HAS0001':
                  has = 'none'
                  break
                case 'HAS0002':
                  has = 'md5'
                  break
                case 'HAS0003':
                  has = 'sha1'
                  break
                default:
                  break
              }
              if (enc === '' || has === '') {
                console.log(header)
                return resolve(undefined)
              }
              // xml数据
              const cryptedData = data.slice(headerLength + 2)
              let keyBuffer: Buffer
              let decryptedData = ''
              if (enc === 'none') decryptedData = cryptedData.toString()
              else {
                if (enc === 'des-ede3') keyBuffer = Buffer.alloc(24)
                else keyBuffer = Buffer.from([79, -94, 121, -103, -1, -48, -117, 31, -28, -46, 96, -43, 123, 109, 60, 23])
                const decipher = createDecipheriv(enc, keyBuffer, '')
                decipher.setAutoPadding(false)
                decryptedData = Buffer.concat([decipher.update(cryptedData), decipher.final()]).toString()
              }
              // 数据校验
              if (has !== 'none') {
                const dataHash = getHash(has, decryptedData)
                const hash = getHash(has, dataHash + serviceID + categoryID)
                if (hash !== digest) {
                  console.log(header)
                  return resolve(undefined)
                }
              }
              resolve(decryptedData)
            }
          })
          .on('error', e => {
            console.error('数据接收错误', e)
            resolve(undefined)
          })
      }).on('error', e => {
        console.error('请求错误', e)
        resolve(undefined)
      })
  })
}
/**
 * 加密 info.xml
 *
 * @param {string} categoryID
 * @param {string} serviceID
 * @param {string} decryptedData
 * @returns {Promise<Buffer>}
 */
function encryptedXML(categoryID: string, serviceID: string, decryptedData: string): Promise<Buffer> {
  return new Promise(resolve => {
    // 去除原有填充
    const decryptedDataBuffer = Buffer.from(decryptedData.trimEnd())
    // 使用 ' ' 填充数据
    const padBuffer = Buffer.alloc(32 - decryptedDataBuffer.length % 32, ' ')
    const WTFXMLBuffer = padBuffer.length === 32 ? decryptedDataBuffer : Buffer.concat([decryptedDataBuffer, padBuffer])
    const dataHash = getHash('sha1', WTFXMLBuffer)
    const hash = getHash('sha1', dataHash + serviceID + categoryID)
    // 构建头部
    const headerBuffer = Buffer.from(`eaid:ENC0003
daid:HAS0003
digest:${hash}

`)
    const keyBuffer = Buffer.from([79, -94, 121, -103, -1, -48, -117, 31, -28, -46, 96, -43, 123, 109, 60, 23])
    const encipher = createCipheriv('aes-128-ecb', keyBuffer, '')
    encipher.setAutoPadding(false)
    const bodyBuffer = Buffer.concat([encipher.update(WTFXMLBuffer), encipher.final()])
    const encryptedData = Buffer.concat([headerBuffer, bodyBuffer])
    resolve(encryptedData)
  })
}
/**
 * 计算 hash
 *
 * @param {string} algorithm
 * @param {(string | Buffer)} data
 * @returns {string}
 */
function getHash(algorithm: string, data: string | Buffer): string {
  return createHash(algorithm).update(data).digest('hex')
}