import { Resource, ResourceKind } from "../resource.js";
import { Secret } from "../secret.js";
import { logger } from "../util/logger.js";
import { isCloudflareApiError } from "./api-error.js";
import { extractCloudflareResult, } from "./api-response.js";
import { createCloudflareApi, } from "./api.js";
/**
 * Type guard for {@link AccessIdentityProvider}.
 */
export function isAccessIdentityProvider(resource) {
    return resource?.[ResourceKind] === "cloudflare::AccessIdentityProvider";
}
/**
 * Creates a Cloudflare Zero Trust [Access identity provider](https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/)
 * which lets users sign in to Access-protected applications.
 *
 * @example
 * // Built-in One-Time PIN (no IdP setup required).
 * const otp = await AccessIdentityProvider("otp", {
 *   type: "onetimepin",
 *   name: "Email OTP",
 * });
 *
 * @example
 * // Google OAuth. clientId is a public OAuth identifier (not a secret).
 * const google = await AccessIdentityProvider("google", {
 *   type: "google",
 *   name: "Google",
 *   clientId: process.env.GOOGLE_CLIENT_ID!,
 *   clientSecret: alchemy.secret.env.GOOGLE_CLIENT_SECRET,
 * });
 *
 * @example
 * // Generic OIDC provider.
 * const oidc = await AccessIdentityProvider("idp", {
 *   type: "oidc",
 *   name: "Corporate IdP",
 *   authUrl: "https://idp.example.com/oauth2/authorize",
 *   tokenUrl: "https://idp.example.com/oauth2/token",
 *   certsUrl: "https://idp.example.com/oauth2/certs",
 *   clientId: "my-app",
 *   clientSecret: alchemy.secret.env.IDP_CLIENT_SECRET,
 *   scopes: ["openid", "email", "profile"],
 *   pkceEnabled: true,
 * });
 */
export const AccessIdentityProvider = Resource("cloudflare::AccessIdentityProvider", async function (id, props) {
    const api = await createCloudflareApi(props);
    const name = props.name ?? this.scope.createPhysicalName(id);
    const basePath = `/accounts/${api.accountId}/access/identity_providers`;
    if (this.phase === "delete") {
        if (this.output?.id && props.delete !== false) {
            await deleteAccessIdentityProvider(api, this.output.id);
        }
        return this.destroy();
    }
    // type is immutable — recreate if it changed.
    if (this.phase === "update" &&
        this.output &&
        this.output.type !== props.type) {
        this.replace(true);
    }
    // Cloudflare requires `config` to always be present, even for variants
    // that don't take any (e.g. `onetimepin`). Omitting it returns
    // [12130] "unexpected end of JSON input".
    const body = {
        name,
        type: props.type,
        config: extractIdpConfig(props),
    };
    let result;
    if (this.phase === "update" && this.output?.id) {
        result = await extractCloudflareResult(`update access identity provider "${name}"`, api.put(`${basePath}/${this.output.id}`, body));
    }
    else {
        const adopt = props.adopt ?? this.scope.adopt;
        try {
            result =
                await extractCloudflareResult(`create access identity provider "${name}"`, api.post(basePath, body));
        }
        catch (err) {
            if (adopt && isAccessDuplicateNameError(err)) {
                const existing = await findAccessIdentityProviderByName(api, name);
                if (!existing) {
                    throw new Error(`Identity provider "${name}" already exists but could not be found for adoption.`, { cause: err });
                }
                logger.log(`Adopting existing access identity provider "${name}" (${existing.id})`);
                result =
                    await extractCloudflareResult(`adopt access identity provider "${name}"`, api.put(`${basePath}/${existing.id}`, body));
            }
            else {
                throw err;
            }
        }
    }
    const rest = { ...props };
    delete rest.adopt;
    delete rest.delete;
    // Output convention (CLAUDE.md): secrets are always wrapped. Strict
    // variants carry `clientSecret` at the top level; the `Other` variant
    // tucks it inside `config`.
    if (typeof rest.clientSecret === "string") {
        rest.clientSecret = Secret.wrap(rest.clientSecret);
    }
    const config = rest.config;
    if (config && typeof config.clientSecret === "string") {
        rest.config = {
            ...config,
            clientSecret: Secret.wrap(config.clientSecret),
        };
    }
    return {
        ...rest,
        id: result.id,
        name: result.name,
    };
});
/**
 * Top-level prop keys that are *not* part of the IdP-specific configuration
 * (the wire `config` blob) — Alchemy/Cloudflare metadata, the type
 * discriminator, and the explicit `config` escape hatch on the `Other`
 * variant.
 */
const IDP_METADATA_KEYS = new Set([
    "name",
    "type",
    "adopt",
    "delete",
    "baseUrl",
    "profile",
    "apiKey",
    "apiToken",
    "accountId",
    "email",
    "config",
]);
/**
 * Build the wire-format `config` blob from props. Strict variants store
 * config fields flat at the top level; the `Other` variant uses an explicit
 * nested `config` object as an escape hatch.
 */
function extractIdpConfig(props) {
    if ("config" in props && props.config) {
        return camelToSnakeWithSecrets(props.config);
    }
    const flat = {};
    for (const [key, value] of Object.entries(props)) {
        if (!IDP_METADATA_KEYS.has(key) && value !== undefined) {
            flat[key] = value;
        }
    }
    return camelToSnakeWithSecrets(flat);
}
/**
 * Convert a camelCase config object to snake_case for the wire, unwrapping
 * any {@link Secret} values along the way. Recurses into arrays of objects
 * (e.g. SAML `headerAttributes`).
 */
function camelToSnakeWithSecrets(input) {
    const out = {};
    for (const [key, value] of Object.entries(input)) {
        if (value === undefined)
            continue;
        const snakeKey = key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
        out[snakeKey] = transformValue(value);
    }
    return out;
}
function transformValue(value) {
    if (value instanceof Secret)
        return value.unencrypted;
    if (Array.isArray(value)) {
        return value.map((item) => item && typeof item === "object" && !Array.isArray(item)
            ? camelToSnakeWithSecrets(item)
            : transformValue(item));
    }
    return value;
}
/**
 * Cloudflare returns 409/400 with an "already exists" message for duplicate
 * IdP names.
 */
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;
}
/**
 * Look up an existing IdP by name across paginated results.
 */
async function findAccessIdentityProviderByName(api, name) {
    let page = 1;
    const perPage = 50;
    while (true) {
        const response = await api.get(`/accounts/${api.accountId}/access/identity_providers?page=${page}&per_page=${perPage}`);
        if (!response.ok)
            return null;
        const data = (await response.json());
        const match = data.result.find((p) => p.name === name);
        if (match)
            return match;
        const info = data.result_info;
        if (!info || info.page * info.per_page >= info.total_count)
            return null;
        page++;
    }
}
/**
 * Delete an IdP. Cloudflare returns 400 if any Application references it.
 */
async function deleteAccessIdentityProvider(api, idpId) {
    const response = await api.delete(`/accounts/${api.accountId}/access/identity_providers/${idpId}`);
    if (!response.ok && response.status !== 404) {
        let body = "";
        try {
            body = await response.text();
        }
        catch { }
        if (/in use|reference|associated/i.test(body)) {
            throw new Error(`Cannot delete identity provider ${idpId}: it is referenced by one or more Access applications. Remove those references first.\n${body}`);
        }
        logger.error(`Error deleting access identity provider ${idpId}: ${response.status} ${response.statusText}\n${body}`);
    }
}
//# sourceMappingURL=access-identity-provider.js.map