import { Resource } from "../resource.js";
import { Secret } from "../secret.js";
import { DockerApi, normalizeDuration, } from "./api.js";
/**
 * Create and manage a Docker Container
 *
 * @example
 * // Create a simple Nginx container
 * const webContainer = await Container("web", {
 *   image: "nginx:latest",
 *   ports: [
 *     { external: 8080, internal: 80 }
 *   ],
 *   start: true
 * });
 *
 * @example
 * // Create a container with environment variables and volume mounts
 * const appContainer = await Container("app", {
 *   image: customImage, // Using an Alchemy RemoteImage resource
 *   environment: {
 *     NODE_ENV: "production",
 *     API_KEY: "secret-key"
 *   },
 *   volumes: [
 *     { hostPath: "./data", containerPath: "/app/data" }
 *   ],
 *   ports: [
 *     { external: 3000, internal: 3000 }
 *   ],
 *   restart: "always",
 *   start: true
 * });
 *
 * @example
 * // Create a container with healthcheck using numeric values (seconds)
 * const healthyContainer = await Container("api", {
 *   image: "my-api:latest",
 *   ports: [
 *     { external: 3000, internal: 3000 }
 *   ],
 *   healthcheck: {
 *     cmd: ["curl", "-f", "http://localhost:3000/health"],
 *     interval: 30,
 *     timeout: 10,
 *     retries: 3,
 *     startPeriod: 40
 *   },
 *   start: true
 * });
 *
 * @example
 * // Create a container with healthcheck using string duration format
 * const healthyContainer2 = await Container("api2", {
 *   image: "my-api:latest",
 *   ports: [
 *     { external: 3001, internal: 3000 }
 *   ],
 *   healthcheck: {
 *     cmd: ["curl", "-f", "http://localhost:3000/health"],
 *     interval: "30s",
 *     timeout: "10s",
 *     retries: 3,
 *     startPeriod: "1m",
 *     startInterval: "500ms"
 *   },
 *   start: true
 * });
 */
