import bytes from '@root/encoding/bytes.js';
import hex from '@root/encoding/hex.js';
import { Address, Script, Tx } from '@ts-bitcoin/core';
import axios from 'axios';
import WS from 'isomorphic-ws';
import { Envelope, util } from 'univrse';
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
// nature restoration fixed values
const NATURE_SATS_PER_TREE = 23500000;
export const NATURE_ADDRESS = '1co2d2DkpSHd8eHCL8X1kcD2vKt5yAYwx';
import { MB_DERIVING_KEY, MB_SIGNING_KEY, OutputType, } from './types.js';
import { DocV1 } from './envelopes/doc/v1.js';
import { pigiV2 } from './envelopes/envelopes.js';
import { Hash } from './envelopes/hash/v1.js';
import { PeppolV1 } from './envelopes/peppol/v1.js';
import * as utils from './utils.js';
const DEFAULT_OPTIONS = {
    url: 'https://api.mintblue.com',
    version: '2',
    SDKTokenPrefix: 'secret-token:mintBlue.com/sdk/',
    ServerTokenPrefix: 'secret-token:mintBlue.com/api/',
};
/**
 * The main SDK class. Instantiate this class using the Mintblue.create() function
 *
 * ```ts
 *   const token = 'YOUR SDK TOKEN';
 *   const project_id = 'YOUR PROJECT ID*
 *   const client = await Mintblue.create({ token: token *
 *   const outputs = [
 *     {
 *       type: 'data',
 *       value: 'Hello world',
 *       sign: true,
 *       encrypt: true,
 *     },
 *   ];
 *
 *   const { txid, rawtx } = await client.createTransaction({ project_id, outputs });
 *   ```
 * @category SDK
 */
