import {injectable, inject} from 'inversify';
import {DockerUtil} from '../../interfaces/docker-util';
import * as _ from 'lodash';
import {
  DockerOde, ImageOrContainer, DockerImageOrContainer,
  ImageOrContainerRemoveResults
} from '../..';
import {CommandUtil, Positive, FailureRetVal, Spawn, SafeJson} from 'firmament-yargs';
import {DockerUtilOptions} from '../../interfaces/docker-util-options';
import {ForceErrorImpl} from 'firmament-yargs';

const deepExtend = require('deep-extend');
import * as  async from 'async';

@injectable()
export class DockerUtilImpl extends ForceErrorImpl implements DockerUtil {
  constructor(@inject('DockerOde') private dockerode: DockerOde,
              @inject('Positive') private positive: Positive,
              @inject('SafeJson') private safeJson: SafeJson,
              @inject('Spawn') private spawn: Spawn,
              @inject('CommandUtil') private commandUtil: CommandUtil) {
    super();
  }

  writeJsonTemplateFile(objectToWrite: any, fullOutputPath: string) {
    const me = this;
    me.commandUtil.log("Writing JSON template file '" + fullOutputPath + "' ...");
    me.safeJson.writeFileSync(fullOutputPath, {spaces: 2}, objectToWrite);
  }

  listImagesOrContainers(options: DockerUtilOptions,
                         cb: (err: Error, imagesOrContainers: any[]) => void) {
    this.dockerode.forceError = this.forceError;
    let me = this;
    let listFn: (options: any, cb: (err: Error, imagesOrContainers: any[]) => void) => void;
    deepExtend(options, {all: true});
    listFn = (options.IorC === ImageOrContainer.Image)
      ? me.dockerode.listImages
      : me.dockerode.listContainers;
    listFn.call(
      me.dockerode,
      {
        all: true
      },
      (err: Error, imagesOrContainers: any[]) => {
        if(me.commandUtil.callbackIfError(cb, err)) {
          return;
        }
        //05-MAR-2017 JReeme sez: Ran across a weird situation where RepoTags was <null> on a container. Things broke,
        //of course. So now we need to cull images or containers without 'Names' or 'RepoTags'. Very painful.
        _.remove(imagesOrContainers, (iOrC) => {
          if(options.IorC === ImageOrContainer.Container) {
            return !iOrC.Names;
          } else if(options.IorC === ImageOrContainer.Image) {
            return !iOrC.RepoTags;
          }
        });
        //Sort by name so firmament id is consistent
        imagesOrContainers.sort(function(a, b) {
          if(options.IorC === ImageOrContainer.Container) {
            return a.Names[0].localeCompare(b.Names[0]);
          } else if(options.IorC === ImageOrContainer.Image) {
            let ref = a.RepoTags[0] + a.Id;
            let cmp = b.RepoTags[0] + b.Id;
            return ref.localeCompare(cmp);
          }
        });
        let firmamentId = 0;
        imagesOrContainers = imagesOrContainers.map(imageOrContainer => {
          imageOrContainer.firmamentId = (++firmamentId).toString();
          if(options.IorC === ImageOrContainer.Container) {
            return (options.listAll || (imageOrContainer.Status.substring(0, 2) === 'Up')) ? imageOrContainer : null;
          } else {
            return (options.listAll || (imageOrContainer.RepoTags[0] !== '<none>:<none>')) ? imageOrContainer : null;
          }
        }).filter(imageOrContainer => {
          return imageOrContainer !== null;
        });
        cb(null, imagesOrContainers);
      });
  }

  getImagesOrContainers(ids: string[],
                        options: DockerUtilOptions,
                        cb: (err: Error, imagesOrContainers: DockerImageOrContainer[]) => void) {
    this.dockerode.forceError = this.forceError;
    let me = this;
    if(!ids) {
      //if 'ids' is 'falsy' then return all containers or images
      options.listAll = true;
      me.listImagesOrContainers(options, (err: Error, imagesOrContainers: any[]) => {
        if(me.commandUtil.callbackIfError(cb, err)) {
          return;
        }
        ids = [];
        imagesOrContainers.forEach(imageOrContainer => {
          ids.push(imageOrContainer.firmamentId);
        });
        me.getImagesOrContainers(ids, options, cb);
      });
      return;
    }
    let fnArray = ids.map(id => {
      return async.apply(me.getImageOrContainer.bind(me), id.toString(), options);
    });
    async.series(fnArray, (err: Error, results: any[]) => {
      if(!me.commandUtil.callbackIfError(cb, err)) {
        cb(err, results.filter(result => !!result));
      }
    });
  }

