import { webcrypto as crypto } from 'node:crypto';
import * as u8 from './u8.js';
import { ChallengeFailedError, FileDeletedError, FileExpiredError, FileNotFoundError, FileUploadError, RecipientVerificationFailedError, } from './errors.js';
import { decryptStream } from './ece.js';
import { transformStream, StreamSlicer } from './streams.js';
import { FILE_FLAGS } from './constants.js';
import retryWrap from 'fetch-retry';
import { Keychain } from './keychain.js';
import { ResponseError } from './api/index.js';
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
export class Download extends Keychain {
    sdk;
    fileId;
    progressCb;
    info;
    meta;
    abortController = new AbortController();
    fetch = retryWrap(fetch, {
        retries: 30,
        retryOn: function (attempt, error, response) {
            if (error !== null) {
                if (error.name === 'AbortError') {
                    return false;
                }
                return true;
            }
            else if ([400, 429, 503].includes(response?.status)) {
                return true;
            }
            return false;
        },
        retryDelay: function (attempt) {
            return Math.min(Math.pow(2, attempt) * 125, 2000);
        },
    });
    bytesDone = 0;
    startTime = 0;
    contentType;
    constructor(sdk, fileId, progressCb) {
        super();
        this.sdk = sdk;
        this.fileId = fileId;
        this.progressCb = progressCb;
    }
    get passwordRequired() {
        if (!this.info) {
            throw new Error('Info not loaded');
        }
        return (this.info.flags & FILE_FLAGS.PASSWORD) !== 0;
    }
    get verifyRecipient() {
        if (!this.info) {
            throw new Error('Info not loaded');
        }
        return (this.info.flags & FILE_FLAGS.VERIFY_RECIPIENT) !== 0;
    }
    get urlFragment() {
        if (!this.info) {
            throw new Error('Info not loaded');
        }
        return (this.info.flags & FILE_FLAGS.URL_FRAGMENT) !== 0;
    }
    get sharedSecret() {
        if (!this.info) {
            throw new Error('Info not loaded');
        }
        return (this.info.flags & FILE_FLAGS.SHARED_SECRET) !== 0;
    }
    abort() {
        this.abortController.abort();
    }
    async getInfo() {
        try {
            this.info = await this.sdk.filesApi.getFile({
                fileId: this.fileId,
                expand: ['invite', 'user'],
            });
            if (this.info.state === 'upload_started') {
                throw new FileUploadError();
            }
            else if (this.info.state === 'deleted') {
                throw new FileDeletedError();
            }
            else if (!this.info.salt) {
                throw new Error('File response does not contain a salt value');
            }
            this.salt = u8.fromBasedString('hex', this.info.salt);
            if (this.info.publicKey) {
                await this.setPeerPublicKeyHex(this.info.publicKey);
            }
            return this.info;
        }
        catch (err) {
            if (err instanceof ResponseError) {
                if (err.response.status === 404) {
                    throw new FileNotFoundError();
                }
                else if (err.response.status === 410) {
                    throw new FileExpiredError();
                }
            }
            throw err;
        }
    }
    async getMeta() {
        if (!this.authKey)
            await this.keygen();
        await this.getInfo();
        const trebuchetChallenge = await this.signNonce();
        let res;
        try {
            res = await this.sdk.filesApi.getMetaUrl({
                fileId: this.fileId,
                trebuchetChallenge,
            });
        }
        catch (err) {
            if (err instanceof ResponseError && err.response.status === 403) {
                const body = await err.response.json();
                if (body.code === 'files_email_verification_list') {
                    throw new RecipientVerificationFailedError();
                }
                throw new ChallengeFailedError();
            }
            throw err;
        }
        const response = await this.fetch(res.url, {
            signal: this.abortController.signal,
        });
        const rawMeta = await response.arrayBuffer();
        this.meta = await this.decryptMetadata(rawMeta);
        return this.meta;
    }
    async getContent(path) {
        if (!this.authKey)
            await this.keygen();
        if (!this.meta)
            throw new Error('Metadata not available');
        if (!this.contentType)
            throw new Error('Content type not known');
        if (!this.rawContentKey)
            throw new Error('Content key not available');
        await this.getInfo();
        const trebuchetChallenge = await this.signNonce();
        const res = await this.sdk.filesApi.getContentUrl({
            fileId: this.fileId,
            trebuchetChallenge,
        });
        const response = await this.fetch(res.url, {
            signal: this.abortController.signal,
        });
        if (!response.body) {
            throw new Error('Response body not available');
        }
        this.startTime = performance.now();
        // @ts-expect-error with the type of response.body
        const plainStream = decryptStream(response.body, this.rawContentKey);
        const reslicedStream = transformStream(plainStream, new StreamSlicer(1024 * 1024, (bytes) => {
            this.bytesDone += bytes;
            if (this.progressCb) {
                const ms = performance.now() - this.startTime;
                const rate = (this.bytesDone * 8) / (ms * 1000);
                this.progressCb(this.bytesDone, rate);
            }
        }));
        const fileStream = createWriteStream(path);
        Readable.fromWeb(reslicedStream).pipe(fileStream);
        await new Promise((resolve) => fileStream.on('finish', resolve));
    }
    async signNonce() {
        if (!this.info)
            throw new Error('Info not loaded');
        if (!this.info.nonce)
            throw new Error('File response did not contain a nonce value');
        if (!this.authKey)
            throw new Error('Auth key not available');
        const data = await crypto.subtle.sign('HMAC', this.authKey, u8.fromBasedString('hex', this.info.nonce));
        return u8.toBasedString('hex', data);
    }
    async decryptMetadata(encryptedMetadata) {
        if (!this.salt)
            throw new Error('Salt not set');
        if (!this.metaKey)
            throw new Error('Meta key not available');
        const data = await crypto.subtle.decrypt({
            name: 'AES-GCM',
            iv: this.salt.slice(0, 12),
            tagLength: 128,
        }, this.metaKey, encryptedMetadata);
        const meta = JSON.parse(new TextDecoder().decode(data));
        this.contentType = meta.type;
        return meta;
    }
}
