import {S3Config} from "../bindings";
import {sha256} from "js-sha256";

export interface S3Params {
    url: string,
    query: string,
    host: string,
    region: string,
    service: string,
    method: string,
    accessKeyId: string,
    secretAccessKey: string,
    sessionToken: string,
    dateNow: string,
    datetimeNow: string
}

export interface S3PayloadParams {
    contentHash: string | null,
    contentType: string | null
}

const getHTTPHost = function (config : S3Config | undefined, url : string, bucket : string) : string {
    if (config?.endpoint?.startsWith("http")) {
        // Endpoint is a full url, we append the bucket
        const httpHost = `${config?.endpoint}`;
        const offset = httpHost.indexOf("://")+3;
        return httpHost.substring(offset);
    } else if (config?.endpoint) {
        // Endpoint is not a full url and the https://{bucket}.{domain} format will be used
        return `${bucket}.${config?.endpoint}`;
    } else {
        // Default aws s3 url
        return `${bucket}.s3.amazonaws.com`;
    }
}

export function getS3Params (config : S3Config | undefined, url: string, method : string) : S3Params {
    const parsedS3Url = parseS3Url(url);

    // when using S3 path-style access, the signed URL should also include the bucket name, 
    //  as it is present in the HTTP URL path.
    // See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html#path-style-url-ex
    let path =  parsedS3Url.path;
    if (isPathStyleAccess(config)) {
        path = `/${parsedS3Url.bucket}${path}`;
    }
    return {
        url: path,
        query: "",
        host: getHTTPHost(config, url, parsedS3Url.bucket),
        region: (config?.region) ?? "",
        service: "s3",
        method: method,
        accessKeyId: (config?.accessKeyId) ?? "",
        secretAccessKey: (config?.secretAccessKey) ?? "",
        sessionToken: (config?.sessionToken) ?? "",
        dateNow: new Date().toISOString().replace(/-/g,'').split('T')[0],
        datetimeNow: new Date().toISOString().replace(/-/g,'').replace(/:/g,'').split('.')[0]+ 'Z',
    };
}

export function uriEncode(input : string, encode_slash = false) {
    // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html

    const hexDigit = "0123456789ABCDEF";
    let result = "";

    for (let i = 0; i < input.length; i++) {
        const ch : string = input[i];

        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' ||
            ch == '-' || ch == '~' || ch == '.') {
            result += ch;
        } else if (ch == '/') {
            if (encode_slash) {
                result += "%2F";
            } else {
                result += ch;
            }
        } else {
            result += "%";
            result += hexDigit[ch.charCodeAt(0) >> 4];
            result += hexDigit[ch.charCodeAt(0) & 15];
        }
    }
    return result;
}

