import { Resource, ResourceKind } from "../resource.js";
import { withExponentialBackoff } from "../util/retry.js";
import { CloudflareApiError, handleApiError } from "./api-error.js";
import { createCloudflareApi, } from "./api.js";
import { withJurisdiction } from "./bucket.js";
import { findQueueByName } from "./queue.js";
/**
 * Type guard to check if a resource is an R2BucketNotification
 */
export function isR2BucketNotification(resource) {
    return resource?.[ResourceKind] === "cloudflare::R2BucketNotification";
}
async function resolveQueueId(api, queue) {
    if (typeof queue !== "string" && queue.id) {
        return queue.id;
    }
    const queueName = typeof queue === "string" ? queue : queue.name;
    return await withExponentialBackoff(async () => {
        const queueId = (await findQueueByName(api, queueName))?.result?.queue_id;
        if (!queueId) {
            throw new Error(`Queue "${queueName}" not found`);
        }
        return queueId;
    }, (error) => error instanceof Error && error.message.includes("not found"), 5, 1000);
}
/**
 * Creates an event notification rule for an R2 bucket that sends messages to a Queue.
 *
 * Event notifications allow you to trigger automated workflows when objects are created,
 * modified, or deleted in your R2 bucket. Messages are delivered to a Cloudflare Queue
 * where they can be processed by a consumer Worker.
 *
 * @example
 * ## Basic object-create notification
 *
 * Send notifications to a queue when objects are created in a bucket:
 *
 * ```ts
 * import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
 *
 * const bucket = await R2Bucket("uploads");
 * const queue = await Queue("upload-events");
 *
 * await R2BucketNotification("upload-notifications", {
 *   bucket,
 *   queue,
 *   eventTypes: ["object-create"],
 * });
 * ```
 *
 * @example
 * ## Filtered notifications with prefix and suffix
 *
 * Only trigger notifications for specific object patterns:
 *
 * ```ts
 * import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
 *
 * const bucket = await R2Bucket("documents");
 * const queue = await Queue("pdf-processing");
 *
 * await R2BucketNotification("pdf-uploads", {
 *   bucket,
 *   queue,
 *   eventTypes: ["object-create"],
 *   prefix: "incoming/",
 *   suffix: ".pdf",
 *   description: "Process newly uploaded PDF files",
 * });
 * ```
 *
 * @example
 * ## Multiple suffixes for different file types
 *
 * Create rules for multiple file extensions at once:
 *
 * ```ts
 * import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
 *
 * const bucket = await R2Bucket("media");
 * const queue = await Queue("audio-processing");
 *
 * await R2BucketNotification("audio-uploads", {
 *   bucket,
 *   queue,
 *   eventTypes: ["object-create"],
 *   prefix: "audio/",
 *   suffix: [".mp3", ".wav", ".flac"],
 * });
 * ```
 *
 * @example
 * ## Multiple event types with typed queue
 *
 * Listen for both create and delete events with type-safe message handling:
 *
 * ```ts
 * import { R2Bucket, Queue, R2BucketNotification, R2BucketNotificationMessage } from "alchemy/cloudflare";
 *
 * const bucket = await R2Bucket("assets");
 * const queue = await Queue<R2BucketNotificationMessage>("asset-events");
 *
 * await R2BucketNotification("asset-notifications", {
 *   bucket,
 *   queue,
 *   eventTypes: ["object-create", "object-delete"],
 * });
 * ```
 *
 * @example
 * ## Process notifications with a Worker
 *
 * Complete example showing bucket notifications processed by a Worker:
 *
 * ```ts
 * import { R2Bucket, Queue, R2BucketNotification, Worker, R2BucketNotificationMessage } from "alchemy/cloudflare";
 *
 * const bucket = await R2Bucket("uploads");
 * const queue = await Queue<R2BucketNotificationMessage>("upload-events");
 *
 * await R2BucketNotification("upload-notifications", {
 *   bucket,
 *   queue,
 *   eventTypes: ["object-create"],
 * });
 *
 * await Worker("processor", {
 *   entrypoint: "./src/processor.ts",
 *   eventSources: [queue],
 * });
 * ```
 *
 * @see https://developers.cloudflare.com/r2/buckets/event-notifications/
 */
