import { KeetaNet } from '../../client/index.js';
import { getUsernameClaimSignable, validateUsernameDefault, Errors, isKeetaNetPublicKeyString, assertKeetaUsernameAnchorClaimRequestJSON, getUsernameTransferSignable, assertKeetaUsernameAnchorReleaseRequestJSON, getUsernameReleaseSignable } from './common.js';
import { KeetaAnchorMetadataServer } from '../../lib/anchor-metadata-server.js';
import { KeetaAnchorUserError, KeetaAnchorUserValidationError } from '../../lib/error.js';
import { verifyBodyAuth } from '../../lib/http-server/common.js';
import * as Signing from '../../lib/utils/signing.js';
function normalizeUsernamePattern(pattern) {
    if (typeof pattern === 'string') {
        try {
            return (new RegExp(pattern));
        }
        catch (error) {
            throw (new Error(`Invalid usernamePattern regex: ${error instanceof Error ? error.message : String(error)}`));
        }
    }
    if (pattern.flags !== '') {
        throw (new Error('usernamePattern RegExp must not specify flags')); // Flags cannot be reliably shared via metadata string
    }
    return (new RegExp(pattern.source));
}
export class KeetaNetUsernameAnchorHTTPServer extends KeetaAnchorMetadataServer {
    homepage;
    usernames;
    routes;
    #usernamePattern;
    constructor(config) {
        super(config);
        this.homepage = config.homepage ?? '';
        this.usernames = config.usernames;
        this.routes = config.routes ?? {};
        if (config.usernamePattern !== undefined) {
            this.#usernamePattern = normalizeUsernamePattern(config.usernamePattern);
        }
    }
    #assertProviderIssuedNameValid(username, fieldPath = 'username') {
        validateUsernameDefault(username, {
            pattern: this.#usernamePattern,
            fieldPath: fieldPath
        });
    }
    async initRoutes(config) {
        const routes = { ...this.routes };
        if ('homepage' in config) {
            routes['GET /'] = async function () {
                let homepageData;
                if (typeof config.homepage === 'string') {
                    homepageData = config.homepage;
                }
                else {
                    if (!config.homepage) {
                        throw (new Error('internal error: No homepage function provided'));
                    }
                    homepageData = await config.homepage();
                }
                return ({
                    output: homepageData,
                    contentType: 'text/html; charset=utf-8'
                });
            };
        }
        routes['GET /api/resolve/:toResolve'] = async (params) => {
            let toResolve = params.get('toResolve');
            if (!toResolve) {
                throw (new KeetaAnchorUserError('Missing toResolve parameter'));
            }
            let resolution;
            let account = null;
            let username = null;
            if (isKeetaNetPublicKeyString(toResolve)) {
                account = KeetaNet.lib.Account.fromPublicKeyString(toResolve);
                resolution = await this.usernames.resolveAccount({ account });
            }
            else {
                try {
                    toResolve = decodeURIComponent(toResolve);
                }
                catch {
                    throw (new KeetaAnchorUserValidationError({
                        fields: [
                            {
                                path: 'toResolve',
                                message: 'toResolve parameter is not a valid URI component',
                                receivedValue: toResolve
                            }
                        ]
                    }));
                }
                this.#assertProviderIssuedNameValid(toResolve);
                username = toResolve;
                resolution = await this.usernames.resolveUsername({ username });
            }
            if (!resolution) {
                throw (new Errors.UserNotFound({ username: username ?? undefined, account: account ?? undefined }));
            }
            if ('account' in resolution) {
                if (account !== null && !(account.comparePublicKey(resolution.account))) {
                    throw (new Error('internal: Resolved account does not match requested account'));
                }
                account = resolution.account;
            }
            if ('username' in resolution) {
                if (username !== null && username !== resolution.username) {
                    throw (new Error('internal: Resolved username does not match requested username'));
                }
                username = resolution.username;
            }
            if (!account || !username) {
                throw (new Error('internal: We should know both the account and username here'));
            }
            return ({
                output: JSON.stringify({
                    ok: true,
                    account: KeetaNet.lib.Account.toPublicKeyString(account),
                    username: username
                }),
                contentType: 'application/json'
            });
        };
        const claimHandler = this.usernames.claim;
        if (claimHandler) {
            routes['POST /api/claim'] = async (_params, body) => {
                const raw = assertKeetaUsernameAnchorClaimRequestJSON(body);
                this.#assertProviderIssuedNameValid(raw.username);
                let transfer;
                if (raw.transfer) {
                    const fromAccount = KeetaNet.lib.Account.toAccount(raw.transfer.from);
                    const toAccount = KeetaNet.lib.Account.toAccount(raw.account);
                    const transferSignable = getUsernameTransferSignable({
                        username: raw.username,
                        from: fromAccount,
                        to: toAccount
                    });
                    const verifiedTransfer = await Signing.VerifySignedData(fromAccount, transferSignable, raw.transfer.signed);
                    if (!verifiedTransfer) {
                        throw (new KeetaAnchorUserError('Invalid username claim transfer signature'));
                    }
                    transfer = { from: fromAccount, signed: raw.transfer.signed };
                }
                const account = await verifyBodyAuth(raw, function (req) {
                    const claimAccount = KeetaNet.lib.Account.toAccount(req.account);
                    const claimPayload = {
                        username: req.username,
                        account: claimAccount
                    };
                    if (transfer) {
                        claimPayload.transfer = transfer;
                    }
                    return (getUsernameClaimSignable(claimPayload));
                }, this.resolvedCertificateChainRequirement);
                const claimResponse = await claimHandler({
                    username: raw.username,
                    account,
                    fromUser: transfer?.from ?? null
                });
                if (!claimResponse.ok) {
                    if (claimResponse.taken) {
                        throw (new Errors.UsernameAlreadyTaken({ username: raw.username }));
                    }
                    else {
                        throw (new KeetaAnchorUserError('Username claim rejected'));
                    }
                }
                return ({
                    output: JSON.stringify({ ok: true }),
                    contentType: 'application/json'
                });
            };
        }
        const releaseHandler = this.usernames.releaseUsername;
        if (releaseHandler) {
            routes['POST /api/release'] = async (_params, body) => {
                const raw = assertKeetaUsernameAnchorReleaseRequestJSON(body);
                const account = await verifyBodyAuth(raw, function (req) {
                    return (getUsernameReleaseSignable({ account: KeetaNet.lib.Account.toAccount(req.account) }));
                }, this.resolvedCertificateChainRequirement);
                const releaseResponse = await releaseHandler({ account });
                if (!releaseResponse.ok) {
                    throw (new KeetaAnchorUserError('Release claim rejected'));
                }
                return ({
                    output: JSON.stringify({ ok: true }),
                    contentType: 'application/json'
                });
            };
        }
        const searchHandler = this.usernames.search;
        if (searchHandler) {
            routes['GET /api/search'] = async (_ignore_params, _ignore_body, _ignore_headers, url) => {
                const request = (() => {
                    const searchParameter = url.searchParams.get('search');
                    if (!searchParameter) {
                        throw (new KeetaAnchorUserValidationError({
                            fields: [
                                {
                                    path: 'search',
                                    message: 'Missing search parameter',
                                    receivedValue: searchParameter
                                }
                            ]
                        }));
                    }
                    this.#assertProviderIssuedNameValid(searchParameter, 'search');
                    return ({ search: searchParameter });
                })();
                const searchResponse = await searchHandler(request);
                const formatted = {
                    ok: true,
                    results: searchResponse.results.map(function (result) {
                        return ({
                            username: result.username,
                            account: result.account.publicKeyString.get()
                        });
                    })
                };
                return ({
                    output: JSON.stringify(formatted),
                    contentType: 'application/json'
                });
            };
        }
        return (routes);
    }
    async buildServiceMetadata() {
        const acceptedIssuerDNs = this.acceptedIssuerDNs();
        const operations = {
            resolve: (new URL('/api/resolve', this.url)).toString() + '/{toResolve}'
        };
        const authentication = {
            method: 'keeta-account',
            type: 'required'
        };
        if (this.usernames.claim) {
            operations.claim = {
                url: (new URL('/api/claim', this.url)).toString(),
                options: { authentication }
            };
        }
        if (this.usernames.releaseUsername) {
            operations.release = {
                url: (new URL('/api/release', this.url)).toString(),
                options: { authentication }
            };
        }
        if (this.usernames.search) {
            operations.search = { url: (new URL('/api/search', this.url)).toString() };
        }
        return ({
            operations,
            ...(this.#usernamePattern ? { usernamePattern: this.#usernamePattern.source } : {}),
            ...(acceptedIssuerDNs !== undefined && { acceptedIssuerDNs })
        });
    }
}
//# sourceMappingURL=server.js.map