import * as fs from "fs";
import * as path from "path";
import {$log} from "ts-log-debug";
import {RestClient} from "typed-rest-client/RestClient";
import {Constants} from "../constants";
import {HttpClient} from "typed-rest-client/HttpClient";
import klawSync = require("klaw-sync");
import { lookup } from 'mime-types';
import {dirname, join, resolve} from "path";
import {ensureDir} from "fs-extra";
import {Version} from "./models/version";
import {isSourceAsset} from "./file-utils";
import {AssetParams} from "./models/asset-params";
import Auth from "./auth";
import {AssetFile} from "./models/asset-file";
import {IHttpClientResponse} from "typed-rest-client/Interfaces";
import progress from 'progress-stream';
import Multiprogress from 'multi-progress';
import chalk from "chalk";
import * as zlib from "zlib";
import {IncomingHttpHeaders} from "http";
import {Stream} from "stream";

export interface AssetFileRef {
    id: string;
    url: string;
}

export interface FileCollectionMap {
    [file: string]: AssetFileRef;
}

const routes = {
    version: (params: AssetVersion) => `${Constants.ASSET_ROOT_URL}/${params.name}/${params.version}`,
    versions: (params: AssetParams) => `${Constants.ASSET_ROOT_URL}/${params.user}/${params.collection}/versions`,
    assetfile: (params: AssetFile) => `${Constants.ASSETFILE_ROOT_URL}/${params.key}`
};

export default class Remote {

    auth = new Auth();

    constructor() {
    }

    async getRestClient(): Promise<RestClient> {
        const token = await this.auth.getToken();
        const modelNestClient = new RestClient('model-nest-cli', Constants.HOST, [], {
            headers: {Authorization: `Bearer ${token}`}
        });
        return modelNestClient;
    }

    async getDownloadClient(): Promise<HttpClient> {
        const token = await this.auth.getToken();
        const modelNestClient = new HttpClient('model-nest-cli', [], {
            headers: {Authorization: `Bearer ${token}`, 'Accept-Encoding': 'gzip'}
        });
        return modelNestClient;
    }


    async upload(assetName: string, localPath: string) {
        const client = await this.getRestClient();

        if (!fs.existsSync(localPath)) {
            $log.error(`${localPath} does not exist`);
            return;
        }

        let files = [path.relative(path.dirname(localPath), localPath)];
        if (fs.lstatSync(localPath).isDirectory()) {
            const fileStats = klawSync(localPath);
            for(const file of fileStats) {
                if (isSourceAsset(file.path)) {
                    if (dirname(file.path) != resolve(localPath)) {
                        throw Error("Root asset file (ie. fbx or obj), must be in the root of the uploaded directory");
                    }
                }
            }
            files = fileStats.map(f=> path.relative(localPath, f.path));
        }
        $log.debug(files);

        $log.debug(Constants.HOST);
        const uploadClient: RestClient = new RestClient('model-nest-cli');
        try {
            const url = `assets/${encodeURIComponent(assetName)}/upload`;
            $log.debug(url);
            $log.debug(JSON.stringify({files}, null, 2));
            const uploadUrlsResp = await client.create<FileCollectionMap>(url, {files});
            $log.debug(uploadUrlsResp);
            const uploadUrls = uploadUrlsResp.result;
            if (uploadUrls != null) {
                for(const filePath of Object.keys(uploadUrls)) {
                    const absPath = path.join(localPath, filePath);
                    if (fs.lstatSync(absPath).isFile()) {
                        const uploadResult = await uploadClient.uploadStream('PUT', uploadUrls[filePath].url, fs.createReadStream(absPath), {
                            additionalHeaders: {
                                'content-type': lookup(absPath) || 'application/octet-stream'
                            }
                        });
                        $log.debug(uploadResult);
                    }
                }
            }

        } catch (err) {
            $log.error(`Unexpected error:\n ${JSON.stringify(err, null, 2)}`);
            return;
        }
    }

    async getAssetVersion(packageVersion: AssetVersion): Promise<Version|null> {
        const client = await this.getRestClient();
        const versionEndpoint = routes.version(packageVersion);
        const resp = await client.get<Version>(versionEndpoint);
        return resp.result;
    }



    async downloadAsset(nestDirectory: string, packageVersion: AssetVersion, version: Version) {
        const client = await this.getDownloadClient();
        const packageDir = join(nestDirectory, packageVersion.name);
        await ensureDir(packageDir);
        const format = version.formats.find(f => f.derived == false);
        const multi = new Multiprogress();
        const downloads = [];
        if (format) {
            for(const assetFile of format.resources) {
                if (assetFile.embedded) continue;

                const downloadPath = routes.assetfile(assetFile);
                const localPath = join(packageDir, assetFile.sourcePath);
                await ensureDir(dirname(localPath));
                let file: NodeJS.WritableStream = fs.createWriteStream(localPath);
                const downloadPromise = client.get(downloadPath);
                const promise = this.pipeToFile(assetFile, downloadPromise, file, multi);
                downloads.push(promise);
            }
        }

        await Promise.all(downloads);
        console.log('\n\nAsset download complete\n\n');
    }

    private async pipeToFile(asset: AssetFile,
                             downloadPromise: Promise<IHttpClientResponse>,
                             file: NodeJS.WritableStream, progressBar: any): Promise<string> {
        const resp = (await downloadPromise);
        const size = Number(resp.message.headers['content-length']);
        const str = progress({
            length: size,
            time: 100 /* ms */
        });

        const bar = progressBar.newBar(`  Downloading :asset [:bar] :percent :etas remaining`, {
            complete: '=',
            incomplete: ' ',
            total: asset.fileSize || size,
            width: 50,

        });

        str.on('data', (chunk: any) => {
            bar.tick(chunk.length, {
                asset: chalk.green(asset.sourcePath).padEnd(40, ' '),
            });
        });

        function gunzipPipe(stream: any, headers: IncomingHttpHeaders): Stream {
            if (headers['content-encoding'] === 'gzip') {
                return stream.pipe(zlib.createGunzip());
            } else {
                return stream;
            }
        }

        return new Promise((resolve, reject) => {
            gunzipPipe(resp.message.pipe(str), resp.message.headers)
                .pipe(file).on('close', () => {
                resolve(asset.sourcePath);
            });
        })

    }
}