import { alchemy } from "../alchemy.js";
import { Resource, ResourceKind } from "../resource.js";
import { logger } from "../util/logger.js";
import { isCloudflareApiError } from "./api-error.js";
import { extractCloudflareResult, } from "./api-response.js";
import { createCloudflareApi, } from "./api.js";
import { findZoneForHostname } from "./zone.js";
/**
 * Type guard for {@link AccessApplication}.
 */
export function isAccessApplication(resource) {
    return resource?.[ResourceKind] === "cloudflare::AccessApplication";
}
/**
 * Creates a Cloudflare Zero Trust [Access application](https://developers.cloudflare.com/cloudflare-one/applications/)
 * — a protected resource users authenticate to via Access.
 *
 * @example
 * // Self-hosted application protected by an Access policy.
 * const app = await AccessApplication("admin", {
 *   type: "self_hosted",
 *   name: "Internal Admin",
 *   domain: "admin.acme.com",
 *   policies: [employeesPolicy],
 *   sessionDuration: "8h",
 * });
 *
 * @example
 * // Bookmark in the Access launcher.
 * const wiki = await AccessApplication("wiki", {
 *   type: "bookmark",
 *   name: "Internal Wiki",
 *   domain: "https://wiki.acme.com",
 *   appLauncherVisible: true,
 * });
 *
 * @example
 * // SaaS OIDC integration.
 * const slack = await AccessApplication("slack-saas", {
 *   type: "saas",
 *   name: "Slack",
 *   saas: {
 *     authType: "oidc",
 *     redirectUris: ["https://acme.slack.com/oidc/callback"],
 *     scopes: ["openid", "email", "profile"],
 *   },
 *   policies: [employeesPolicy],
 * });
 */
export const AccessApplication = Resource("cloudflare::AccessApplication", async function (id, props) {
    const api = await createCloudflareApi(props);
    const name = props.name ?? this.scope.createPhysicalName(id);
    const zoneId = "zone" in props && props.zone
        ? typeof props.zone === "string"
            ? (await findZoneForHostname(api, props.zone)).zoneId
            : props.zone.id
        : undefined;
    const basePath = zoneId
        ? `/zones/${zoneId}/access/apps`
        : `/accounts/${api.accountId}/access/apps`;
    if (this.phase === "delete") {
        if (this.output?.id && props.delete !== false) {
            const deletePath = this.output.zoneId
                ? `/zones/${this.output.zoneId}/access/apps/${this.output.id}`
                : `/accounts/${api.accountId}/access/apps/${this.output.id}`;
            await deleteAccessApplication(api, deletePath);
        }
        return this.destroy();
    }
    // type and zone are immutable — recreate if either changed.
    if (this.phase === "update" && this.output) {
        if (this.output.type !== props.type ||
            (this.output.zoneId ?? undefined) !== (zoneId ?? undefined)) {
            this.replace(true);
        }
    }
    if (props.type === "self_hosted" &&
        !props.domain) {
        throw new Error(`AccessApplication "${name}": 'domain' is required for self_hosted applications.`);
    }
    const body = {
        name,
        type: props.type,
    };
    if ("domain" in props && props.domain)
        body.domain = props.domain;
    if (props.allowedIdps)
        body.allowed_idps = props.allowedIdps.map((idp) => typeof idp === "string" ? idp : idp.id);
    if (props.policies)
        body.policies = props.policies.map((p) => typeof p === "string" ? p : p.id);
    if (props.sessionDuration !== undefined)
        body.session_duration = props.sessionDuration;
    if (props.appLauncherVisible !== undefined)
        body.app_launcher_visible = props.appLauncherVisible;
    if (props.autoRedirectToIdentity !== undefined)
        body.auto_redirect_to_identity = props.autoRedirectToIdentity;
    if (props.customDenyMessage !== undefined)
        body.custom_deny_message = props.customDenyMessage;
    if (props.customDenyUrl !== undefined)
        body.custom_deny_url = props.customDenyUrl;
    if (props.customNonIdentityDenyUrl !== undefined)
        body.custom_non_identity_deny_url = props.customNonIdentityDenyUrl;
    if (props.tags)
        body.tags = props.tags;
    if (props.skipInterstitial !== undefined)
        body.skip_interstitial = props.skipInterstitial;
    if (props.serviceAuth401Redirect !== undefined)
        body.service_auth_401_redirect = props.serviceAuth401Redirect;
    if (props.type === "saas" && props.saas) {
        const saas = props.saas;
        body.saas =
            saas.authType === "oidc"
                ? {
                    auth_type: "oidc",
                    redirect_uris: saas.redirectUris,
                    scopes: saas.scopes,
                    group_filter_regex: saas.groupFilterRegex,
                }
                : {
                    auth_type: "saml",
                    sp_entity_id: saas.spEntityId,
                    consumer_service_url: saas.consumerServiceUrl,
                    name_id_format: saas.nameIdFormat,
                    default_relay_state: saas.defaultRelayState,
                };
    }
    let app;
    if (this.phase === "update" && this.output?.id) {
        const updatePath = this.output.zoneId
            ? `/zones/${this.output.zoneId}/access/apps/${this.output.id}`
            : `/accounts/${api.accountId}/access/apps/${this.output.id}`;
        app = await extractCloudflareResult(`update access application "${name}"`, api.put(updatePath, body));
    }
    else {
        const adopt = props.adopt ?? this.scope.adopt;
        try {
            app = await extractCloudflareResult(`create access application "${name}"`, api.post(basePath, body));
        }
        catch (err) {
            if (adopt && isAccessDuplicateNameError(err)) {
                const existing = await findAccessApplicationByName(api, basePath, name);
                if (!existing) {
                    throw new Error(`Access application "${name}" already exists but could not be found for adoption.`, { cause: err });
                }
                logger.log(`Adopting existing access application "${name}" (${existing.id})`);
                app = await extractCloudflareResult(`adopt access application "${name}"`, api.put(`${basePath}/${existing.id}`, body));
            }
            else {
                throw err;
            }
        }
    }
    // SaaS OIDC client_secret is only returned on POST — retain on update.
    const clientSecret = app.client_secret
        ? alchemy.secret(app.client_secret)
        : this.output?.clientSecret;
    const rest = { ...props };
    delete rest.adopt;
    delete rest.delete;
    delete rest.zone;
    return {
        ...rest,
        id: app.id,
        name: app.name,
        aud: app.aud,
        zoneId,
        clientId: app.client_id,
        clientSecret,
        createdAt: app.created_at,
        updatedAt: app.updated_at,
    };
});
function isAccessDuplicateNameError(err) {
    if (isCloudflareApiError(err, { status: 409 }) ||
        isCloudflareApiError(err, { status: 400 })) {
        const data = err.errorData;
        return (Array.isArray(data) &&
            data.some((e) => "message" in e && /already exists/i.test(String(e.message))));
    }
    return false;
}
async function findAccessApplicationByName(api, basePath, name) {
    let page = 1;
    const perPage = 50;
    while (true) {
        const response = await api.get(`${basePath}?page=${page}&per_page=${perPage}`);
        if (!response.ok)
            return null;
        const data = (await response.json());
        const match = data.result.find((a) => a.name === name);
        if (match)
            return match;
        const info = data.result_info;
        if (!info || info.page * info.per_page >= info.total_count)
            return null;
        page++;
    }
}
async function deleteAccessApplication(api, path) {
    const response = await api.delete(path);
    if (!response.ok && response.status !== 404) {
        logger.error(`Error deleting access application at ${path}: ${response.status} ${response.statusText}`);
    }
}
//# sourceMappingURL=access-application.js.map