import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { DockerContainer } from './classes.container.js';
import { DockerNetwork } from './classes.network.js';
import { DockerService } from './classes.service.js';
import { logger } from './logger.js';
import path from 'path';
import { DockerImageStore } from './classes.imagestore.js';
import { DockerImage } from './classes.image.js';

export interface IAuthData {
  serveraddress: string;
  username: string;
  password: string;
}

export interface IDockerHostConstructorOptions {
  dockerSockPath?: string;
  imageStoreDir?: string;
}

export class DockerHost {
  public options: IDockerHostConstructorOptions;

  /**
   * the path where the docker sock can be found
   */
  public socketPath: string;
  private registryToken: string = '';
  public imageStore: DockerImageStore;
  public smartBucket: plugins.smartbucket.SmartBucket;

  /**
   * the constructor to instantiate a new docker sock instance
   * @param pathArg
   */
  constructor(optionsArg: IDockerHostConstructorOptions) {
    this.options = {
      ...{
        imageStoreDir: plugins.path.join(paths.nogitDir, 'temp-docker-image-store'),
      },
      ...optionsArg,
    }
    let pathToUse: string;
    if (optionsArg.dockerSockPath) {
      pathToUse = optionsArg.dockerSockPath;
    } else if (process.env.DOCKER_HOST) {
      pathToUse = process.env.DOCKER_HOST;
    } else if (process.env.CI) {
      pathToUse = 'http://docker:2375/';
    } else {
      pathToUse = 'http://unix:/var/run/docker.sock:';
    }
    if (pathToUse.startsWith('unix:///')) {
      pathToUse = pathToUse.replace('unix://', 'http://unix:');
    }
    if (pathToUse.endsWith('.sock')) {
      pathToUse = pathToUse.replace('.sock', '.sock:');
    }
    console.log(`using docker sock at ${pathToUse}`);
    this.socketPath = pathToUse;
    this.imageStore = new DockerImageStore({
      bucketDir: null,
      localDirPath: this.options.imageStoreDir,
    })
  }

  public async start() {
    await this.imageStore.start();
  }
  public async stop() {
    await this.imageStore.stop();
  }

  /**
   * authenticate against a registry
   * @param userArg
   * @param passArg
   */
  public async auth(authData: IAuthData) {
    const response = await this.request('POST', '/auth', authData);
    if (response.body.Status !== 'Login Succeeded') {
      console.log(`Login failed with ${response.body.Status}`);
      throw new Error(response.body.Status);
    }
    console.log(response.body.Status);
    this.registryToken = plugins.smartstring.base64.encode(plugins.smartjson.stringify(authData));
  }