  getImageOrContainer(id: string,
                      options: DockerUtilOptions,
                      cb: (err: Error, imageOrContainer: any) => void) {
    this.dockerode.forceError = this.forceError;
    let me = this;
    async.waterfall([
        (cb: (err: Error) => void) => {
          options.listAll = true;
          me.listImagesOrContainers(options, cb);
        },
        (imagesOrContainers: any[], cb: (err: Error, imageOrContainerOrString: any) => void) => {
          let foundImagesOrContainers = imagesOrContainers.filter(imageOrContainer => {
            if(imageOrContainer.firmamentId === id) {
              return true;
            } else {
              if(options.IorC === ImageOrContainer.Container) {
                for(let i = 0; i < imageOrContainer.Names.length; ++i) {
                  if(imageOrContainer.Names[i] === (id[0] === '/' ? id : '/' + id)) {
                    return true;
                  }
                }
              } else if(options.IorC === ImageOrContainer.Image) {
                for(let i = 0; i < imageOrContainer.RepoTags.length; ++i) {
                  //Don't match <none>:<none> (intermediate layers) since many images can have that as a RepoTag
                  if(imageOrContainer.RepoTags[i] === '<none>:<none>') {
                    continue;
                  }
                  if(imageOrContainer.RepoTags[i] === id) {
                    return true;
                  }
                }
              }
              let charCount = id.length;
              if(charCount < 3) {
                return false;
              }
              return DockerUtilImpl.compareIds(id, imageOrContainer.Id);
            }
          });
          if(foundImagesOrContainers.length > 0) {
            let imageOrContainer: DockerImageOrContainer;
            if(options.IorC === ImageOrContainer.Container) {
              imageOrContainer = me.dockerode.getContainer(foundImagesOrContainers[0].Id, options);
              imageOrContainer.Name = foundImagesOrContainers[0].Names[0];
            } else if(options.IorC === ImageOrContainer.Image) {
              imageOrContainer = me.dockerode.getImage(foundImagesOrContainers[0].Id, options);
              imageOrContainer.Name = foundImagesOrContainers[0].RepoTags[0];
            }
            imageOrContainer.Id = DockerUtilImpl.stripSha256(foundImagesOrContainers[0].Id);
            cb(null, imageOrContainer);
          } else {
            cb(null, `Unable to find: ${id}`);
          }
        }
      ],
      cb);
  }

  removeImagesOrContainers(ids: string[],
                           options: DockerUtilOptions,
                           cb: (err: Error, imageOrContainerRemoveResults: ImageOrContainerRemoveResults[]) => void) {
    this.dockerode.forceError = this.forceError;
    let me = this;
    ids = ids || [];
    let thingsToRemove = options.IorC === ImageOrContainer.Container
      ? 'containers'
      : 'images';
    let thingToRemove = options.IorC === ImageOrContainer.Container
      ? 'container'
      : 'image';
    if(!ids.length) {
      throw new Error(`Specify ${thingsToRemove} to remove by FirmamentId, Docker ID or Name. Or 'all' to remove all.`);
    }
    if(_.indexOf(ids, 'all') !== -1) {
      if(!this.positive.areYouSure(`You're sure you want to remove all ${thingsToRemove}? [y/N] `,
        `Operation canceled.`,
        false,
        FailureRetVal.TRUE)) {
        return cb(null, null);
      }
      ids = null;
    }
    //BEGIN - Use brutal 'docker images -a|awk '{print $3}'|docker rmi -f {}' to really remove all images
    if(!ids && thingsToRemove === 'images') {
      return async.waterfall([
        (cb) => {
          me.spawn.spawnShellCommandAsync([
              'bash',
              '-c',
              `docker images -a | awk '{print \$3}'`
            ],
            {
              cacheStdOut: true
            },
            me.logErrAndResult.bind(me),
            cb);
        },
        (listImagesResult, cb) => {
          me.safeJson.safeParse(listImagesResult, (err: Error, obj: any) => {
            const imagesToDelete = obj.stdoutText.toString().split('\n');
            if(imagesToDelete.shift() !== 'IMAGE') {
              return cb(new Error('Remove docker images FAILED'));
            }
            cb(err, imagesToDelete);
          });
        },
        (imagesToDelete, cb) => {
          async.eachSeries(imagesToDelete, (imageToDelete, cb) => {
            me.spawn.spawnShellCommandAsync([
                'docker',
                'rmi',
                '-f',
                `${imageToDelete}`
              ],
              {
                cacheStdOut: true
              },
              me.logErrAndResult.bind(me),
              () => {
                cb(null);
              });
          }, cb);
        }
      ], cb);
    }
    //END - Use brutal 'docker images -a|awk '{print $3}'|docker rmi -f {}' to really remove all images
    //Use old technique to remove images/containers
    me.getImagesOrContainers(ids, options, (err: Error, dockerImagesOrContainers: DockerImageOrContainer[]) => {
      if(me.commandUtil.callbackIfError(cb, err)) {
        return;
      }
      async.map(dockerImagesOrContainers,
        (imageOrContainer: DockerImageOrContainer, cb) => {
          if(typeof imageOrContainer === 'string') {
            me.commandUtil.logAndCallback(imageOrContainer,
              cb,
              null,
              {msg: imageOrContainer});
          } else {
            imageOrContainer.remove({force: 1}, (err: Error) => {
              let msg = `Removing ${thingToRemove} '${imageOrContainer.Name}' with id: '${imageOrContainer.Id.substr(0, 8)}'`;
              me.commandUtil.logAndCallback(msg, cb, err, {msg});
            });
          }
        }, cb);
    });
  }

  private static compareIds(id0: string, id1: string): boolean {
    let str0 = DockerUtilImpl.stripSha256(id0).toLowerCase();
    let str1 = DockerUtilImpl.stripSha256(id1).toLowerCase();
    let len = str0.length < str1.length
      ? str0.length
      : str1.length;
    str0 = str0.substr(0, len);
    str1 = str1.substr(0, len);
    return str0 === str1;
  }

  private static stripSha256(id: string): string {
    const testPrefix = 'sha256:';
    let startIdx = (testPrefix === id.substr(0, testPrefix.length))
      ? testPrefix.length
      : 0;
    return id.substr(startIdx);
  }

  private logErrAndResult(err: Error, result: string) {
    const me = this;
    if(err) {
      return me.commandUtil.log(err.message);
    }
    me.commandUtil.log((result || '').toString());
  }
}
