"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentityClient = void 0;
const index_js_1 = require("./types/index.js");
const index_js_2 = require("../wallet/index.js");
const index_js_3 = require("../transaction/index.js");
const Certificate_js_1 = __importDefault(require("../auth/certificates/Certificate.js"));
const index_js_4 = require("../script/index.js");
const index_js_5 = require("../primitives/index.js");
const index_js_6 = require("../overlay-tools/index.js");
const ContactsManager_js_1 = require("./ContactsManager.js");
/**
 * IdentityClient lets you discover who others are, and let the world know who you are.
 */
class IdentityClient {
    constructor(wallet, options = index_js_1.DEFAULT_IDENTITY_CLIENT_OPTIONS, originator) {
        this.options = options;
        this.originator = originator;
        this.originator = originator;
        this.wallet = wallet ?? new index_js_2.WalletClient();
        this.contactsManager = new ContactsManager_js_1.ContactsManager(this.wallet, this.originator);
    }
    /**
     * Publicly reveals selected fields from a given certificate by creating a publicly verifiable certificate.
     * The publicly revealed certificate is included in a blockchain transaction and broadcast to a federated overlay node.
     *
     * @param {Certificate} certificate - The master certificate to selectively reveal.
     * @param {CertificateFieldNameUnder50Bytes[]} fieldsToReveal - An array of certificate field names to reveal. Only these fields will be included in the public certificate.
     *
     * @returns {Promise<object>} A promise that resolves with the broadcast result from the overlay network.
     * @throws {Error} Throws an error if the certificate is invalid, the fields cannot be revealed, or if the broadcast fails.
     */
    async publiclyRevealAttributes(certificate, fieldsToReveal) {
        if (Object.keys(certificate.fields).length === 0) {
            throw new Error('Public reveal failed: Certificate has no fields to reveal!');
        }
        if (fieldsToReveal.length === 0) {
            throw new Error('Public reveal failed: You must reveal at least one field!');
        }
        try {
            const masterCert = new Certificate_js_1.default(certificate.type, certificate.serialNumber, certificate.subject, certificate.certifier, certificate.revocationOutpoint, certificate.fields, certificate.signature);
            await masterCert.verify();
        }
        catch (_certVerificationError) {
            // Low-level cert error details are suppressed — surface a user-facing message only
            throw new Error('Public reveal failed: Certificate verification failed!');
        }
        // Given we already have a master certificate from a certifier,
        // create an anyone verifiable certificate with selectively revealed fields
        const { keyringForVerifier } = await this.wallet.proveCertificate({
            certificate,
            fieldsToReveal,
            verifier: new index_js_5.PrivateKey(1).toPublicKey().toString()
        }, this.originator);
        // Build the lockingScript with pushdrop.create() and the transaction with createAction()
        const lockingScript = await new index_js_4.PushDrop(this.wallet, this.originator).lock([
            index_js_5.Utils.toArray(JSON.stringify({ ...certificate, keyring: keyringForVerifier }))
        ], this.options.protocolID, this.options.keyID, 'anyone', true, true);
        // Consider verification and if this is necessary
        // counterpartyCanVerifyMyOwnership: true
        const { tx } = await this.wallet.createAction({
            description: 'Create a new Identity Token',
            outputs: [
                {
                    satoshis: this.options.tokenAmount,
                    lockingScript: lockingScript.toHex(),
                    outputDescription: 'Identity Token'
                }
            ],
            options: {
                randomizeOutputs: false
            }
        }, this.originator);
        if (tx !== undefined) {
            // Submit the transaction to an overlay
            const broadcaster = new index_js_6.TopicBroadcaster(['tm_identity'], {
                networkPreset: (await this.wallet.getNetwork({})).network
            });
            return await broadcaster.broadcast(index_js_3.Transaction.fromAtomicBEEF(tx));
        }
        throw new Error('Public reveal failed: failed to create action!');
    }
    /**
     * Resolves displayable identity certificates, issued to a given identity key by a trusted certifier.
     *
     * By default, contacts are consulted first and the overlay is only queried on a contacts miss.
     * Pass `{ parallel: true }` to fire both in parallel (the legacy behavior, useful when callers
     * specifically want a fresh overlay answer even when a contact exists).
     *
     * @param args - Arguments for requesting the discovery based on the identity key.
     * @param overrideWithContacts - Whether to consult personal contacts. Default true. When `false`,
     *   contacts are skipped entirely and the overlay is queried directly. Boolean kept for legacy
     *   call sites; new code should prefer the options-object form.
     * @returns The promise resolves to displayable identities.
     */
    async resolveByIdentityKey(args, overrideWithContacts = true) {
        const opts = typeof overrideWithContacts === 'boolean'
            ? { overrideWithContacts }
            : { overrideWithContacts: true, ...overrideWithContacts };
        const useContacts = opts.overrideWithContacts !== false;
        const parallel = opts.parallel === true;
        if (useContacts && !parallel) {
            const contacts = await this.contactsManager.getContacts(args.identityKey);
            if (contacts.length > 0)
                return contacts;
            const certificatesResult = await this.wallet.discoverByIdentityKey(args, this.originator);
            const certs = certificatesResult?.certificates ?? [];
            return certs.map((cert) => IdentityClient.parseIdentity(cert));
        }
        const [contacts, certificatesResult] = await Promise.all([
            useContacts
                ? this.contactsManager.getContacts(args.identityKey)
                : Promise.resolve([]),
            this.wallet.discoverByIdentityKey(args, this.originator)
        ]);
        if (contacts.length > 0)
            return contacts;
        const certs = certificatesResult?.certificates ?? [];
        return certs.map((cert) => IdentityClient.parseIdentity(cert));
    }
    /**
     * Resolves displayable identity certificates by specific identity attributes, issued by a trusted entity.
     *
     * By default, contacts are consulted first: if any contact matches, the overlay call is skipped
     * entirely. Set `parallel: true` to keep the legacy parallel behavior.
     *
     * @param args - Attributes and optional parameters used to discover certificates.
     * @param overrideWithContacts - Whether to consult personal contacts. Default true. Boolean kept
     *   for legacy call sites; new code should prefer the options-object form.
     * @returns The promise resolves to displayable identities.
     */
    async resolveByAttributes(args, overrideWithContacts = true) {
        const opts = typeof overrideWithContacts === 'boolean'
            ? { overrideWithContacts }
            : { overrideWithContacts: true, ...overrideWithContacts };
        const useContacts = opts.overrideWithContacts !== false;
        const parallel = opts.parallel === true;
        if (useContacts && !parallel) {
            const contacts = await this.contactsManager.getContacts();
            const matches = this.matchContactsByAttributes(contacts, args);
            if (matches.length > 0)
                return matches;
            const certificatesResult = await this.wallet.discoverByAttributes(args, this.originator);
            const certs = certificatesResult?.certificates ?? [];
            const contactByKey = new Map(contacts.map((contact) => [contact.identityKey, contact]));
            return certs.map((cert) => contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert));
        }
        const [contacts, certificatesResult] = await Promise.all([
            useContacts ? this.contactsManager.getContacts() : Promise.resolve([]),
            this.wallet.discoverByAttributes(args, this.originator)
        ]);
        const contactByKey = new Map(contacts.map((contact) => [contact.identityKey, contact]));
        const certs = certificatesResult?.certificates ?? [];
        return certs.map((cert) => contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert));
    }
    /**
     * Best-effort match of contacts against a `DiscoverByAttributesArgs.attributes` shape.
     * Used by the contacts-first path of {@link resolveByAttributes} to decide whether the overlay
     * can be skipped. Compares string-valued attributes against same-named fields on the contact's
     * decrypted record. Returns the subset of contacts that match every supplied attribute.
     */
    matchContactsByAttributes(contacts, args) {
        const attrs = (args).attributes;
        if (attrs == null || typeof attrs !== 'object' || Array.isArray(attrs))
            return [];
        const entries = Object.entries(attrs).filter(([, v]) => typeof v === 'string' && v.length > 0);
        if (entries.length === 0)
            return [];
        return contacts.filter((contact) => {
            const bag = {
                name: contact.name,
                identityKey: contact.identityKey
            };
            return entries.every(([k, v]) => typeof bag[k] === 'string' && bag[k].toLowerCase() === v.toLowerCase());
        });
    }
    /**
     * Remove public certificate revelation from overlay services by spending the identity token
     * @param serialNumber - Unique serial number of the certificate to revoke revelation
     */
    async revokeCertificateRevelation(serialNumber) {
        // 1. Find existing UTXO
        const lookupResolver = new index_js_6.LookupResolver({
            networkPreset: (await this.wallet.getNetwork({})).network
        });
        const result = await lookupResolver.query({
            service: 'ls_identity',
            query: {
                serialNumber
            }
        });
        if (result.type !== 'output-list') {
            throw new Error('Failed to get lookup result');
        }
        const topicBroadcaster = new index_js_6.SHIPBroadcaster(['tm_identity'], {
            networkPreset: (await this.wallet.getNetwork({})).network,
            requireAcknowledgmentFromAllHostsForTopics: [],
            requireAcknowledgmentFromAnyHostForTopics: [],
            requireAcknowledgmentFromSpecificHostsForTopics: { tm_identity: [] }
        });
        await (0, index_js_6.withDoubleSpendRetry)(async () => {
            const tx = index_js_3.Transaction.fromBEEF(result.outputs[0].beef);
            const outpoint = `${tx.id('hex')}.${this.options.outputIndex}`;
            const lockingScript = tx.outputs[this.options.outputIndex].lockingScript;
            if (lockingScript === undefined || outpoint === undefined) {
                throw new Error('Failed to get locking script for revelation output!');
            }
            // 2. Parse results
            const { signableTransaction } = await this.wallet.createAction({
                description: 'Spend certificate revelation token',
                inputBEEF: result.outputs[0].beef,
                inputs: [
                    {
                        inputDescription: 'Revelation token',
                        outpoint,
                        unlockingScriptLength: 74
                    }
                ],
                options: {
                    randomizeOutputs: false,
                    acceptDelayedBroadcast: false,
                    noSend: true
                }
            }, this.originator);
            if (signableTransaction === undefined) {
                throw new Error('Failed to create signable transaction');
            }
            const partialTx = index_js_3.Transaction.fromBEEF(signableTransaction.tx);
            const unlocker = new index_js_4.PushDrop(this.wallet, this.originator).unlock(this.options.protocolID, this.options.keyID, 'anyone');
            const unlockingScript = await unlocker.sign(partialTx, this.options.outputIndex);
            const { tx: signedTx } = await this.wallet.signAction({
                reference: signableTransaction.reference,
                spends: {
                    [this.options.outputIndex]: {
                        unlockingScript: unlockingScript.toHex()
                    }
                },
                options: {
                    acceptDelayedBroadcast: false,
                    noSend: true
                }
            }, this.originator);
            if (signedTx === undefined) {
                throw new Error('Failed to sign transaction');
            }
            await topicBroadcaster.broadcast(index_js_3.Transaction.fromAtomicBEEF(signedTx));
        }, topicBroadcaster);
    }
    /**
     * Load all records from the contacts basket
     * @param identityKey Optional specific identity key to fetch
     * @param forceRefresh Whether to force a check for new contact data
     * @param limit Optional limit on number of contacts to fetch
     * @returns A promise that resolves with an array of contacts
     */
    async getContacts(identityKey, forceRefresh = false, limit = 1000) {
        return await this.contactsManager.getContacts(identityKey, forceRefresh, limit);
    }
    /**
     * Save or update a Metanet contact
     * @param contact The displayable identity information for the contact
     * @param metadata Optional metadata to store with the contact (ex. notes, aliases, etc)
     */
    async saveContact(contact, metadata) {
        return await this.contactsManager.saveContact(contact, metadata);
    }
    /**
     * Remove a contact from the contacts basket
     * @param identityKey The identity key of the contact to remove
     */
    async removeContact(identityKey) {
        return await this.contactsManager.removeContact(identityKey);
    }
    /**
     * Parse out identity and certifier attributes to display from an IdentityCertificate
     * @param identityToParse - The Identity Certificate to parse
     * @returns - IdentityToDisplay
     */
    static parseIdentity(identityToParse) {
        const { type, decryptedFields, certifierInfo } = identityToParse;
        let name, avatarURL, badgeLabel, badgeIconURL, badgeClickURL;
        // Parse out the name to display based on the specific certificate type which has clearly defined fields.
        switch (type) {
            case index_js_1.KNOWN_IDENTITY_TYPES.xCert:
                name = decryptedFields.userName;
                avatarURL = decryptedFields.profilePhoto;
                badgeLabel = `X account certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://socialcert.net'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.discordCert:
                name = decryptedFields.userName;
                avatarURL = decryptedFields.profilePhoto;
                badgeLabel = `Discord account certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://socialcert.net'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.emailCert:
                name = decryptedFields.email;
                avatarURL = 'XUTZxep7BBghAJbSBwTjNfmcsDdRFs5EaGEgkESGSgjJVYgMEizu';
                badgeLabel = `Email certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://socialcert.net'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.phoneCert:
                name = decryptedFields.phoneNumber;
                avatarURL = 'XUTLxtX3ELNUwRhLwL7kWNGbdnFM8WG2eSLv84J7654oH8HaJWrU';
                badgeLabel = `Phone certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://socialcert.net'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.identiCert:
                name = `${decryptedFields.firstName} ${decryptedFields.lastName}`;
                avatarURL = decryptedFields.profilePhoto;
                badgeLabel = `Government ID certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://identicert.me'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.registrant:
                name = decryptedFields.name;
                avatarURL = decryptedFields.icon;
                badgeLabel = `Entity certified by ${certifierInfo.name}`;
                badgeIconURL = certifierInfo.iconUrl;
                badgeClickURL = 'https://bsv-blockchain.github.io/ts-sdk/reference/identity/'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.coolCert:
                name = decryptedFields.cool === 'true' ? 'Cool Person!' : 'Not cool!';
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.anyone:
                name = 'Anyone';
                avatarURL = 'XUT4bpQ6cpBaXi1oMzZsXfpkWGbtp2JTUYAoN7PzhStFJ6wLfoeR';
                badgeLabel =
                    'Represents the ability for anyone to access this information.';
                badgeIconURL = 'XUUV39HVPkpmMzYNTx7rpKzJvXfeiVyQWg2vfSpjBAuhunTCA9uG';
                badgeClickURL = 'https://bsv-blockchain.github.io/ts-sdk/reference/identity/'; // (no dedicated page yet)
                break;
            case index_js_1.KNOWN_IDENTITY_TYPES.self:
                name = 'You';
                avatarURL = 'XUT9jHGk2qace148jeCX5rDsMftkSGYKmigLwU2PLLBc7Hm63VYR';
                badgeLabel = 'Represents your ability to access this information.';
                badgeIconURL = 'XUUV39HVPkpmMzYNTx7rpKzJvXfeiVyQWg2vfSpjBAuhunTCA9uG';
                badgeClickURL = 'https://bsv-blockchain.github.io/ts-sdk/reference/identity/'; // (no dedicated page yet)
                break;
            default: {
                const parsed = IdentityClient.tryToParseGenericIdentity(type, decryptedFields, certifierInfo);
                name = parsed.name;
                avatarURL = parsed.avatarURL;
                badgeLabel = parsed.badgeLabel;
                badgeIconURL = parsed.badgeIconURL;
                badgeClickURL = parsed.badgeClickURL;
                break;
            }
        }
        return {
            name,
            avatarURL,
            abbreviatedKey: identityToParse.subject.length > 0
                ? `${identityToParse.subject.substring(0, 10)}...`
                : '',
            identityKey: identityToParse.subject,
            badgeIconURL,
            badgeLabel,
            badgeClickURL
        };
    }
    /**
     * Helper to check if a value is a non-empty string
     */
    static hasValue(value) {
        return value !== undefined && value !== null && value !== '';
    }
    /**
     * Try to parse identity information from unknown certificate types
     * by checking common field names
     */
    static tryToParseGenericIdentity(type, decryptedFields, certifierInfo) {
        // Try to construct a name from common field patterns
        const firstName = decryptedFields.firstName;
        const lastName = decryptedFields.lastName;
        let fullName;
        if (IdentityClient.hasValue(firstName) && IdentityClient.hasValue(lastName)) {
            fullName = `${firstName} ${lastName}`;
        }
        else if (IdentityClient.hasValue(firstName)) {
            fullName = firstName;
        }
        else if (IdentityClient.hasValue(lastName)) {
            fullName = lastName;
        }
        let name;
        if (IdentityClient.hasValue(decryptedFields.name)) {
            name = decryptedFields.name;
        }
        else if (IdentityClient.hasValue(decryptedFields.userName)) {
            name = decryptedFields.userName;
        }
        else if (fullName !== undefined) {
            name = fullName;
        }
        else if (IdentityClient.hasValue(decryptedFields.email)) {
            name = decryptedFields.email;
        }
        else {
            name = index_js_1.defaultIdentity.name;
        }
        // Try to find an avatar/photo from common field names
        let avatarURL;
        if (IdentityClient.hasValue(decryptedFields.profilePhoto)) {
            avatarURL = decryptedFields.profilePhoto;
        }
        else if (IdentityClient.hasValue(decryptedFields.avatar)) {
            avatarURL = decryptedFields.avatar;
        }
        else if (IdentityClient.hasValue(decryptedFields.icon)) {
            avatarURL = decryptedFields.icon;
        }
        else if (IdentityClient.hasValue(decryptedFields.photo)) {
            avatarURL = decryptedFields.photo;
        }
        else {
            avatarURL = index_js_1.defaultIdentity.avatarURL;
        }
        // Generate badge information
        const badgeLabel = IdentityClient.hasValue(certifierInfo?.name)
            ? `${type} certified by ${String(certifierInfo.name)}`
            : index_js_1.defaultIdentity.badgeLabel;
        const badgeIconURL = IdentityClient.hasValue(certifierInfo?.iconUrl)
            ? certifierInfo.iconUrl
            : index_js_1.defaultIdentity.badgeIconURL;
        const badgeClickURL = index_js_1.defaultIdentity.badgeClickURL;
        return { name, avatarURL, badgeLabel, badgeIconURL, badgeClickURL };
    }
}
exports.IdentityClient = IdentityClient;
//# sourceMappingURL=IdentityClient.js.map