  /**
   * gets the token from the .docker/config.json file for GitLab registry
   */
  public async getAuthTokenFromDockerConfig(registryUrlArg: string) {
    const dockerConfigPath = plugins.smartpath.get.home('~/.docker/config.json');
    const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath);
    const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
    const gitlabAuth: string = plugins.smartstring.base64.decode(gitlabAuthBase64);
    const gitlabAuthArray = gitlabAuth.split(':');
    await this.auth({
      username: gitlabAuthArray[0],
      password: gitlabAuthArray[1],
      serveraddress: registryUrlArg,
    });
  }

  // ==============
  // NETWORKS
  // ==============
  /**
   * gets all networks
   */
  public async getNetworks() {
    return await DockerNetwork.getNetworks(this);
  }

  /**
   * create a network
   */
  public async createNetwork(optionsArg: Parameters<typeof DockerNetwork.createNetwork>[1]) {
    return await DockerNetwork.createNetwork(this, optionsArg);
  }

  /**
   * get a network by name
   */
  public async getNetworkByName(networkNameArg: string) {
    return await DockerNetwork.getNetworkByName(this, networkNameArg);
  }


  // ==============
  // CONTAINERS
  // ==============
  /**
   * gets all containers
   */
  public async getContainers() {
    const containerArray = await DockerContainer.getContainers(this);
    return containerArray;
  }

  // ==============
  // SERVICES
  // ==============

  /**
   * gets all services
   */
  public async getServices() {
    const serviceArray = await DockerService.getServices(this);
    return serviceArray;
  }

  // ==============
  // IMAGES
  // ==============

  /**
   * get all images
   */
  public async getImages() {
    return await DockerImage.getImages(this);
  }

  /**
   * get an image by name
   */
  public async getImageByName(imageNameArg: string) {
    return await DockerImage.getImageByName(this, imageNameArg);
  }

  /**
   *
   */
  public async getEventObservable(): Promise<plugins.rxjs.Observable<any>> {
    const response = await this.requestStreaming('GET', '/events');
    return plugins.rxjs.Observable.create((observer) => {
      response.on('data', (data) => {
        const eventString = data.toString();
        try {
          const eventObject = JSON.parse(eventString);
          observer.next(eventObject);
        } catch (e) {
          console.log(e);
        }
      });
      return () => {
        response.emit('end');
      };
    });
  }

  /**
   * activates docker swarm
   */
  public async activateSwarm(addvertisementIpArg?: string) {
    // determine advertisement address
    let addvertisementIp: string;
    if (addvertisementIpArg) {
      addvertisementIp = addvertisementIpArg;
    } else {
      const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
      const defaultGateway = await smartnetworkInstance.getDefaultGateway();
      if (defaultGateway) {
        addvertisementIp = defaultGateway.ipv4.address;
      }
    }

    const response = await this.request('POST', '/swarm/init', {
      ListenAddr: '0.0.0.0:2377',
      AdvertiseAddr: addvertisementIp,
      DataPathPort: 4789,
      DefaultAddrPool: ['10.10.0.0/8', '20.20.0.0/8'],
      SubnetSize: 24,
      ForceNewCluster: false,
    });
    if (response.statusCode === 200) {
      logger.log('info', 'created Swam succesfully');
    } else {
      logger.log('error', 'could not initiate swarm');
    }
  }

  /**
   * fire a request
   */
  public async request(methodArg: string, routeArg: string, dataArg = {}) {
    const requestUrl = `${this.socketPath}${routeArg}`;
    const response = await plugins.smartrequest.request(requestUrl, {
      method: methodArg,
      headers: {
        'Content-Type': 'application/json',
        'X-Registry-Auth': this.registryToken,
        Host: 'docker.sock',
      },
      requestBody: dataArg,
      keepAlive: false,
    });
    if (response.statusCode !== 200) {
      console.log(response.body);
    }
    return response;
  }

  public async requestStreaming(methodArg: string, routeArg: string, readStream?: plugins.smartstream.stream.Readable) {
    const requestUrl = `${this.socketPath}${routeArg}`;
    const response = await plugins.smartrequest.request(
      requestUrl,
      {
        method: methodArg,
        headers: {
          'Content-Type': 'application/json',
          'X-Registry-Auth': this.registryToken,
          Host: 'docker.sock',
        },
        requestBody: null,
        keepAlive: false,
      },
      true,
      (readStream ? reqArg => {
        let counter = 0;
        const smartduplex = new plugins.smartstream.SmartDuplex({
          writeFunction: async (chunkArg) => {
            if (counter % 1000 === 0) {
              console.log(`posting chunk ${counter}`);
            }
            counter++;
            return chunkArg;
          }
        });
        readStream.pipe(smartduplex).pipe(reqArg);
      } : null),
    );
    console.log(response.statusCode);
    console.log(response.body);
    return response;
  }

  /**
   * add s3 storage
   * @param optionsArg
   */
  public async addS3Storage(optionsArg: plugins.tsclass.storage.IS3Descriptor) {
    this.smartBucket = new plugins.smartbucket.SmartBucket(optionsArg);
    if (!optionsArg.bucketName) {
      throw new Error('bucketName is required');
    }
    const bucket = await this.smartBucket.getBucketByName(optionsArg.bucketName);
    let wantedDirectory = await bucket.getBaseDirectory();
    if (optionsArg.directoryPath) {
      wantedDirectory = await wantedDirectory.getSubDirectoryByName(optionsArg.directoryPath);
    }
    this.imageStore.options.bucketDir = wantedDirectory;
  }
}