export const Container = Resource("docker::Container", { alwaysUpdate: true }, async function (id, props) {
    // Initialize Docker API client
    const api = new DockerApi();
    // Get image reference
    const imageRef = typeof props.image === "string" ? props.image : props.image.imageRef;
    // Use provided name or generate one based on resource ID
    const containerName = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    if (this.phase === "update" && this.output.name !== containerName) {
        this.replace();
    }
    // Handle delete phase
    if (this.phase === "delete") {
        if (this.output?.id) {
            // Stop container if running
            await api.stopContainer(this.output.id);
            // Remove container
            await api.removeContainer(this.output.id, true);
        }
        // Return destroyed state
        return this.destroy();
    }
    let containerState = "created";
    // Check if container already exists
    const containerExists = await api.containerExists(containerName);
    if (containerExists) {
        // Create phase - check for adoption
        if (this.phase === "create" && !props.adopt) {
            throw new Error(`Container "${containerName}" already exists. Use adopt: true to adopt it.`);
        }
        const [containerInfo] = await api.inspectContainer(containerName);
        // Compute what changes are needed
        if (shouldReplace(imageRef, props, containerInfo)) {
            // Need to recreate - remove existing container
            if (this.phase === "update") {
                // In update phase, we can replace the resource
                // Force because we need to delete the old one first if the name is the same
                return this.replace(true);
            }
            else {
                // In create phase, we cannot replace the resource, so manually delete instead
                await api.removeContainer(containerName, true);
            }
        }
        else {
            // Apply incremental changes without recreating the container
            const { toConnect, toDisconnect } = getNetworkChanges(props, containerInfo);
            // Apply network disconnections
            for (const network of toDisconnect) {
                await api.disconnectNetwork(containerInfo.Id, network);
            }
            // Apply network connections
            for (const network of toConnect) {
                await api.connectNetwork(containerInfo.Id, network.name, {
                    aliases: network.aliases,
                });
            }
            // Optionally start the container if requested
            if (props.start && containerInfo.State.Status !== "running") {
                await api.startContainer(containerInfo.Id);
                containerState = "running";
            }
            return {
                ...props,
                id: containerInfo.Id,
                name: containerName,
                state: containerState,
                createdAt: new Date(containerInfo.Created).getTime(),
                inspect: async () => {
                    const [info] = await api.inspectContainer(containerName);
                    if (!info) {
                        throw new Error(`Container ${containerName} not found`);
                    }
                    return toRuntimeInfo(info);
                },
            };
        }
    }
    // Prepare port mappings
    const portMappings = {};
    if (props.ports) {
        for (const port of props.ports) {
            const protocol = port.protocol || "tcp";
            portMappings[`${port.external}`] = `${port.internal}/${protocol}`;
        }
    }
    // Prepare volume mappings
    const volumeMappings = {};
    if (props.volumes) {
        for (const volume of props.volumes) {
            const readOnlyFlag = volume.readOnly ? ":ro" : "";
            volumeMappings[volume.hostPath] =
                `${volume.containerPath}${readOnlyFlag}`;
        }
    }
    // Create new container
    const containerId = await api.createContainer(imageRef, containerName, {
        ports: portMappings,
        env: normalizeEnvironment(props.environment),
        volumes: volumeMappings,
        cmd: props.command,
        healthcheck: props.healthcheck,
    });
    // Connect to networks if specified
    if (props.networks) {
        for (const network of props.networks) {
            const networkId = typeof network === "string" ? network : network.name;
            await api.connectNetwork(containerId, networkId, {
                aliases: network.aliases,
            });
        }
    }
    // Start container if requested
    if (props.start) {
        await api.startContainer(containerId);
        containerState = "running";
    }
    return {
        ...props,
        id: containerId,
        name: containerName,
        state: containerState,
        createdAt: Date.now(),
        inspect: async () => {
            const [info] = await api.inspectContainer(containerName);
            if (!info) {
                throw new Error(`Container ${containerName} not found`);
            }
            return toRuntimeInfo(info);
        },
    };
});
function toRuntimeInfo(info) {
    const ports = {};
    const networkSettings = info.NetworkSettings;
    if (networkSettings?.Ports) {
        for (const [internal, bindings] of Object.entries(networkSettings.Ports)) {
            if (bindings && bindings.length > 0) {
                ports[internal] = parseInt(bindings[0].HostPort, 10);
            }
        }
    }
    // Also check HostConfig.PortBindings as a fallback or additional source
    // though NetworkSettings.Ports is usually the source of truth for running containers
    if (info.HostConfig.PortBindings) {
        for (const [internal, bindings] of Object.entries(info.HostConfig.PortBindings)) {
            if (bindings && bindings.length > 0 && !(internal in ports)) {
                ports[internal] = parseInt(bindings[0].HostPort, 10);
            }
        }
    }
    return {
        ports,
    };
}
function getNetworkChanges(props, containerInfo) {
    const currentNetworks = new Set(Object.keys(containerInfo.NetworkSettings.Networks || {}));
    const desiredNetworks = new Map((props.networks || []).map((n) => [n.name, n]));
    const toConnect = [];
    const toDisconnect = [];
    for (const network of currentNetworks) {
        if (!desiredNetworks.has(network) && network !== "bridge") {
            toDisconnect.push(network);
        }
    }
    for (const [name, config] of desiredNetworks) {
        if (!currentNetworks.has(name)) {
            toConnect.push(config);
        }
    }
    return { toConnect, toDisconnect };
}
function shouldReplace(imageRef, props, containerInfo) {
    // Check immutable properties that require recreation
    // Image change - compare the image ID/digest if available
    // The container stores the resolved image ID, so we compare against imageRef
    if (containerInfo.Config.Image !== imageRef) {
        return true;
    }
    // Command change
    const containerCmd = containerInfo.Config.Cmd || [];
    if (props.command && // only compare if command is set; otherwise we'd be comparing against the image's default command
        (props.command.length !== containerCmd.length ||
            !props.command.every((c, i) => c === containerCmd[i]))) {
        return true;
    }
    // Environment variables
    if (!compareEnv(normalizeEnvironment(props.environment), containerInfo.Config.Env)) {
        return true;
    }
    // Port bindings
    if (!comparePorts(props.ports, containerInfo.HostConfig.PortBindings)) {
        return true;
    }
    // Volume bindings
    if (!compareVolumes(props.volumes, containerInfo.HostConfig.Binds)) {
        return true;
    }
    // Healthcheck
    if (!compareHealthcheck(props.healthcheck, containerInfo.Config.Healthcheck)) {
        return true;
    }
    // Restart policy
    if (!compareRestartPolicy(props.restart, containerInfo.HostConfig.RestartPolicy)) {
        return true;
    }
    // AutoRemove (removeOnExit)
    if ((props.removeOnExit || false) !== containerInfo.HostConfig.AutoRemove) {
        return true;
    }
    return false;
}
/**
 * Unwrap secrets in given environment variables
 * @internal
 */
function normalizeEnvironment(environment) {
    return Object.fromEntries(Object.entries(environment ?? {}).map(([key, value]) => [
        key,
        Secret.unwrap(value),
    ]));
}
/**
 * Normalize port mappings to a comparable format
 * @internal
 */