export class Mintblue {
    // private keys: Keys;
    // Supply token as 'server-token'
    /**
     * @ignore
     */
    constructor(config) {
        this.config = config;
        this.api =
            this.config.mockApi ||
                axios.create({
                    headers: {
                        Authorization: 'Bearer ' + this.config.apiToken,
                    },
                    baseURL: `${this.config.url}/api/v${this.config.version}/`,
                });
    }
    /**
     * Instantiate client with accesstoken (SDK-token)
     *
     * @param options needs to contain an SDK token
     * @returns MintBlue SDK instance
     */
    static async create(options) {
        const opts = { ...DEFAULT_OPTIONS, ...options };
        const { tokenId, tokenSecret, bearerToken } = await utils.parseSDKToken(opts.token, opts.SDKTokenPrefix, opts.ServerTokenPrefix);
        const userKeyAttributes = opts.userKeyAttributes ||
            (await axios
                .get(`${opts.url}/api/v${opts.version}/accesstoken/${tokenId}`, {
                headers: {
                    authorization: `Bearer ${bearerToken}`,
                },
            })
                .then((res) => res.data.data.userKeyAttributes));
        const keys = await utils.getKeysFromUserKeyAttributes(tokenSecret, userKeyAttributes);
        const config = {
            keys,
            apiToken: bearerToken,
            url: opts.url || DEFAULT_OPTIONS.url,
            version: opts.version || DEFAULT_OPTIONS.version,
            mockApi: options.mockApi || null,
        };
        return new Mintblue(config);
    }
    /**
     * Instantiate client with login (e-mail and password)
     *
     * @param options LoginOptions
     * @returns MintBlue SDK instance
     */
    static async login(options) {
        options = { ...DEFAULT_OPTIONS, ...options };
        const hashedPassword = await utils.sha256s(options.password);
        const res = await axios
            .post(`${options.url}/api/v${options.version}/auth`, {
            email: options.email,
            password: hashedPassword,
        })
            .then((res) => res.data);
        const userKeyAttributes = res.userKeyAttributes;
        const keys = await utils.getKeysFromUserKeyAttributes(options.password, userKeyAttributes);
        const o = options;
        delete o.password;
        delete o.email;
        return new Mintblue({ token: res.token, userKeyAttributes, keys, ...o });
    }
    /**
     * Create access token
     *
     * @param options
     * @returns SDK token & API token
     */
    async createAccesstoken(options) {
        const secret = (await import('nanoid')).customAlphabet(alphabet, 20)();
        const hashedSecret = await utils.sha256s(secret);
        const { userKeyAttributes } = await utils.generateUserKeyAttributes(secret, this.config.keys);
        const body = {
            ...options,
            secret: hashedSecret,
            userKeyAttributes,
        };
        const res = await this.api.post('accesstoken', body);
        const id = res.data.data.accesstoken.id;
        // TODO: return sdkToken and apiToken
        return {
            sdkToken: `${DEFAULT_OPTIONS.SDKTokenPrefix}${id}.${secret}`,
            apiToken: `${DEFAULT_OPTIONS.ServerTokenPrefix}${id}.${hashedSecret}`,
        };
    }
    /**
     * @ignore
     */
    async createPigi(fileContent) {
        return this.api
            .postForm('pigi/upload', {
            file: new Blob([fileContent]),
        })
            .then((res) => res.data);
    }
    /**
     * @ignore
     */
    async createAPIOptions(options) {
        const convertedKey = util.fromBsvPrivKey(this.config.keys.privateKey);
        const apiOptions = {
            ...options,
            outputs: [],
        };
        apiOptions.outputs = await Promise.all(options.outputs.map(async (output) => {
            if (output.type === 'data' || output.type === 'file') {
                const nonce = await utils.generateSaltToDeriveKey(16);
                const headers = output.type === 'file'
                    ? {
                        proto: 'mintBlue',
                        meta: { type: 'file', contentType: output.value.contentType },
                    }
                    : { proto: 'mintBlue', meta: {} };
                const env = Envelope.wrap(output.value, headers);
                // TODO: determine algos
                if (output.sign) {
                    await env.sign(convertedKey, { alg: 'ES256K' });
                }
                if (output.encrypt) {
                    // TODO: derive key
                    await env.encrypt(convertedKey.toPublic(), { alg: 'ECDH-ES+A128GCM', nonce });
                }
                delete output.sign;
                delete output.encrypt;
                return { type: 'env', value: env.toString() };
            }
            else if (output.type === OutputType.pigi) {
                if (output.version === 1) {
                    //
                }
                else if (output.version === 2) {
                    const signingKey = await utils.crypto.subtle.importKey('jwk', output.signingKey, {
                        name: 'ECDSA',
                        namedCurve: 'P-256',
                    }, true, ['sign']);
                    const p = await pigiV2.create(output.value, [signingKey], output.options);
                    // Todo remove @bitcoin-ts/core in favor of bsv wasm or nimble to get rid of these buffers
                    const script = Script.fromSafeDataArray([
                        Buffer.from(bytes.strToBuf(pigiV2.PROTOCOL_ID)),
                        Buffer.from(p.encryptedData),
                    ]);
                    return { type: 'script', encoding: 'hex', value: script.toHex() };
                }
            }
            else if (output.type == OutputType.peppol) {
                if (output.version !== 1) {
                    throw new Error('Peppol unsupported version. Try version 1');
                }
                const p = await PeppolV1.create(output);
                return { type: 'script', encoding: 'hex', value: p.toOpReturnScriptHex() };
            }
            else if (output.type == OutputType.doc) {
                const docv1 = await DocV1.create(output);
                return { type: 'script', encoding: 'hex', value: docv1.toOpReturnScriptHex() };
            }
            else if (output.type === OutputType.hash) {
                const hashed = await Hash.create({ ...output, key: convertedKey });
                return { type: 'script', encoding: 'hex', value: hashed.toOpReturnScriptHex() };
            }
            else if (output.type === OutputType.tree) {
                if (output.trees < 0.01) {
                    throw new Error('Must be at least 0.01 trees.');
                }
                return { type: 'payment', to: NATURE_ADDRESS, satoshis: Math.ceil(output.trees * NATURE_SATS_PER_TREE) };
            }
            else if (output.type === OutputType.script) {
                if (!output.encoding) {
                    output.encoding = 'hex';
                }
                return { type: 'script', encoding: output.encoding, value: output.value };
            }
            else {
                if (!Object.values(OutputType).includes(output.type)) {
                    throw new Error('Unsupported output type');
                }
            }
            return output;
        }));
        return apiOptions;
    }
    /**
     * Create a blockchain transaction
     *
     * @param options
     * @returns Published transaction
     */
    async createTransaction(options) {
        const apiOptions = await this.createAPIOptions(options);
        return this.api.post('transaction', apiOptions).then((res) => res.data.data);
    }
    async getTransaction(options) {
        const res = await this.api.get(`transaction/${options.txid}`);
        const tx = res.data.data;
        // Even when default options now points to version 2 there's nothing that stops the user to request v1 data, hence a compatibility check.
        const rawtx = this.config.version === '1' ? hex.bufToHex(new Uint8Array(tx.rawtx.data)) : tx.rawtx;
        if (options.parse) {
            tx.outputs = await this.parseTransaction(options.txid, rawtx, options.secret);
        }
        return tx;
    }
    /**
     * @ignore
     */
    async parseTransaction(txid, rawtx, secret) {
        const convertedKey = util.fromBsvPrivKey(this.config.keys.privateKey);
        const bsvTx = Tx.fromHex(rawtx);
        const outputs = [];
        for (const txOut of bsvTx.txOuts) {
            outputs.push(await this.parseOutput(txid, txOut, secret ? secret : convertedKey));
        }
        return outputs;
    }
    /**
     * List transactions
     *
     * @param options
     * @returns Array of transactions (without payload)
     */
    async listTransactions(options) {
        var _a, _b;
        const params = {
            ...options,
            start_date: (_a = options.startDate) === null || _a === void 0 ? void 0 : _a.toISOString(),
            end_date: (_b = options.startDate) === null || _b === void 0 ? void 0 : _b.toISOString(),
        };
        return this.api.get('transaction', { params }).then((res) => res.data.data);
    }
    /**
     * Stream Project (experimental)
     *
     * This function allows you to stream experimental data from a project.
     *
     * @param {experimentalStreamProjectOptions} options - The options for streaming experimental data.
     * @returns {Promise<{ close: () => void }>} A promise that resolves with an object containing a `close` method.
     * The `close` method can be used to gracefully close the streaming connection.
     *
     * @experimental This function is experimental and subject to change. Use it with caution.
     */
    async experimentalStreamProject(options) {
        const queue = new (await import('p-queue')).default({ concurrency: 1 });
        const url = new URL(this.config.url);
        const token = this.api.defaults.headers.Authorization.split(DEFAULT_OPTIONS.ServerTokenPrefix)[1];
        const params = new URLSearchParams({
            project_id: options.project_id,
            ...(options.history ? { history: '1' } : {}),
            ...(options.last_event_id ? { last_event_id: options.last_event_id } : {}),
        });
        const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
        const ws = new WS(`${protocol}//${url.host}/ws/project?${params.toString()}`, token);
        ws.onerror = (e) => {
            options.onError(e);
        };
        ws.onmessage = (ev) => {
            const tx = JSON.parse(ev.data);
            queue.add(() => options.onTransaction(tx));
        };
        return { close: ws.close };
    }
    /**
     * Create a project
     *
     * @param options
     * @returns Array of transactions (without payload)
     */
    async createProject(options) {
        const response = await this.api.post('project', options);
        return response.data.data;
    }
    /**
     * Retrieves project data with the specified ID.
     * @param {Object} params - The parameters for retrieving the project.
     * @param {string} params.id - The ID of the project to retrieve.
     * @returns {Promise<Project>} A promise that resolves with the retrieved project data.
     */
    async getProject(options) {
        const response = await this.api.get(`project/${options.id}`);
        return response.data.data;
    }
    /**
     * Updates a project with the specified ID using the provided data.
     * @param {Object} params - The parameters for updating the project.
     * @param {string} params.id - The ID of the project to update.
     * @param {UpdateProjectOptions} params.data - The data to update the project with.
     * @returns {Promise<Project>} A promise that resolves with the updated project data.
     */
    async updateProject(options) {
        const response = await this.api.patch(`project/${options.id}`, options.data);
        return response.data.data;
    }
    /**
     * Retrieves a list of projects.
     * @returns {Promise<Project[]>} A promise that resolves with an array of project data.
     */
    async listProjects() {
        const response = await this.api.get('project');
        return response.data.data;
    }
    /**
     * Deletes a project with the specified ID.
     * @param {Object} params - The parameters for deleting the project.
     * @param {string} params.id - The ID of the project to delete.
     * @returns {Promise<void>} A promise that resolves when the project is deleted.
     */
    async destroyProject(options) {
        await this.api.delete(`project/${options.id}`);
    }
    /**
     * Retrieves a list of event listeners.
     * @returns {Promise<EventListener[]>} A promise that resolves with an array of event listener data.
     */
    async listEventListeners() {
        const response = await this.api.get('subscription');
        return response.data.data;
    }
    /**
     * Retrieves event listener data with the specified ID.
     * @param {string} id - The ID of the event listener to retrieve.
     * @returns {Promise<EventListener>} A promise that resolves with the retrieved event listener data.
     */
    async getEventListener(options) {
        const response = await this.api.get(`subscription/${options.id}`);
        return response.data.data;
    }
    /**
     * Creates a new event listener.
     * @param {EventListener} data - The data for creating the event listener.
     * @returns {Promise<EventListener>} A promise that resolves with the created event listener data.
     */
    async createEventListener(data) {
        const response = await this.api.post('subscription', data);
        return response.data.data;
    }
    /**
     * @experimental
     * Get Peppol Key information
     * @returns Object containing uncompressed public key in hex format. \
     * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
     */
    experimentalGetPeppolKeyInfo() {
        const key = this.config.keys.additionalKeys.find((x) => x.info === 'peppol');
        const uncompressedPublicKey = utils.JWKToUncompressedHex(key.jwk);
        return {
            uncompressedPublicKey,
            compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
            jwk: key.jwk,
            kid: key.kid,
            info: key.info,
        };
    }
    /**
     * @experimental
     * Get Peppol Key information
     * @returns Object containing uncompressed public key in hex format. \
     * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
     */
    async experimentalGetKVKKeyInfo() {
        const key = this.config.keys.additionalKeys.find((x) => x.info === 'kvk');
        if (!key) {
            throw new Error('KVK key not found');
        }
        const uncompressedPublicKey = utils.JWKToUncompressedHex(key.jwk);
        return {
            uncompressedPublicKey,
            compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
            jwk: key.jwk,
            kid: key.kid,
            info: key.info,
            SHA1Fingerprint: await utils.getSHA1Fingerprint(key.jwk),
        };
    }
    /**
     * @experimental
     * Get Signing Key information
     * @returns Object containing uncompressed public key in hex format. \
     * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
     */
    async experimentalGetSigningKeyInfo() {
        const key = this.config.keys.additionalKeys.find((x) => x.info === MB_SIGNING_KEY);
        if (!key) {
            throw new Error('Signing key not found');
        }
        const uncompressedPublicKey = utils.JWKToUncompressedHex(key.jwk);
        return {
            uncompressedPublicKey,
            compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
            jwk: key.jwk,
            kid: key.kid,
            info: key.info,
            SHA1Fingerprint: await utils.getSHA1Fingerprint(key.jwk),
        };
    }
    /**
     * @experimental
     * Get Derive Key information
     * @returns Object containing uncompressed public key in hex format. \
     * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
     */
    async experimentalGetDeriveKeyInfo() {
        const key = this.config.keys.additionalKeys.find((x) => x.info === MB_DERIVING_KEY);
        if (!key) {
            throw new Error('Derive key not found');
        }
        const uncompressedPublicKey = utils.JWKToUncompressedHex(key.jwk);
        return {
            uncompressedPublicKey,
            compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
            jwk: key.jwk,
            kid: key.kid,
            info: key.info,
            SHA1Fingerprint: await utils.getSHA1Fingerprint(key.jwk),
        };
    }
    async queryTransactions(options) {
        const response = await this.api.post('transaction/query', options);
        return response.data.data;
    }
    /**
     * @ignore
     */
    async parseOutput(_txid, txOut, secret) {
        const outputType = utils.identifyOutput(txOut);
        let output;
        if (outputType === OutputType.data || outputType === OutputType.file) {
            let envelope = Envelope.fromScript(txOut.script);
            output = { type: OutputType[outputType], encrypt: false, sign: false };
            if (envelope.recipient) {
                envelope = await envelope.decrypt(secret);
                output.encrypt = true;
            }
            if (envelope.signature) {
                // TODO: verify
                output.sign = true;
            }
            output.value = envelope.payload;
        }
        else if (outputType === OutputType.payment) {
            const address = Address.fromTxOutScript(txOut.script).toString();
            output = { type: OutputType[outputType], to: address, satoshis: txOut.valueBn.toNumber() };
        }
        else if (outputType === OutputType.peppol) {
            const key = this.config.keys.additionalKeys.find((key) => {
                return key.info === 'peppol';
            });
            if (!key) {
                throw new Error('Peppol key not found');
            }
            const p = await PeppolV1.fromOpReturnScriptHex(txOut.script.toHex(), {
                privateKey: key.jwk,
            });
            output = p.info;
        }
        else if (outputType === OutputType.doc) {
            const data = txOut.script.chunks[3].buf;
            if (!data) {
                throw new Error('Could not get script chunk [3] from output');
            }
            const key = this.config.keys.additionalKeys.find((key) => {
                return key.info === MB_DERIVING_KEY;
            });
            if (!key) {
                throw new Error('Could not key MB_DERIVING_KEY');
            }
            // For auto parsing lets just decrypt with some key we have
            // TODO provide ways to parse an output more completely with additional keys and interactivity
            const payload = await DocV1.parse({
                encryptedData: data,
                receiverKey: key.jwk,
                secret1: null,
                secret2: null,
                verifyKey: null,
            });
            return payload.info;
        }
        else if (outputType === OutputType.hash) {
            const hash = await Hash.fromOpReturnScript(txOut.script, secret);
            return hash.info;
        }
        else if (outputType === OutputType.tree) {
            output = { type: OutputType[outputType], trees: txOut.valueBn.toNumber() / NATURE_SATS_PER_TREE };
        }
        else {
            throw new Error('Could not automatically parse output');
        }
        return output;
    }
}
//# sourceMappingURL=mintblue.js.map