export function createS3Headers(params: S3Params, payloadParams : S3PayloadParams | null = null) : Map<string, string> {
    // this is the sha256 of the empty string, its useful since we have no payload for GET requests
    const payloadHash = (payloadParams?.contentHash) ?? "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

    const res = new Map<string, string>();
    // res.set("host", params.host)
    res.set("x-amz-date", params.datetimeNow);
    res.set("x-amz-content-sha256", payloadHash);

    if (params.sessionToken) {
        res.set("x-amz-security-token", params.sessionToken);
    }

    // construct string to sign
    let signedHeaders = "";
    if (payloadParams?.contentType) {
        signedHeaders += "content-type;";
    }
    signedHeaders += "host;x-amz-content-sha256;x-amz-date";
    if (params.sessionToken) {
        signedHeaders += ";x-amz-security-token";
    }

    let canonicalRequest = params.method + "\n" + uriEncode(params.url) + "\n" + params.query;
    if (payloadParams?.contentType) {
        canonicalRequest += "\ncontent-type:" + payloadParams?.contentType;
    }
    canonicalRequest += "\nhost:" + params.host +
        "\nx-amz-content-sha256:" + payloadHash + "\nx-amz-date:" + params.datetimeNow;
    if (params.sessionToken && params.sessionToken.length > 0) {
        canonicalRequest += "\nx-amz-security-token:" + params.sessionToken;
    }

    canonicalRequest += "\n\n" + signedHeaders + "\n" + payloadHash;
    const canonicalRequestHashStr = sha256(canonicalRequest);

    const stringToSign = "AWS4-HMAC-SHA256\n" + params.datetimeNow + "\n" + params.dateNow + "/" + params.region + "/" + params.service +
        "/aws4_request\n" + canonicalRequestHashStr;

    // ts-ignore's because library can accept array buffer as key, but TS arg is incorrect
    const signKey = "AWS4" + params.secretAccessKey;
    const kDate = sha256.hmac.arrayBuffer(signKey, params.dateNow);

    // Note, js-sha256 has a bug in the TS interface that only supports strings as keys, while we need a bytearray
    // as key. PR is open but unmerged: https://github.com/emn178/js-sha256/pull/25
    // eslint-disable-next-line
    // @ts-ignore
    const kRegion = sha256.hmac.arrayBuffer(kDate, params.region);
    // eslint-disable-next-line
    // @ts-ignore
    const kService = sha256.hmac.arrayBuffer(kRegion, params.service,);
    // eslint-disable-next-line
    // @ts-ignore
    const signingKey = sha256.hmac.arrayBuffer(kService, "aws4_request");
    // eslint-disable-next-line
    // @ts-ignore
    const signature = sha256.hmac(signingKey, stringToSign);

    res.set("Authorization", "AWS4-HMAC-SHA256 Credential=" + params.accessKeyId + "/" + params.dateNow + "/" + params.region + "/" +
        params.service + "/aws4_request, SignedHeaders=" + signedHeaders +
        ", Signature=" + signature);

    return res;
}

const createS3HeadersFromS3Config = function (config : S3Config | undefined, url : string, method : string, contentType: string | null = null, payload : Uint8Array | null = null) : Map<string, string> {
    const params = getS3Params(config, url, method);
    const payloadParams = {
        contentType: contentType,
        contentHash: payload ? sha256.hex(payload!) : null
    } as S3PayloadParams;
    return createS3Headers(params, payloadParams);
}

export function addS3Headers(xhr: XMLHttpRequest, config : S3Config | undefined, url : string, method: string, contentType: string | null = null, payload : Uint8Array | null = null) {
    if (config?.accessKeyId || config?.sessionToken) {
        const headers = createS3HeadersFromS3Config(config, url, method, contentType, payload);
        headers.forEach((value: string, header: string) => {
            xhr.setRequestHeader(header, value);
        });

        if (contentType) {
            xhr.setRequestHeader("content-type", contentType);
        }
    }
}

export function parseS3Url (url: string) : {bucket : string, path : string} {
    if (url.indexOf("s3://") != 0) {
        throw new Error("URL needs to start with s3://");
    }
    const slashPos = url.indexOf('/', 5);

    if (slashPos == -1) {
        throw new Error("URL needs to contain a '/' after the host");
    }

    const bucket = url.substring(5, slashPos);
    if (!bucket) {
        throw new Error("URL needs to contain a bucket name");
    }
    const path = url.substring(slashPos);
    if (!path) {
        throw new Error("URL needs to contain key");
    }

    return {bucket: bucket, path: path}
}

function isPathStyleAccess(config : S3Config | undefined) : boolean {
    if (config?.endpoint?.startsWith("http")) {
        return true
    }
    return false
}

export function getHTTPUrl(config : S3Config | undefined, url : string) : string {
    const parsedUrl = parseS3Url(url);
    if (isPathStyleAccess(config)) {
        // Endpoint is a full url, we append the bucket
        return `${config?.endpoint}/${parsedUrl.bucket}` + parsedUrl.path;
    }
    return 'https://' + getHTTPHost(config, url, parsedUrl.bucket) + parsedUrl.path;
}