function normalizePortMappings(ports) {
    const map = new Map();
    if (!ports)
        return map;
    for (const port of ports) {
        const protocol = port.protocol || "tcp";
        map.set(`${port.external}`, `${port.internal}/${protocol}`);
    }
    return map;
}
/**
 * Normalize volume mappings to a comparable format
 * @internal
 */
function normalizeVolumeMappings(volumes) {
    const set = new Set();
    if (!volumes)
        return set;
    for (const volume of volumes) {
        const readOnlyFlag = volume.readOnly ? ":ro" : "";
        set.add(`${volume.hostPath}:${volume.containerPath}${readOnlyFlag}`);
    }
    return set;
}
/**
 * Compare environment variables
 * @internal
 */
function compareEnv(propsEnv, containerEnv) {
    const propsEntries = Object.entries(propsEnv || {}).sort(([a], [b]) => a.localeCompare(b));
    const containerEntries = (containerEnv || [])
        .map((e) => {
        const idx = e.indexOf("=");
        return idx >= 0 ? [e.slice(0, idx), e.slice(idx + 1)] : null;
    })
        .filter((e) => e !== null)
        // Filter out PATH and other system env vars that Docker adds
        .filter(([key]) => propsEnv && key in propsEnv)
        .sort(([a], [b]) => a.localeCompare(b));
    if (propsEntries.length !== containerEntries.length)
        return false;
    for (let i = 0; i < propsEntries.length; i++) {
        if (propsEntries[i][0] !== containerEntries[i][0] ||
            propsEntries[i][1] !== containerEntries[i][1]) {
            return false;
        }
    }
    return true;
}
/**
 * Compare port bindings
 * @internal
 */
function comparePorts(propsPorts, containerPorts) {
    const propsMap = normalizePortMappings(propsPorts);
    // Extract container port mappings
    const containerMap = new Map();
    if (containerPorts) {
        for (const [containerPort, bindings] of Object.entries(containerPorts)) {
            if (bindings && bindings.length > 0) {
                containerMap.set(bindings[0].HostPort, containerPort);
            }
        }
    }
    if (propsMap.size !== containerMap.size)
        return false;
    for (const [hostPort, containerPort] of propsMap) {
        if (containerMap.get(hostPort) !== containerPort)
            return false;
    }
    return true;
}
/**
 * Compare volume bindings
 * @internal
 */
function compareVolumes(propsVolumes, containerBinds) {
    const propsSet = normalizeVolumeMappings(propsVolumes);
    const containerSet = new Set(containerBinds || []);
    if (propsSet.size !== containerSet.size)
        return false;
    for (const bind of propsSet) {
        if (!containerSet.has(bind))
            return false;
    }
    return true;
}
/**
 * Compare healthcheck configuration
 * @internal
 */
function compareHealthcheck(propsHc, containerHc) {
    // Both undefined/null
    if (!propsHc && !containerHc)
        return true;
    // One defined, one not
    if (!propsHc || !containerHc)
        return false;
    // Compare command
    const propsCmd = Array.isArray(propsHc.cmd)
        ? propsHc.cmd.join(" ")
        : propsHc.cmd;
    const containerCmd = containerHc.Test
        ? containerHc.Test.slice(1).join(" ")
        : "";
    if (propsCmd !== containerCmd)
        return false;
    // Helper to convert Duration to nanoseconds for comparison
    const toNanos = (d) => {
        if (d === undefined)
            return 0;
        const str = normalizeDuration(d);
        const match = str.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
        if (!match)
            return 0;
        const value = parseFloat(match[1]);
        const unit = match[2];
        switch (unit) {
            case "ms":
                return value * 1_000_000;
            case "s":
                return value * 1_000_000_000;
            case "m":
                return value * 60 * 1_000_000_000;
            case "h":
                return value * 3600 * 1_000_000_000;
            default:
                return 0;
        }
    };
    if (toNanos(propsHc.interval) !== (containerHc.Interval || 0))
        return false;
    if (toNanos(propsHc.timeout) !== (containerHc.Timeout || 0))
        return false;
    if ((propsHc.retries || 0) !== (containerHc.Retries || 0))
        return false;
    if (toNanos(propsHc.startPeriod) !== (containerHc.StartPeriod || 0))
        return false;
    if (toNanos(propsHc.startInterval) !== (containerHc.StartInterval || 0))
        return false;
    return true;
}
/**
 * Compare restart policy
 * @internal
 */
function compareRestartPolicy(propsRestart, containerRestart) {
    const propsPolicy = propsRestart || "no";
    return propsPolicy === containerRestart.Name;
}
//# sourceMappingURL=container.js.map