import * as crypto from "node:crypto";
import jsonld from "jsonld";
import { CONTEXTS, WellKnownContext } from "./contexts.js";
import fetch from "node-fetch";
import { httpAgent, httpsAgent } from "../../../misc/fetch.js";
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
export class LdSignature {
    debug = false;
    preLoad = true;
    loaderTimeout = 10 * 1000;
    async signRsaSignature2017(data, privateKey, creator, domain, created) {
        const options = {
            type: "RsaSignature2017",
            creator,
            domain,
            nonce: crypto.randomBytes(16).toString("hex"),
            created: (created || new Date()).toISOString()
        };
        if (!domain) {
            options.domain = undefined;
        }
        const toBeSigned = await this.createVerifyData(data, options);
        const signer = crypto.createSign("sha256");
        signer.update(toBeSigned);
        signer.end();
        const signature = signer.sign(privateKey);
        return {
            ...data,
            signature: {
                ...options,
                signatureValue: signature.toString("base64")
            }
        };
    }
    async verifyRsaSignature2017(data, signature, publicKey) {
        const toBeSigned = await this.createVerifyData(data, signature);
        const verifier = crypto.createVerify("sha256");
        verifier.update(toBeSigned);
        return verifier.verify(publicKey, signature.signatureValue, "base64");
    }
    async createVerifyData(data, options) {
        const transformedOptions = {
            ...options,
            "@context": "https://w3id.org/identity/v1"
        };
        delete transformedOptions["type"];
        delete transformedOptions["id"];
        delete transformedOptions["signatureValue"];
        const canonizedOptions = await this.normalize(transformedOptions);
        const optionsHash = this.sha256(canonizedOptions);
        const transformedData = {
            ...data
        };
        const cannonidedData = await this.normalize(transformedData);
        if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
        const documentHash = this.sha256(cannonidedData);
        const verifyData = `${optionsHash}${documentHash}`;
        return verifyData;
    }
    async normalize(data) {
        const customLoader = this.getLoader();
        return await jsonld.normalize(data, {
            documentLoader: customLoader
        });
    }
    async compactToWellKnown(data) {
        const options = {
            documentLoader: this.getLoader()
        };
        const context = WellKnownContext;
        return await jsonld.compact(data, context, options);
    }
    getLoader() {
        return async (url)=>{
            if (!url.match("^https?://")) throw new Error(`Invalid URL ${url}`);
            if (this.preLoad) {
                if (url in CONTEXTS) {
                    if (this.debug) console.debug(`HIT: ${url}`);
                    return {
                        contextUrl: null,
                        document: CONTEXTS[url],
                        documentUrl: url
                    };
                }
            }
            if (this.debug) console.debug(`MISS: ${url}`);
            const document = await this.fetchDocument(url);
            return {
                contextUrl: null,
                document: document,
                documentUrl: url
            };
        };
    }
    async fetchDocument(url) {
        const json = await fetch(url, {
            headers: {
                Accept: "application/ld+json, application/json"
            },
            size: 1024 * 1024,
            // TODO
            //timeout: this.loaderTimeout,
            agent: (u)=>u.protocol === "http:" ? httpAgent : httpsAgent
        }).then((res)=>{
            if (!res.ok) {
                throw new Error(`${res.status} ${res.statusText}`);
            } else {
                return res.json();
            }
        });
        return json;
    }
    sha256(data) {
        const hash = crypto.createHash("sha256");
        hash.update(data);
        return hash.digest("hex");
    }
    containsForbiddenDirectives(doc) {
        if (typeof doc === "object" && doc !== null) {
            if (Array.isArray(doc)) {
                for (const item of doc){
                    if (this.containsForbiddenDirectives(item)) {
                        return true;
                    }
                }
            } else {
                for (const [key, value] of Object.entries(doc)){
                    if ([
                        "@included",
                        "@graph",
                        "@reverse"
                    ].includes(key)) {
                        return true;
                    }
                    if (typeof value === "object" && value !== null) {
                        if (this.containsForbiddenDirectives(value)) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}
