"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AmbiguousError = exports.AppNotFound = exports.NotFound = void 0;
const tslib_1 = require("tslib");
const color_1 = require("@heroku-cli/color");
const command_1 = require("@heroku-cli/command");
const core_1 = require("@oclif/core");
const tsheredoc_1 = tslib_1.__importDefault(require("tsheredoc"));
const api_client_1 = require("@heroku-cli/command/lib/api-client");
const http_call_1 = tslib_1.__importDefault(require("@heroku/http-call"));
class NotFound extends Error {
    constructor(addonIdentifier, appIdentifier) {
        const message = (0, tsheredoc_1.default) `
      We can’t find a model resource called ${color_1.color.yellow(addonIdentifier)}${appIdentifier ? ` on ${color_1.color.app(appIdentifier)}` : ''}.
      Run ${color_1.color.cmd(`heroku ai:models:info --app ${appIdentifier ? appIdentifier : '<value>'}`)} to see a list of model resources.
    `;
        super(message);
    }
    statusCode = 404;
    id = 'not_found';
}
exports.NotFound = NotFound;
class AppNotFound extends Error {
    constructor(appIdentifier) {
        const message = (0, tsheredoc_1.default) `
      We can’t find the ${color_1.color.app(appIdentifier)} app. Check your spelling.
    `;
        super(message);
    }
    statusCode = 404;
    id = 'not_found';
}
exports.AppNotFound = AppNotFound;
class AmbiguousError extends Error {
    matches;
    constructor(matches, addonIdentifier, appIdentifier) {
        const message = (0, tsheredoc_1.default) `
      Multiple model resources match ${color_1.color.yellow(addonIdentifier)}${appIdentifier ? ` on ${color_1.color.app(appIdentifier)}` : ''}: ${matches.map(match => color_1.color.addon(match)).join(', ')}.
      Specify the model resource by its alias instead.
    `;
        super(message);
        this.matches = matches;
    }
    statusCode = 422;
    id = 'multiple_matches';
}
exports.AmbiguousError = AmbiguousError;
// This function only exists because the
// heroku-cli-command legacy heroku client
// uses funky logic to prepend 'https://' to the request url
// See logic here --> https://github.com/heroku/heroku-cli-command/blob/32f1010c7a949948aac2b5ce1526dc65824a616c/src/command.ts#L38
const stripHttps = (url) => {
    return url.startsWith('https://') ? url.slice(8) : url;
};
class default_1 extends command_1.Command {
    _addon;
    _addonAttachment;
    _addonServiceSlug;
    _addonResourceId;
    _apiKey;
    _apiModelId;
    _apiUrl;
    _herokuAI;
    _defaultInferenceHost = process.env.HEROKU_INFERENCE_HOST || 'us.inference.heroku.com';
    async configureHerokuAIClient(addonIdentifier, appIdentifier) {
        const defaultHeaders = {
            ...this.heroku.defaults.headers,
            accept: 'application/json',
            'user-agent': `heroku-cli-plugin-ai/${process.env.npm_package_version} ${this.config.platform}`,
        };
        delete defaultHeaders.authorization;
        const defaults = http_call_1.default.defaults;
        if (addonIdentifier) {
            ({ addon: this._addon, attachment: this._addonAttachment } = await this.resolveAddonAndAttachment(addonIdentifier, appIdentifier));
            const { body: configVars } = await this.heroku.get(`/apps/${this.addonAttachment.app.id}/config-vars`);
            this._apiKey = configVars[this.apiKeyConfigVarName];
            this._apiModelId = configVars[this.apiModelIdConfigVarName] ||
                this.addon.plan.name?.split(':')[1]; // Fallback to plan name (e.g. "heroku-inference:claude-3-haiku" => "claude-3-haiku"
            this._apiUrl = configVars[this.apiUrlConfigVarName];
            this._addonServiceSlug = this.addon.addon_service.name;
            this._addonResourceId = this.addon.id;
            defaults.host = stripHttps(this.apiUrl);
            defaults.headers = {
                ...defaultHeaders,
                authorization: `Bearer ${this.apiKey}`,
            };
        }
        else {
            defaults.host = this.defaultInferenceHost;
            defaults.headers = defaultHeaders;
        }
        this._herokuAI = new command_1.APIClient(this.config);
        this._herokuAI.http.defaults = defaults;
        this._herokuAI.auth = this._apiKey;
    }
    /*
     * Resolution logic:
     * 1. Use the add-on and app identifiers to fetch matching add-ons and attachments from Platform API
     *    resolver endpoints.
     * 2. If we don't get any add-ons or add-on attachments from the resolvers, we throw a NotFound error.
     * 3. Try to resolve the add-on through the add-ons resolver response:
     *    a. If we have no add-ons, we move to the next step.
     *    b. We deduplicate the resolved add-ons based on their add-on ids.
     *    c. If we're left with a single add-on, it will be the selected one.
     *    d. If we still have multiple add-ons, we throw an AmbiguousError.
     * 4. If we didn't resolve for an add-on yet, try to do it through the add-on attachments resolver response:
     *    a. If we have a single attachment, we select it as the resolved attachment and try to resolve the
     *       add-on fetching its info using the app and add-on ids from the attachment.
     *    b. If we have multiple attachments, we deduplicate attachments to the same app and filter
     *       based on add-ons that are accessible to the user through the attached app (we do this
     *       fetching all add-ons using the app and add-on ids from the remaining attachments and keeping
     *       only the successful responses).
     *       - If we're left with no accessible add-ons, we throw a NotFound error.
     *       - If we still have multiple attachments, we throw an AmbiguousError.
     *       - If we get a single accessible add-on, we select it as the resolved add-on and its associated
     *         attachment as the resolved attachment.
     * 5. If we resolved for an add-on, check that it's a Managed Inference add-on or throw a NotFound error.
     * 6. If we resolved for an add-on but not yet for an attachment:
     *    a. If we have a single attachment on the resolver response, we select that one.
     *    b. If not, we try to select the first attachment that matches the resolved add-on by app and add-on id.
     *    c. If no attachment matched, we fetch all add-on attachments for the resolved add-on and try to
     *       select the first one that matches the resolved add-on by app id.
     * 7. If we get to this point without the add-on or the attachment resolved, throw a NotFound error.
     * 8. Return the resolved add-on and attachment.
     */
    // eslint-disable-next-line complexity
    async resolveAddonAndAttachment(addonIdentifier, appIdentifier) {
        let resolvedAddon;
        let resolvedAttachment;
        // 1. Use the add-on and app identifiers to fetch matching add-ons and attachments from Platform API
        //    resolver endpoints.
        const addonResolverRequest = this.heroku.post('/actions/addons/resolve', {
            body: {
                addon: addonIdentifier,
                app: appIdentifier || null,
            },
        });
        const attachmentResolverRequest = this.heroku.post('/actions/addon-attachments/resolve', {
            body: {
                addon_attachment: addonIdentifier,
                app: appIdentifier || null,
            },
        });
        const [settledAddons, settledAttachments] = await Promise.allSettled([addonResolverRequest, attachmentResolverRequest]);
        if (settledAddons.status === 'rejected' && settledAttachments.status === 'rejected') {
            // 2. If we don't get any add-ons or add-on attachments from the resolvers, we throw a NotFound error.
            this.handleErrors(settledAddons, settledAttachments, addonIdentifier, appIdentifier);
        }
        const resolvedAddons = settledAddons.status === 'fulfilled' ? settledAddons.value.body : [];
        const resolvedAttachments = settledAttachments.status === 'fulfilled' ? settledAttachments.value.body : [];
        // 3. If the addon attachment resolved, check for multiple attachments
        if (resolvedAttachments && resolvedAttachments.length > 1) {
            throw new AmbiguousError(resolvedAttachments.map((a) => a.name), addonIdentifier, appIdentifier);
        }
        // 4. Try to resolve the add-on through the add-ons resolver response.
        if (resolvedAddons.length > 0) {
            // The add-on resolver may duplicate add-ons when there's more than one attachment and the user has access to the different
            // apps where it's attached, so we dedup here trying to get a single result.
            const uniqueAddons = resolvedAddons.filter((addon, index, self) => {
                return self.findIndex(a => a.id === addon.id) === index;
            });
            if (uniqueAddons.length === 1)
                resolvedAddon = uniqueAddons[0];
            else
                throw new AmbiguousError(uniqueAddons.map(a => a.name), addonIdentifier, appIdentifier);
        }
        // 5. If we didn't resolve for an add-on yet, try to do it through the add-on attachments resolver response.
        if (!resolvedAddon) {
            if (resolvedAttachments.length === 1) {
                resolvedAttachment = resolvedAttachments[0];
                ({ body: resolvedAddon } = await this.heroku.get(`/apps/${resolvedAttachment.app.id}/addons/${resolvedAttachment.addon.id}`));
            }
            else if (resolvedAttachments.length > 1) {
                const uniqueAppAddons = resolvedAttachments.map(a => {
                    return { attachment: a, appId: a.app.id, addonId: a.addon.id };
                }).filter((appAddon, index, self) => {
                    return self.findIndex(a => a.appId === appAddon.appId && a.addonId === appAddon.addonId) === index;
                });
                const addonRequests = uniqueAppAddons.map(a => this.heroku.get(`/apps/${a.appId}/addons/${a.addonId}`));
                const settledAddons = await Promise.allSettled(addonRequests);
                const accessibleAddons = settledAddons.filter(s => s.status === 'fulfilled').map(s => s.value.body);
                if (accessibleAddons.length === 0)
                    throw new NotFound(addonIdentifier, appIdentifier);
                else if (accessibleAddons.length > 1)
                    throw new AmbiguousError(accessibleAddons.map(a => a.name), addonIdentifier, appIdentifier);
                else {
                    resolvedAddon = accessibleAddons[0];
                    resolvedAttachment = uniqueAppAddons[settledAddons.findIndex(s => s.status === 'fulfilled')].attachment;
                }
            }
        }
        // 6. If we resolved for an add-on, check that it's a Managed Inference add-on or throw a NotFound error.
        if (resolvedAddon && resolvedAddon.addon_service.name !== this.addonServiceSlug) {
            throw new NotFound(addonIdentifier, appIdentifier);
        }
        // 7. If we resolved for an add-on but not for an attachment yet, try to resolve the attachment
        if (resolvedAddon && !resolvedAttachment) {
            resolvedAttachment = resolvedAttachments.length === 1 ?
                resolvedAttachments[0] :
                resolvedAttachments.find(a => a.addon.id === resolvedAddon.id && a.app.id === resolvedAddon.app.id);
            if (!resolvedAttachment) {
                const { body: addonAttachments } = await this.heroku.get(`/addons/${resolvedAddon.id}/addon-attachments`);
                resolvedAttachment = addonAttachments.find(a => a.app.id === resolvedAddon.app.id);
            }
        }
        // 8. If we get to this point without the add-on or the attachment resolved, throw a NotFound error.
        if (!resolvedAddon || !resolvedAttachment)
            throw new NotFound(addonIdentifier, appIdentifier);
        // 9. Return the resolved add-on and attachment.
        return { addon: resolvedAddon, attachment: resolvedAttachment };
    }
    handleErrors(addonRejection, attachmentRejection, addonIdentifier, appIdentifier) {
        const addonResolverError = addonRejection.reason;
        const attachmentResolverError = attachmentRejection.reason;
        const addonNotFound = addonResolverError instanceof api_client_1.HerokuAPIError &&
            addonResolverError.http.statusCode === 404 &&
            addonResolverError.body.resource === 'add_on';
        const attachmentNotFound = attachmentResolverError instanceof api_client_1.HerokuAPIError &&
            attachmentResolverError.http.statusCode === 404 &&
            attachmentResolverError.body.resource === 'add_on attachment';
        const appNotFound = attachmentResolverError instanceof api_client_1.HerokuAPIError &&
            attachmentResolverError.http.statusCode === 404 &&
            attachmentResolverError.body.resource === 'app';
        let error = addonResolverError;
        if (addonNotFound)
            error = attachmentNotFound ? new NotFound(addonIdentifier, appIdentifier) : attachmentResolverError;
        if (appNotFound)
            error = new AppNotFound(appIdentifier);
        throw error;
    }
    get addon() {
        if (this._addon)
            return this._addon;
        core_1.ux.error('Heroku AI API Client not configured.', { exit: 1 });
    }
    get addonAttachment() {
        if (this._addonAttachment)
            return this._addonAttachment;
        core_1.ux.error('Heroku AI API Client not configured.', { exit: 1 });
    }
    get addonServiceSlug() {
        return process.env.HEROKU_INFERENCE_ADDON ||
            this._addonServiceSlug ||
            'heroku-inference';
    }
    get addonResourceId() {
        if (this.addon && this._addonResourceId)
            return this._addonResourceId;
        core_1.ux.error(`Model resource ${color_1.color.addon(this.addon?.name)} isn’t fully provisioned on ${color_1.color.app(this.addon?.app.name)}.`, { exit: 1 });
    }
    get apiKey() {
        if (this.addon && this._apiKey)
            return this._apiKey;
        core_1.ux.error(`Model resource ${color_1.color.addon(this.addon?.name)} isn’t fully provisioned on ${color_1.color.app(this.addon?.app.name)}.`, { exit: 1 });
    }
    get apiKeyConfigVarName() {
        return `${this.addonAttachment.name.toUpperCase()}_KEY`;
    }
    get modelAlias() {
        return this.addonAttachment.name;
    }
    get apiModelId() {
        return this._apiModelId;
    }
    get apiModelIdConfigVarName() {
        return `${this.addonAttachment.name.toUpperCase()}_MODEL_ID`;
    }
    get apiUrl() {
        if (this.addon && this._apiUrl)
            return this._apiUrl;
        core_1.ux.error(`Model resource ${color_1.color.addon(this.addon?.name)} isn’t fully provisioned on ${color_1.color.app(this.addon?.app.name)}.`, { exit: 1 });
    }
    get apiUrlConfigVarName() {
        return `${this.addonAttachment.name.toUpperCase()}_URL`;
    }
    get herokuAI() {
        if (this._herokuAI)
            return this._herokuAI;
        core_1.ux.error('Heroku AI API Client not configured.', { exit: 1 });
    }
    get defaultInferenceHost() {
        return this._defaultInferenceHost;
    }
}
exports.default = default_1;