export const R2BucketNotification = Resource("cloudflare::R2BucketNotification", async function (_id, props) {
    const prefixIsArray = Array.isArray(props.prefix);
    const suffixIsArray = Array.isArray(props.suffix);
    if (prefixIsArray && suffixIsArray) {
        throw new Error("Cannot specify both prefix and suffix as arrays. Only one can be an array at a time.");
    }
    const bucketName = typeof props.bucket === "string" ? props.bucket : props.bucket.name;
    const queueName = typeof props.queue === "string" ? props.queue : props.queue.name;
    const jurisdiction = typeof props.bucket === "string"
        ? props.jurisdiction
        : (props.bucket.jurisdiction ?? props.jurisdiction);
    const adopt = props.adopt ?? this.scope.adopt;
    const api = await createCloudflareApi(props);
    const queueId = await resolveQueueId(api, props.queue);
    const ruleConfigs = [];
    if (prefixIsArray) {
        const prefixArray = props.prefix;
        const singleSuffix = typeof props.suffix === "string" ? props.suffix : undefined;
        for (const prefix of prefixArray) {
            ruleConfigs.push({ prefix, suffix: singleSuffix });
        }
    }
    else if (suffixIsArray) {
        const suffixArray = props.suffix;
        const singlePrefix = typeof props.prefix === "string" ? props.prefix : undefined;
        for (const suffix of suffixArray) {
            ruleConfigs.push({ prefix: singlePrefix, suffix });
        }
    }
    else {
        const singlePrefix = typeof props.prefix === "string" ? props.prefix : undefined;
        const singleSuffix = typeof props.suffix === "string" ? props.suffix : undefined;
        ruleConfigs.push({ prefix: singlePrefix, suffix: singleSuffix });
    }
    if (this.scope.local) {
        const mockRuleIds = ruleConfigs.map((_, i) => Array.isArray(this.output?.ruleId)
            ? (this.output.ruleId[i] ?? "")
            : (this.output?.ruleId ?? ""));
        return {
            type: "r2_bucket_notification",
            ruleId: ruleConfigs.length === 1 ? mockRuleIds[0] : mockRuleIds,
            bucketName,
            queueName,
            eventTypes: props.eventTypes,
            prefix: props.prefix,
            suffix: props.suffix,
            description: props.description,
            jurisdiction,
            accountId: this.output?.accountId ?? "",
        };
    }
    if (this.phase === "delete") {
        if (props.delete !== false && this.output?.ruleId) {
            const ruleIds = Array.isArray(this.output.ruleId)
                ? this.output.ruleId
                : [this.output.ruleId];
            await Promise.all(ruleIds.map((ruleId) => deleteNotificationRule(api, bucketName, queueId, ruleId, {
                jurisdiction,
            })));
        }
        return this.destroy();
    }
    const existingRuleIds = Array.isArray(this.output?.ruleId)
        ? this.output.ruleId
        : this.output?.ruleId
            ? [this.output.ruleId]
            : [];
    if (this.phase === "update" && existingRuleIds.length > 0) {
        const existingRules = await listNotificationRules(api, bucketName, {
            jurisdiction,
        });
        for (const existingRuleId of existingRuleIds) {
            const existingRule = existingRules.find((r) => r.ruleId === existingRuleId);
            if (existingRule) {
                await deleteNotificationRule(api, bucketName, existingRule.queueId, existingRuleId, { jurisdiction });
            }
        }
    }
    const createdRuleIds = [];
    for (const config of ruleConfigs) {
        try {
            const result = await createNotificationRule(api, bucketName, {
                queueId,
                queueName,
                eventTypes: props.eventTypes,
                prefix: config.prefix,
                suffix: config.suffix,
                description: props.description,
                jurisdiction,
            });
            createdRuleIds.push(result.ruleId);
        }
        catch (err) {
            const isConflictError = err instanceof CloudflareApiError &&
                (err.status === 409 ||
                    (err.status === 400 &&
                        err.errorData?.some((e) => e.code === 11020)));
            if (isConflictError && adopt) {
                const matchingRule = await withExponentialBackoff(async () => {
                    const existingRules = await listNotificationRules(api, bucketName, {
                        jurisdiction,
                    });
                    const match = existingRules.find((r) => matchesNotificationRule(r, {
                        queueId,
                        queueName,
                        eventTypes: props.eventTypes,
                        prefix: config.prefix,
                        suffix: config.suffix,
                    }));
                    if (!match) {
                        throw new Error(`Failed to find existing notification rule for bucket ${bucketName} and queue ${queueName}`);
                    }
                    return match;
                }, (error) => error instanceof Error &&
                    error.message.includes("Failed to find existing notification rule"), 5, 1000);
                if (matchingRule) {
                    createdRuleIds.push(matchingRule.ruleId);
                }
                else {
                    throw err;
                }
            }
            else {
                throw err;
            }
        }
    }
    return {
        type: "r2_bucket_notification",
        ruleId: createdRuleIds.length === 1 ? createdRuleIds[0] : createdRuleIds,
        bucketName,
        queueName,
        eventTypes: props.eventTypes,
        prefix: props.prefix,
        suffix: props.suffix,
        description: props.description,
        jurisdiction,
        accountId: api.accountId,
    };
});
function mapEventTypesToActions(eventTypes) {
    const actions = [];
    for (const eventType of eventTypes) {
        if (eventType === "object-create") {
            actions.push("PutObject", "CompleteMultipartUpload", "CopyObject");
        }
        else if (eventType === "object-delete") {
            actions.push("DeleteObject", "LifecycleDeletion");
        }
    }
    return [...new Set(actions)].sort();
}
function arraysEqual(a, b) {
    const sortedA = [...a].sort();
    const sortedB = [...b].sort();
    return (sortedA.length === sortedB.length &&
        sortedA.every((v, i) => v === sortedB[i]));
}
function normalizeRuleFilter(value) {
    return value ?? "";
}
function normalizeQueueId(queueId) {
    return queueId.replaceAll("-", "");
}
function matchesNotificationRule(rule, options) {
    return ((normalizeQueueId(rule.queueId) === normalizeQueueId(options.queueId) ||
        (options.queueName !== undefined &&
            rule.queueName === options.queueName)) &&
        arraysEqual(rule.actions, mapEventTypesToActions(options.eventTypes)) &&
        rule.prefix === normalizeRuleFilter(options.prefix) &&
        rule.suffix === normalizeRuleFilter(options.suffix));
}
async function createNotificationRule(api, bucketName, options) {
    const payload = {
        rules: [
            {
                actions: mapEventTypesToActions(options.eventTypes),
                prefix: options.prefix ?? "",
                suffix: options.suffix ?? "",
            },
        ],
    };
    const response = await api.put(`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration/queues/${options.queueId}`, payload, {
        headers: withJurisdiction({ jurisdiction: options.jurisdiction }),
    });
    if (!response.ok) {
        return await handleApiError(response, "creating", "R2 Bucket Notification", `for bucket ${bucketName}`);
    }
    const data = (await response.json());
    if (data.result?.event_notification_detail_id) {
        return { ruleId: data.result.event_notification_detail_id };
    }
    const createdRule = await withExponentialBackoff(async () => {
        const rules = await listNotificationRules(api, bucketName, {
            jurisdiction: options.jurisdiction,
        });
        const match = rules.find((r) => matchesNotificationRule(r, options));
        if (!match) {
            throw new Error(`Failed to find created notification rule for bucket ${bucketName} and queue ${options.queueName}`);
        }
        return match;
    }, (error) => error instanceof Error && error.message.includes("Failed to find"), 5, 1000);
    if (!createdRule) {
        throw new Error(`Failed to find created notification rule for bucket ${bucketName} and queue ${options.queueName}`);
    }
    return { ruleId: createdRule.ruleId };
}
async function listNotificationRules(api, bucketName, options = {}) {
    const response = await api.get(`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration`, {
        headers: withJurisdiction(options),
    });
    if (!response.ok) {
        if (response.status === 404) {
            return [];
        }
        return await handleApiError(response, "listing", "R2 Bucket Notifications", `for bucket ${bucketName}`);
    }
    const data = (await response.json());
    if (!data.success || !data.result?.queues) {
        return [];
    }
    const rules = [];
    for (const queue of data.result.queues) {
        for (const rule of queue.rules) {
            rules.push({
                ruleId: rule.ruleId,
                queueId: queue.queueId,
                queueName: queue.queueName,
                actions: rule.actions,
                prefix: rule.prefix,
                suffix: rule.suffix,
                createdAt: rule.createdAt,
            });
        }
    }
    return rules;
}
async function deleteNotificationRule(api, bucketName, queueId, ruleId, options = {}) {
    const response = await api.delete(`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration/queues/${queueId}`, {
        headers: withJurisdiction(options),
        body: JSON.stringify({ ruleIds: [ruleId] }),
    });
    if (!response.ok && response.status !== 404) {
        await handleApiError(response, "deleting", "R2 Bucket Notification", `rule ${ruleId} for bucket ${bucketName}`);
    }
}
/**
 * List all notification rules for an R2 bucket
 */
export async function listR2BucketNotifications(api, bucketName, options = {}) {
    return listNotificationRules(api, bucketName, options);
}
//# sourceMappingURL=r2-bucket-notification.js.map