#!/usr/bin/env node
import S3 from 'aws-sdk/clients/s3'
import STS from 'aws-sdk/clients/sts'
import AWS from 'aws-sdk'
import chokidar from 'chokidar'
import { promises as fs, createReadStream,createWriteStream } from 'fs'
import p from 'path'
import { program } from 'commander'
import inquirer from 'inquirer'
import c from 'chalk'
import readLastLines from 'read-last-lines'
import { exec, ExecException, spawn, spawnSync } from 'child_process';

const sts = new STS()

const fileCreds = new AWS.SharedIniFileCredentials()
const { version, name } = require('../package')

inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));

// console.clear()
console.log(c.green(`running ${name}@${version}`))

const s3 = new S3()
const bucketReg = /(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)/

async function getBucketNames() {
  const { Buckets } = await s3.listBuckets().promise()
  const buckets = Buckets != null ?
    (Buckets
    .filter(bucket => bucket != null && bucket.Name != null)
    .map(b => b.Name)) as string[]
     : null

  if (buckets == null) {
    return []
  } else {
    return buckets
  }

}

async function createBucket ({ name, region }: { name?: string, region: string }): Promise<string> {
  let Bucket
  if (!name) {
    let { bucketName } = await inquirer.prompt([{
      type: 'input',
      name: 'bucketName',
      validate: async (name) => {
        if (!bucketReg.test(name)) {
          return 'must be a valid bucket name -> https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html'
        }
        return true
      }
    }])
    try {
      console.log(c.green(`Creating bucket ${bucketName} in region: ${region}`))
      await s3.createBucket({
        Bucket: bucketName,
        CreateBucketConfiguration: {
          LocationConstraint: region
        }
      }).promise()
    } catch (err) {
      // console.log(c.red(err.message))
      bucketName = createBucket({ name, region }) 
    }
    return bucketName
  } else if (!bucketReg.test(name)) {
    console.error('must be a valid bucket name -> https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html')
    const bucket = await createBucket({ region })
    return bucket
  } else {
    let bucketName: string = name
    await s3.createBucket({
      Bucket: name
    }).promise()
    return bucketName
  }
}

async function makeBucketName({ name, region }: { name?: string, region: string, }) {
  if (name != null) {
    const bucketName = await createBucket({ name, region })
    return bucketName
  } else {

    const { createNew } = await inquirer.prompt([{
      type: 'confirm',
      message: 'Would you like a new bucket?',
      name: 'createNew',
    }])

    if (createNew) {
      const bucketName = await createBucket({ region })
      return bucketName
    } else {

    const buckets = await getBucketNames()
    const { bucketName } = await inquirer.prompt([{
      type: 'autocomplete',
      name: 'bucketName',
      message: 'Which s3 bucket would you like to sync?',
      source: (answersSoFar: any, input: any) => {
        if (typeof input == 'string' && input.length > 0) {
          return buckets.filter((bucket: string) => bucket.includes(input))
        } else {
          return buckets
        }
      }
    }])
      return bucketName
    }
  }
}

async function makeDir(dirName?: string): Promise<string> {
  if (dirName == null) {
    const { dir } = await inquirer.prompt([{
      type: 'autocomplete',
      name: 'dir',
      message: 'Select a path to watch (Can be file or folder)',
      source: async (answersSoFar: any, input: any) => {
        const _input = input || '.'
        const dirname = p.dirname(_input.replace(/\/$/, '/.'))
        const possibleMatches = await fs.readdir(dirname)
        const dirs = possibleMatches
          .filter(file => {
            const name = p.relative(dirname, input || '')
            return file.includes(name)
          })

        return ['.'].concat(dirs)
      }
    }])

    return dir as string
  } else {
    const stat = await fs.stat(dirName)
    if (!stat.isDirectory() && !stat.isFile()) {
      console.error(`Path ${dirName} doesn't exist, please choose one which does or create one`)
      const dir = await makeDir()
      return dir
    }
    return dirName
  }
}

type Progress = {
  [key: string]: string
}

type ProgressPlayList = {
  [key: string]: string
}


var processingPlaylist:ProgressPlayList = {};

