import { DockerApi } from "../docker/api.js";
import { Image, } from "../docker/image.js";
import { pushImageToRegistry } from "../docker/registry.js";
import { Resource } from "../resource.js";
import { Scope } from "../scope.js";
import { createCloudflareApi, } from "./api.js";
/**
 * Type guard to check if a binding is a Container binding.
 *
 * @param binding - The binding to check
 * @returns True if the binding is a Container binding
 */
export function isContainer(binding) {
    return binding.type === "container";
}
/**
 * Normalize an image reference for Cloudflare Container deployments.
 *
 * Follows wrangler's resolveImageName logic:
 * - Short names like "myapp:v1" → "registry.cloudflare.com/{accountId}/myapp:v1"
 * - CF registry without accountId like "registry.cloudflare.com/myapp:v1" → adds accountId
 * - External registries like "docker.io/nginx:1.25" → pass through unchanged
 */
export function resolveImageName(accountId, image) {
    const cfRegistry = getCloudflareContainerRegistry();
    // Check if image has a registry prefix (contains a dot in the first segment)
    const segments = image.split("/");
    const hasRegistryPrefix = segments.length > 1 && segments[0].includes(".");
    if (!hasRegistryPrefix) {
        // Short name like "myapp:v1" → add CF registry + accountId
        return `${cfRegistry}/${accountId}/${image}`;
    }
    if (image.startsWith(`${cfRegistry}/`)) {
        // CF registry image - check if accountId is present
        const afterRegistry = image.slice(`${cfRegistry}/`.length);
        const segments = afterRegistry.split("/");
        // If only one segment (e.g., "myapp:tag"), add accountId
        // If first segment doesn't look like an accountId (32 hex chars), add it
        if (segments.length === 1) {
            return `${cfRegistry}/${accountId}/${afterRegistry}`;
        }
        // Check if first segment is the accountId (32 hex chars)
        const possibleAccountId = segments[0];
        const isAccountId = /^[a-f0-9]{32}$/.test(possibleAccountId);
        if (!isAccountId) {
            // First segment is not an accountId, prepend it
            return `${cfRegistry}/${accountId}/${afterRegistry}`;
        }
    }
    // External registry or already fully-qualified CF registry → pass through
    return image;
}
export async function Container(id, props) {
    // Validate that build and image are mutually exclusive
    if (props.build && props.image) {
        throw new Error("Container: specify either `build` or `image`, not both.");
    }
    const scope = Scope.current;
    const name = props.name ?? scope.createPhysicalName(id);
    const tag = props.tag === undefined || props.tag === "latest"
        ? `latest-${Date.now()}`
        : props.tag;
    const output = {
        type: "container",
        id,
        name,
        className: props.className,
        maxInstances: props.maxInstances,
        scriptName: props.scriptName,
        instanceType: props.instanceType,
        observability: props.observability,
        schedulingPolicy: props.schedulingPolicy,
        sqlite: true,
        dev: props.dev,
        adopt: props.adopt,
        rollout: props.rollout,
    };
    const isDev = scope.local && !props.dev?.remote;
    // Prebuilt image path: use as-is, no Docker pull/push
    // This matches wrangler's behavior where registry URIs are passed directly
    // to the Cloudflare API without any local Docker operations
    if (props.image && !props.build) {
        const rawImageRef = typeof props.image === "string" ? props.image : props.image.imageRef;
        if (isDev) {
            // For local dev with prebuilt images, we need to pull and re-tag
            const dockerApi = new DockerApi();
            const devImageRef = `cloudflare-dev/${name}:${tag}`;
            if (isCloudflareRegistryLink(rawImageRef)) {
                // For CF registry images, authenticate before pulling
                const api = await createCloudflareApi(props);
                const credentials = await getContainerCredentials(api);
                const cfRegistry = getCloudflareContainerRegistry();
                await dockerApi.login(cfRegistry, credentials.username || credentials.user, credentials.password);
                try {
                    // CF Containers run on linux/amd64, so we need to pull that platform
                    await dockerApi.pullImage(rawImageRef, { platform: "linux/amd64" });
                    await dockerApi.tagImage(rawImageRef, devImageRef);
                }
                finally {
                    await dockerApi.logout(cfRegistry);
                }
                return {
                    ...output,
                    image: {
                        kind: "Image",
                        name: `cloudflare-dev/${name}`,
                        imageRef: devImageRef,
                        tag,
                        builtAt: Date.now(),
                        build: undefined,
                    },
                };
            }
            // For external registry images in dev mode, pull and re-tag for Miniflare
            const image = await Image(id, {
                image: props.image,
                tag,
            });
            await dockerApi.tagImage(image.imageRef, devImageRef);
            return {
                ...output,
                image: {
                    ...image,
                    name: `cloudflare-dev/${name}`,
                    imageRef: devImageRef,
                },
            };
        }
        // Non-dev mode: normalize the image reference and use directly
        const api = await createCloudflareApi(props);
        const imageRef = resolveImageName(api.accountId, rawImageRef);
        // Extract name and tag from the resolved reference
        const [refWithoutDigest] = imageRef.split("@");
        const lastColonIndex = refWithoutDigest.lastIndexOf(":");
        const namePart = lastColonIndex > -1
            ? refWithoutDigest.slice(0, lastColonIndex)
            : refWithoutDigest;
        const tagPart = lastColonIndex > -1 ? refWithoutDigest.slice(lastColonIndex + 1) : tag;
        const image = {
            kind: "Image",
            name: namePart,
            imageRef,
            tag: tagPart,
            builtAt: Date.now(),
            build: undefined,
        };
        return {
            ...output,
            image,
        };
    }
    // Build path: build locally and push to Cloudflare registry
    if (isDev) {
        const image = await Image(id, {
            ...props,
            name: `cloudflare-dev/${name}`, // prefix used by Miniflare
            tag,
            build: props.build ?? {
                dockerfile: "Dockerfile",
                platform: "linux/amd64",
                context: process.cwd(),
            },
        });
        return {
            ...output,
            image,
        };
    }
    const api = await createCloudflareApi(props);
    const image = await Image(id, {
        name: `${api.accountId}/${name}`,
        tag,
        build: props.build
            ? {
                platform: props.build?.platform ?? "linux/amd64",
                ...props.build,
            }
            : {
                dockerfile: "Dockerfile",
                platform: "linux/amd64",
                context: process.cwd(),
            },
        image: props.image,
        skipPush: true,
    });
    const credentials = await getContainerCredentials(api);
    const pushedImage = await pushImageToRegistry(image.imageRef, {
        server: "registry.cloudflare.com",
        username: credentials.username || credentials.user,
        password: credentials.password,
    });
    return {
        ...output,
        image: {
            ...image,
            imageRef: pushedImage.imageRef,
            repoDigest: pushedImage.repoDigest,
        },
    };
}
/**
 * Deploy and manage container applications on Cloudflare's global network.
 *
 * ContainerApplication creates a managed container deployment that runs your Docker images
 * with automatic scaling, scheduling, and integration with Cloudflare's services.
 *
 * @example
 * // Deploy a simple web application container
 * const webApp = await ContainerApplication("my-web-app", {
 *   name: "my-web-app",
 *   image: await Image("web-app", {
 *     name: "web-app",
 *     build: {
 *       context: "./docker/web-app"
 *     }
 *   }),
 *   instances: 1,
 *   maxInstances: 3
 * });
 *
 * @example
 * // Deploy a container with GPU support for AI workloads
 * const aiApp = await ContainerApplication("ai-inference", {
 *   name: "ai-inference",
 *   image: await Image("ai-model", {
 *     name: "ai-model",
 *     build: {
 *       context: "./docker/ai"
 *     }
 *   }),
 *   schedulingPolicy: "gpu",
 *   instances: 2,
 *   maxInstances: 5
 * });
 *
 * @example
 * // Deploy a container integrated with Durable Objects
 * const doApp = await ContainerApplication("stateful-app", {
 *   name: "stateful-app",
 *   image: await Image("do-app", {
 *     name: "do-app",
 *     build: {
 *       context: "./container"
 *     }
 *   }),
 *   durableObjects: {
 *     namespaceId: myDurableObjectNamespace.id
 *   },
 *   instances: 1,
 *   maxInstances: 10
 * });
 *
 * @example
 * // Create a Container binding for use in a Worker
 * const worker = await Worker("my-worker", {
 *   name: "my-worker",
 *   entrypoint: "./src/worker.ts",
 *   bindings: {
 *     MY_CONTAINER: new Container("my-container", {
 *       className: "MyContainerClass",
 *       image: await Image("container-do", {
 *         name: "container-do",
 *         context: "./docker/durable-object"
 *       }),
 *       maxInstances: 100,
 *       name: "my-container-do"
 *     })
 *   }
 * });
 *
 * @example
 * // Adopt an existing container application
 * const existingApp = await ContainerApplication("existing-app", {
 *   name: "existing-app",
 *   adopt: true, // Will adopt instead of failing if already exists
 *   image: await Image("updated-app", {
 *     name: "updated-app",
 *     build: {
 *       context: "./docker/updated"
 *     }
 *   }),
 *   instances: 2,
 *   maxInstances: 5
 * });
 */