async function renderProgress({ bucket, dir, files }: { bucket: string, dir: string, files: Progress }) {
//   console.clear()
//   console.log(`${c.grey('s3-auto-sync: ')} ${dir} > s3://${bucket}`)
//   Object.keys(files).forEach(key => {
//     console.log(`${key}: ${c.white(files[key])}`)
//   })
}

// async function createAndEndStreamHSL(path:string,dir: string) {
//     // console.log(c.yellow(path))
//     // console.log(c.yellow(p.extname(path)))
//     try {
//         if (p.extname(path) == '.m3u8' && p.basename(path) != 'video.m3u8') {
//             // console.log(c.green(path))
//             // build outpath

//             let dirName = p.dirname(path)
//             let outPath = dirName + '/video.m3u8'

//             // let file = await fs.readFile(path, 'utf8');
//             await fs.copyFile(path, outPath)

//             // add end hsl tag into m3u8 file
//             const hsl_end_tag = '#EXT-X-ENDLIST'
//             // file = file + hsl_end_tag
//             await fs.writeFile(outPath, hsl_end_tag,  {'flag':'a'})

//             // write to disk
//             // await fs.writeFile(outPath, file);

//             // createReadStream(path).pipe(createWriteStream('newLog.log'));
//         }
//     } catch (error) {
//         console.error(c.red(error));
//     }
// }

async function parsePlaylistM3U8File(path:string,dir: string,files:Progress,Bucket:string) {
  // console.log(c.yellow(path))
  // console.log(c.yellow(p.extname(path)))

  try {
      const dirName = p.dirname(path)
      // read playlist to indentify new ts file 
      const lines = await readLastLines.read(path, 3)
      const linesArr = lines.split(/\r?\n/);
      console.log(c.yellow(`last 3 lines: ${lines}`))

      let tsFile:string = "";
      let index:string = "";
      if (linesArr.length > 0 && linesArr[0].includes('.ts')) {
        // get number from ts file name: index1.tst -> 1
        index = linesArr[0]
      } else if (linesArr.length > 1 && linesArr[1].includes('.ts')) {
        // detect end of file
        index = linesArr[1]
      }

      if (index != "") {

        // parse number of index
        let parseIndex = index.replace('index', '')
        parseIndex = parseIndex.replace('.ts', '')
        const Key = p.relative(dir, path)

        let isEndLive:boolean = false;

        if (!processingPlaylist[Key] || Number(parseIndex) == Number(processingPlaylist[Key]) + 1) {
          // live still continue
          processingPlaylist[Key] = parseIndex
        } else {
          // upload last upload files from last indexes -> end live
          const missedIndex = `index` + String(Number(parseIndex) -1) + `.ts`
          tsFile = dirName + '/' + missedIndex
          console.log(c.yellow(`upload last upload files from last indexes: ${tsFile}`))
          await uploadFileToS3(tsFile,dir,files,Bucket)
          isEndLive = true
        }
        
        
        // upload index ts file to s3
        tsFile = dirName + '/' + index
        console.log(c.yellow(`processing index: ${processingPlaylist[Key]}`))
        console.log(c.yellow(`upload ts file to s3: ${tsFile}`))
        // update load file to s3
        await uploadFileToS3(tsFile,dir,files,Bucket)

        // create live play list and end play list
        await createLivePlayList(path,isEndLive)
        await createEndPlayList(path)
        
      }
    
  } catch (error) {
    console.error(c.red(error));
  }
}

// remove last 2 line of current m3u8
async function createLivePlayList(path:string,isEndLive:boolean) {
  try {
    let dirName = p.dirname(path)
    let livePath = dirName + '/live.m3u8'
    let cmd = `head -n -2 ${path} > ${livePath}`
    if (isEndLive == true) {
      cmd = `cat ${path} > ${livePath}`
    }
    const ls = await spawn(cmd, [], {shell: true})
  } catch (error) {
    console.error(c.red(error));
  }
}