export const ContainerApplication = Resource("cloudflare::ContainerApplication", async function (id, props) {
    const applicationName = props.name ?? this.scope.createPhysicalName(id);
    if (this.scope.local && props.dev) {
        return {
            id: this.output?.id ?? "",
            name: applicationName,
        };
    }
    const adopt = props.adopt ?? this.scope.adopt;
    const api = await createCloudflareApi(props);
    if (this.phase === "delete") {
        if (props.delete !== false && this.output?.id) {
            // Delete the container application
            await deleteContainerApplication(api, this.output.id);
        }
        return this.destroy();
    }
    // Prefer the immutable repo digest if present. Falls back to the tag reference.
    const imageReference = props.image.repoDigest ?? props.image.imageRef;
    const configuration = {
        image: imageReference,
        instance_type: props.instanceType ?? "dev",
        observability: {
            logs: {
                enabled: true,
            },
        },
    };
    if (this.phase === "update" && this.output?.id) {
        const application = await updateContainerApplication(api, this.output.id, {
            instances: props.instances ?? 1,
            max_instances: props.maxInstances ?? 10,
            scheduling_policy: props.schedulingPolicy ?? "default",
            configuration,
        });
        // TODO(sam): should we wait for the rollout to complete?
        const stepPercentage = props.rollout?.strategy === "immediate"
            ? 100
            : (props.rollout?.stepPercentage ?? 25);
        await createContainerApplicationRollout(api, application.id, {
            description: props.rollout?.strategy === "immediate"
                ? "Immediate update"
                : "Progressive update",
            strategy: "rolling",
            kind: props.rollout?.kind ?? "full_auto",
            step_percentage: stepPercentage,
            target_configuration: configuration,
        });
        return {
            id: application.id,
            name: application.name,
        };
    }
    else {
        let application;
        try {
            application = await createContainerApplication(api, adopt, {
                name: applicationName,
                scheduling_policy: props.schedulingPolicy ?? "default",
                instances: props.instances ?? 1,
                max_instances: props.maxInstances ?? 1,
                durable_objects: props.durableObjects
                    ? {
                        namespace_id: props.durableObjects.namespaceId,
                    }
                    : undefined,
                constraints: {
                    tier: 1,
                },
                configuration: {
                    image: imageReference,
                    instance_type: props.instanceType ?? "dev",
                    observability: {
                        logs: {
                            enabled: true,
                        },
                    },
                },
            });
        }
        catch (error) {
            // Check if this is an "already exists" error and adopt is enabled
            if (adopt &&
                error instanceof Error &&
                error.message.includes("already exists")) {
                // Find the existing container application
                const existingApplication = await findContainerApplicationByName(api, applicationName);
                if (!existingApplication) {
                    throw new Error(`Failed to find existing container application '${applicationName}' for adoption`);
                }
                // Update the existing application with new properties
                application = await updateContainerApplication(api, existingApplication.id, {
                    instances: props.instances ?? existingApplication.instances,
                    max_instances: props.maxInstances ?? existingApplication.max_instances,
                    scheduling_policy: props.schedulingPolicy ?? existingApplication.scheduling_policy,
                    configuration,
                });
                // Create a rollout for the updated configuration
                const adoptStepPercentage = props.rollout?.strategy === "immediate"
                    ? 100
                    : (props.rollout?.stepPercentage ?? 25);
                await createContainerApplicationRollout(api, application.id, {
                    description: props.rollout?.strategy === "immediate"
                        ? "Immediate update on adoption"
                        : "Update configuration on adoption",
                    strategy: "rolling",
                    kind: props.rollout?.kind ?? "full_auto",
                    step_percentage: adoptStepPercentage,
                    target_configuration: configuration,
                });
            }
            else {
                // Re-throw the error if adopt is false or different error
                throw error;
            }
        }
        return {
            id: application.id,
            name: application.name,
        };
    }
});
export async function getContainerApplicationByName(api, name) {
    return (await listContainerApplications(api)).find((app) => app.name === name);
}
export async function getContainerApplication(api, applicationId) {
    const response = await api.get(`/accounts/${api.accountId}/containers/applications/${applicationId}`);
    const result = (await response.json());
    if (response.ok) {
        return result.result;
    }
}
export async function listContainerApplications(api) {
    const deployments = await api.get(`/accounts/${api.accountId}/containers/applications`);
    const response = (await deployments.json());
    if (deployments.ok) {
        return response.result;
    }
    throw Error(`Failed to list container applications: ${response.errors.map((e) => e.message).join(", ")}`);
}
export async function findContainerApplicationByName(api, name) {
    const applications = await listContainerApplications(api);
    return applications.find((app) => app.name === name);
}
export async function createContainerApplication(api, adopt, body) {
    const response = await api.post(`/accounts/${api.accountId}/containers/applications`, body);
    const result = (await response.json());
    if (response.ok) {
        return result.result;
    }
    const error = result.errors.find((e) => e.code === 1608);
    if (error) {
        // WEIRD: json-encoded error message - might change when they release an official API
        const errorMessage = JSON.parse(error.message);
        if (errorMessage.error === "DURABLE_OBJECT_ALREADY_HAS_APPLICATION") {
            if (!adopt) {
                throw new Error(`Durable Object namespace ${body.durable_objects?.namespace_id} already has an application with id ${errorMessage.details.id}. Use adopt: true to adopt it.`);
            }
            const application = await getContainerApplication(api, errorMessage.details.id);
            if (application) {
                const currentId = application.durable_objects.namespace_id;
                const desiredId = body.durable_objects?.namespace_id;
                if (currentId === desiredId) {
                    await updateContainerApplication(api, application.id, {
                        configuration: body.configuration,
                        instances: body.instances,
                        constraints: body.constraints,
                        scheduling_policy: body.scheduling_policy,
                        max_instances: body.max_instances,
                        affinities: body.affinities,
                        // TODO(sam): API does not accepet this as input, we may need to repace
                        // durable_objects: body.durable_objects,
                    });
                    // it's the same application, so we can just adpt it
                    return application;
                }
                else {
                    // TODO(sam): not sure what to do in this case
                    throw new Error("Cannot adopt an existing Container Application that is bound to a different Durable Object");
                }
            }
        }
    }
    throw Error(`Failed to create container application: ${result.errors?.map((e) => `[${e.code}] ${e.message}`).join(", ") ?? "Unknown error"}`);
}
export async function updateContainerApplication(api, applicationId, body) {
    const response = await api.patch(`/accounts/${api.accountId}/containers/applications/${applicationId}`, body);
    const result = (await response.json());
    if (response.ok) {
        return result.result;
    }
    throw Error(`Failed to create container application: ${result.errors?.map((e) => `[${e.code}] ${e.message}`).join(", ") ?? "Unknown error"}`);
}
export async function deleteContainerApplication(api, applicationId) {
    const response = await api.delete(`/accounts/${api.accountId}/containers/applications/${applicationId}`);
    const result = (await response.json());
    // Treat missing applications as already-deleted so that destroy() is idempotent
    if (response.ok || response.status === 404) {
        return result?.result;
    }
    const errorMessages = Array.isArray(result?.errors)
        ? result.errors.map((e) => e.message).join(", ")
        : (result?.error ?? "Unknown error");
    throw Error(`Failed to delete container application: ${errorMessages}`);
}
export async function createContainerApplicationRollout(api, applicationId, body) {
    const response = await api.post(`/accounts/${api.accountId}/containers/applications/${applicationId}/rollouts`, body);
    const result = (await response.json());
    if (response.ok) {
        return result.result;
    }
    throw Error(`Failed to create container application rollout: ${result.errors.map((e) => `[${e.code}] ${e.message}`).join(", ")}`);
}
export async function getContainerCredentials(api, registryId = "registry.cloudflare.com") {
    const credentials = await api.post(`/accounts/${api.accountId}/containers/registries/${registryId}/credentials`, {
        permissions: ["pull", "push"],
        expiration_minutes: 60,
    });
    const result = (await credentials.json());
    if (credentials.ok) {
        return result.result;
    }
    throw Error(`Failed to get container credentials: ${result.errors.map((e) => e.message).join(", ")}`);
}
// The Cloudflare managed registry is special in that the namespaces for repos should always
// start with the Cloudflare Account tag
// This is a helper to generate the image tag with correct namespace attached to the Cloudflare Registry host
export const getCloudflareRegistryWithAccountNamespace = (accountID, tag) => {
    return `${getCloudflareContainerRegistry()}/${accountID}/${tag}`;
};
// default cloudflare managed registry, can be overriden with the env var - CLOUDFLARE_CONTAINER_REGISTRY
export const getCloudflareContainerRegistry = () => {
    // previously defaulted to registry.cloudchamber.cfdata.org
    return process.env.CLOUDFLARE_CONTAINER_REGISTRY ?? "registry.cloudflare.com";
};
/**
 * Given a container image that is a registry link, this function
 * returns true if the link points the Cloudflare container registry
 * (defined as per `getCloudflareContainerRegistry` above)
 */
export function isCloudflareRegistryLink(image) {
    const cfRegistry = getCloudflareContainerRegistry();
    return image.includes(cfRegistry);
}
/** Prefixes with the cloudflare-dev namespace. The name should be the container's DO classname, and the tag a build uuid. */
export const getDevContainerImageName = (name, tag) => {
    return `${MF_DEV_CONTAINER_PREFIX}/${name.toLowerCase()}:${tag}`;
};
export const MF_DEV_CONTAINER_PREFIX = "cloudflare-dev";
export async function getContainerIdentity(api) {
    const metrics = await api.get(`/accounts/${api.accountId}/containers/me`);
    const result = (await metrics.json());
    if (metrics.ok) {
        return result.result;
    }
    throw Error(`Failed to get container me: ${result.errors.map((e) => e.message).join(", ")}`);
}
//# sourceMappingURL=container.js.map