async function createEndPlayList(path:string) {
  try {
    let dirName = p.dirname(path)
    let videoPath = dirName + '/video.m3u8'
    let cmd = `(cat ${path}; echo "#EXT-X-ENDLIST")> ${videoPath}`
    const ls = await spawn(cmd, [], {shell: true})
  } catch (error) {
    console.error(c.red(error));
  }
}

async function m3u8FileChange(path:string,dir: string,files:Progress,Bucket:string) {
  // console.log(c.yellow(path))
  // console.log(c.yellow(p.extname(path)))
  try {
      if (p.extname(path) == '.m3u8' && p.basename(path) == 'index.m3u8') {
          // console.log(c.green(path))
          
          await parsePlaylistM3U8File(path,dir,files,Bucket)
          // await createLivePlayList(path)
          // await createEndPlayList(path)

          // // build outpath
          // let dirName = p.dirname(path)
          // let outPath = dirName + '/video.m3u8'
          
          // // create the playlist file with end sign
          // // let file = await fs.readFile(path, 'utf8');
          // await fs.copyFile(path, outPath)
          // // add end hsl tag into m3u8 file
          // const hsl_end_tag = '#EXT-X-ENDLIST'
          // // file = file + hsl_end_tag
          // await fs.writeFile(outPath, hsl_end_tag,  {'flag':'a'})

          // write to disk
          // await fs.writeFile(outPath, file);
      }
  } catch (error) {
      console.error(c.red(error));
  }
}

async function uploadFileToS3(path:string,dir: string,files:Progress,Bucket:string) {
    console.log(c.green(`uploading to s3 ${path}`))
    const file = createReadStream(path)
    const Key = p.relative(dir, path)
    files[Key] = '0%'
    // renderProgress({ bucket: Bucket , dir, files })
    const upload = new S3.ManagedUpload({
      params: {
        Bucket,
        Key, 
        Body: file,
      },
    })
    // upload.on('httpUploadProgress', (prog) => {
    //   const percent = Math.round((prog.loaded / prog.total) * 100)
    //   files[Key] = `${percent}%`
    //   renderProgress({ bucket: Bucket , dir, files })
    // })
    upload.send()
    await upload.promise()
}
async function watchAndSync(Bucket: string, dir: string) {
  // console.clear()
  const files: Progress = {}
  const keepFile: string = "keep"
  let customDir = dir + `/**/*.m3u8`
  chokidar.watch(customDir, { ignored: /\.DS_Store$/}).on('add', async (path: string) => {
    console.log(c.green(`adding... ${path}`))
    // await createAndEndStreamHSL(path,dir)
    // await uploadFileToS3(path,dir,files,Bucket)
    await m3u8FileChange(path,dir,files,Bucket)
    await uploadFileToS3(path,dir,files,Bucket)
  }).on('change', async (path: string) => {
    console.log(c.green(`change... ${path}`))
    // await createAndEndStreamHSL(path,dir)
    await m3u8FileChange(path,dir,files,Bucket)
    await uploadFileToS3(path,dir,files,Bucket)
  }).on('unlink', async (path: string) => {
    if (keepFile == null) {
        const Key = p.relative(dir, path)

        await s3.deleteObject({
        Bucket,
        Key: path
        }).promise()
        files[Key] = `Removed`
        // renderProgress({ bucket: Bucket , dir, files })
    }
  });
}


async function run () {
  const id = await sts.getCallerIdentity().promise()

  program
    .option('-r --region <region>')
    .option('-d --dir <dir>')
    .option('-b --bucket <bucket>')
    .parse(process.argv);

  const options: { bucket?: string, dir?: string, region?: string, keep?: string} = program.opts()
  let region: string | undefined = options.region
  if (region == null) {
    console.log(c.yellow('Warning: Missing --region argument, defaulting to AWS_REGION env variable'))
  }
  if (options.region == null ) {
    region = process.env.AWS_REGION
  } 

  if (region != null) {
    const dirName = await makeDir(options.dir)
    const bucketName = await makeBucketName({ region, name: options.bucket })
    watchAndSync(bucketName, dirName)
  } else {
    console.log(c.red('Error: Missing AWS_REGION env variable as well as --region argument, cannot set region'))
    return 
  }
}

run()