import { alchemy } from "../alchemy.js";
import { Resource, ResourceKind } from "../resource.js";
import { logger } from "../util/logger.js";
import { handleApiError } from "./api-error.js";
import { createCloudflareApi, } from "./api.js";
import { DnsRecords } from "./dns-records.js";
import { findZoneForHostname } from "./zone.js";
export function isTunnel(resource) {
    return resource?.[ResourceKind] === "cloudflare::Tunnel";
}
/**
 * Creates and manages a Cloudflare Tunnel, which provides a secure connection between
 * your origin server and Cloudflare's edge. This resource handles the tunnel lifecycle
 * (create, update, delete) and configuration.
 *
 * @remarks
 * After creating a tunnel, use the returned credentials and token to run the
 * cloudflared connector on your origin server.
 *
 * When hostnames are specified in ingress rules, this resource automatically creates
 * the required DNS CNAME records pointing to <tunnel-id>.cfargotunnel.com, following
 * the Cloudflare API documentation for connecting applications (step 3a).
 *
 * @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
 * @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel-api/
 * @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/
 *
 * @example
 * // Create a basic tunnel
 * const tunnel = await Tunnel("my-app", {
 *   name: "my-app-tunnel"
 * });
 *
 * // Run cloudflared with:
 * // cloudflared tunnel run --token <tunnel.token.unencrypted>
 *
 * @example
 * // Create a tunnel with ingress configuration for a web app
 * // DNS records are automatically created for each hostname
 * const webTunnel = await Tunnel("web-app", {
 *   name: "web-app-tunnel",
 *   ingress: [
 *     {
 *       hostname: "app.example.com",
 *       service: "http://localhost:3000"
 *     },
 *     {
 *       service: "http_status:404"  // catch-all rule
 *     }
 *   ]
 * });
 * // A CNAME record for app.example.com → webTunnel.tunnelId.cfargotunnel.com
 * // is automatically created in the appropriate zone
 *
 * @example
 * // Create a tunnel with multiple services and origin configuration
 * const apiTunnel = await Tunnel("api", {
 *   name: "api-tunnel",
 *   ingress: [
 *     {
 *       hostname: "api.example.com",
 *       path: "/v1/*",
 *       service: "http://localhost:8080",
 *       originRequest: {
 *         httpHostHeader: "api.internal",
 *         connectTimeout: 30
 *       }
 *     },
 *     {
 *       hostname: "api.example.com",
 *       path: "/v2/*",
 *       service: "http://localhost:8081"
 *     },
 *     {
 *       service: "http_status:404"
 *     }
 *   ]
 * });
 *
 * @example
 * // Create a tunnel for private network access with WARP
 * const privateTunnel = await Tunnel("private-network", {
 *   name: "private-network-tunnel",
 *   warpRouting: {
 *     enabled: true
 *   }
 * });
 *
 * @example
 * // Create a tunnel with origin request configuration
 * const secureTunnel = await Tunnel("secure", {
 *   name: "secure-tunnel",
 *   originRequest: {
 *     noTLSVerify: false,
 *     connectTimeout: 30,
 *     httpHostHeader: "internal.service"
 *   },
 *   ingress: [
 *     {
 *       hostname: "secure.example.com",
 *       service: "https://localhost:8443"
 *     },
 *     {
 *       service: "http_status:404"
 *     }
 *   ]
 * });
 *
 * @example
 * // Adopt an existing tunnel if it already exists
 * const existingTunnel = await Tunnel("existing", {
 *   name: "existing-tunnel",
 *   adopt: true,
 *   ingress: [
 *     {
 *       hostname: "updated.example.com",
 *       service: "http://localhost:5000"
 *     },
 *     {
 *       service: "http_status:404"
 *     }
 *   ]
 * });
 *
 * @example
 * // Tunnel with automatic DNS record creation
 * // The Tunnel resource automatically creates DNS records for hostnames in ingress rules
 * const appTunnel = await Tunnel("app", {
 *   name: "app-tunnel",
 *   ingress: [
 *     {
 *       hostname: "app.example.com",
 *       service: "http://localhost:3000"
 *     },
 *     {
 *       hostname: "api.example.com",
 *       service: "http://localhost:8080"
 *     },
 *     {
 *       service: "http_status:404"
 *     }
 *   ]
 * });
 * // DNS CNAME records are automatically created:
 * // - app.example.com → {tunnelId}.cfargotunnel.com
 * // - api.example.com → {tunnelId}.cfargotunnel.com
 *
 * // Run the tunnel:
 * // cloudflared tunnel run --token <appTunnel.token.unencrypted>
 *
 * @example
 * // For advanced DNS control, you can still manually manage DNS records
 * // by omitting hostnames from ingress rules:
 * const tunnel = await Tunnel("manual-dns", {
 *   name: "manual-dns-tunnel",
 *   ingress: [
 *     {
 *       service: "http://localhost:3000"
 *     },
 *     {
 *       service: "http_status:404"
 *     }
 *   ]
 * });
 * // Then create DNS records separately with custom configuration
 */
export const Tunnel = Resource("cloudflare::Tunnel", async function (id, props) {
    // Create Cloudflare API client with automatic account discovery
    const api = await createCloudflareApi(props);
    const name = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    // Track if we're replacing the tunnel (don't fetch token in this case)
    let isReplacing = false;
    if (this.phase === "update" && this.output.name !== name) {
        console.log("replacing tunnel", this.output.name, name);
        this.replace(true);
    }
    if (this.phase === "delete") {
        // For delete operations, check if the tunnel ID exists in the output
        const tunnelId = this.output?.tunnelId;
        if (tunnelId && props.delete !== false) {
            await deleteTunnel(api, tunnelId);
        }
        // Return destroyed state
        return this.destroy();
    }
    // For create or update operations
    let tunnelData;
    if (this.phase === "update" && this.output?.tunnelId) {
        // Get existing tunnel data
        tunnelData = await getTunnel(api, this.output.tunnelId);
        // Check if name is being changed - tunnel names are immutable
        if (props.name && props.name !== tunnelData.name) {
            this.replace(true);
        }
        // Update configuration if provided
        if ((props.ingress || props.warpRouting || props.originRequest) &&
            props.configSrc !== "local") {
            const config = {
                ingress: props.ingress,
                warpRouting: props.warpRouting,
                originRequest: props.originRequest,
            };
            await updateTunnelConfiguration(api, this.output.tunnelId, config);
        }
    }
    else {
        // Create new tunnel
        try {
            tunnelData = await createTunnel(api, {
                name,
                configSrc: props.configSrc,
                tunnelSecret: props.tunnelSecret,
                metadata: props.metadata,
            });
            // Configure tunnel if config is provided
            if ((props.ingress || props.warpRouting || props.originRequest) &&
                props.configSrc !== "local") {
                await updateTunnelConfiguration(api, tunnelData.id, {
                    ingress: props.ingress,
                    warpRouting: props.warpRouting,
                    originRequest: props.originRequest,
                });
            }
        }
        catch (error) {
            // Check if this is a "tunnel already exists" error and adopt is enabled
            if (props.adopt &&
                error instanceof Error &&
                (error.message.includes("already have a tunnel with this name") ||
                    error.message.includes("already exists"))) {
                logger.log(`Tunnel '${name}' already exists, adopting it`);
                // Find the existing tunnel by name
                const existingTunnel = await findTunnelByName(api, name);
                if (!existingTunnel) {
                    throw new Error(`Failed to find existing tunnel '${name}' for adoption`);
                }
                tunnelData = existingTunnel;
                // Update configuration if provided
                if ((props.ingress || props.warpRouting || props.originRequest) &&
                    props.configSrc !== "local") {
                    const config = {
                        ingress: props.ingress,
                        warpRouting: props.warpRouting,
                        originRequest: props.originRequest,
                    };
                    await updateTunnelConfiguration(api, existingTunnel.id, config);
                }
            }
            else {
                // Re-throw the error if adopt is false or it's not an "already exists" error
                throw error;
            }
        }
    }
    // Ensure tunnel token is available (unless we're replacing)
    // Sometimes Cloudflare doesn't return the token in the initial response
    if (!isReplacing && !tunnelData.token) {
        tunnelData.token = await getTunnelToken(api, tunnelData.id);
    }
    // Handle DNS records for ingress hostnames
    let dnsRecords = this.output?.dnsRecords || {};
    // Extract hostnames from ingress rules
    const hostnames = new Set();
    if (props.ingress) {
        for (const rule of props.ingress) {
            if (rule.hostname && !rule.hostname.includes("*")) {
                // Skip wildcard hostnames as they need special handling
                hostnames.add(rule.hostname);
            }
        }
    }
    // Create or update DNS records for each hostname
    if (hostnames.size > 0) {
        // Group hostnames by zone
        const hostnamesByZone = new Map();
        for (const hostname of hostnames) {
            const { zoneId } = await findZoneForHostname(api, hostname);
            if (!hostnamesByZone.has(zoneId)) {
                hostnamesByZone.set(zoneId, { zoneId, hostnames: [] });
            }
            hostnamesByZone.get(zoneId).hostnames.push(hostname);
        }
        // Create DNS records for each zone in parallel
        const dnsPromises = Array.from(hostnamesByZone.entries()).map(async ([zoneId, { hostnames: zoneHostnames }]) => {
            const dnsResourceId = `${id}-dns-${zoneId}`;
            const dnsResource = await DnsRecords(dnsResourceId, {
                zoneId,
                records: zoneHostnames.map((hostname) => ({
                    name: hostname,
                    type: "CNAME",
                    content: `${tunnelData.id}.cfargotunnel.com`,
                    proxied: true,
                    comment: `Cloudflare Tunnel: ${tunnelData.name}`,
                })),
            });
            // Return records for mapping storage
            return dnsResource.records;
        });
        const allDnsRecords = await Promise.all(dnsPromises);
        // Store DNS record mappings
        for (const records of allDnsRecords) {
            for (const record of records) {
                dnsRecords[record.name] = record.id;
            }
        }
    }
    // Clean up DNS records that are no longer needed
    const currentHostnames = Array.from(hostnames);
    for (const hostname of Object.keys(dnsRecords)) {
        if (!currentHostnames.includes(hostname)) {
            delete dnsRecords[hostname];
        }
    }
    // Transform API response to our interface
    return {
        tunnelId: tunnelData.id,
        accountTag: tunnelData.account_tag,
        name: tunnelData.name,
        createdAt: tunnelData.created_at,
        deletedAt: tunnelData.deleted_at,
        credentials: tunnelData.credentials_file
            ? {
                accountTag: tunnelData.credentials_file.AccountTag,
                tunnelId: tunnelData.credentials_file.TunnelID,
                tunnelName: tunnelData.credentials_file.TunnelName,
                tunnelSecret: alchemy.secret(tunnelData.credentials_file.TunnelSecret),
            }
            : this.output?.credentials || {
                accountTag: tunnelData.account_tag,
                tunnelId: tunnelData.id,
                tunnelName: tunnelData.name,
                tunnelSecret: alchemy.secret(""),
            },
        token: tunnelData.token
            ? alchemy.secret(tunnelData.token)
            : this.output?.token || alchemy.secret(""),
        metadata: props.metadata,
        ingress: props.ingress,
        warpRouting: props.warpRouting,
        originRequest: props.originRequest,
        configSrc: props.configSrc,
        dnsRecords: Object.keys(dnsRecords).length > 0 ? dnsRecords : undefined,
    };
});
/**
 * Get tunnel details
 * @internal
 */
export async function getTunnel(api, tunnelId) {
    const response = await api.get(`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}`);
    if (!response.ok) {
        await handleApiError(response, "get", "tunnel", tunnelId);
    }
    const data = (await response.json());
    return data.result;
}
/**
 * Get tunnel configuration
 * @internal
 */
export async function getTunnelConfiguration(api, tunnelId) {
    const response = await api.get(`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/configurations`);
    if (!response.ok) {
        await handleApiError(response, "get configuration", "tunnel", tunnelId);
    }
    const data = (await response.json());
    return data.result.config;
}
/**
 * Delete a tunnel
 * @internal
 */
async function deleteTunnel(api, tunnelId) {
    const response = await api.delete(`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}`);
    if (!response.ok && response.status !== 404) {
        await handleApiError(response, "delete", "tunnel", tunnelId);
    }
}
/**
 * Create a new tunnel
 * @internal
 */
async function createTunnel(api, props) {
    const payload = {
        name: props.name,
        config_src: props.configSrc || "cloudflare",
    };
    if (props.tunnelSecret) {
        payload.tunnel_secret = props.tunnelSecret.unencrypted;
    }
    if (props.metadata) {
        payload.metadata = props.metadata;
    }
    const response = await api.post(`/accounts/${api.accountId}/cfd_tunnel`, payload);
    if (!response.ok) {
        await handleApiError(response, "create", "tunnel", props.name);
    }
    const data = (await response.json());
    return data.result;
}
/**
 * Update tunnel configuration
 * @internal
 */
async function updateTunnelConfiguration(api, tunnelId, config) {
    const response = await api.put(`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/configurations`, { config });
    if (!response.ok) {
        await handleApiError(response, "update configuration", "tunnel", tunnelId);
    }
}
/**
 * List all tunnels with pagination support
 * @internal
 */
export async function listTunnels(api, options) {
    const tunnels = [];
    let page = 1;
    const perPage = 100; // Maximum allowed by API
    let hasMorePages = true;
    const limit = options?.limit;
    const includeDeleted = options?.includeDeleted ?? false;
    while (hasMorePages) {
        const params = new URLSearchParams({
            page: page.toString(),
            per_page: perPage.toString(),
        });
        if (!includeDeleted) {
            params.append("is_deleted", "false");
        }
        const response = await api.get(`/accounts/${api.accountId}/cfd_tunnel?${params.toString()}`);
        if (!response.ok) {
            await handleApiError(response, "list", "tunnel", "all");
        }
        const data = (await response.json());
        tunnels.push(...data.result);
        const resultInfo = data.result_info;
        // Check if we've reached the limit
        if (limit && tunnels.length >= limit) {
            return tunnels.slice(0, limit);
        }
        // Check if we've seen all pages
        hasMorePages =
            resultInfo.page * resultInfo.per_page < resultInfo.total_count;
        page++;
    }
    return tunnels;
}
/**
 * Find a tunnel by name with pagination support
 * @internal
 */
export async function findTunnelByName(api, name) {
    let page = 1;
    const perPage = 100; // Maximum allowed by API
    let hasMorePages = true;
    while (hasMorePages) {
        const response = await api.get(`/accounts/${api.accountId}/cfd_tunnel?page=${page}&per_page=${perPage}&is_deleted=false`);
        if (!response.ok) {
            await handleApiError(response, "list", "tunnel", "all");
        }
        const data = (await response.json());
        const tunnels = data.result;
        const resultInfo = data.result_info;
        // Look for a tunnel with matching name
        const match = tunnels.find((tunnel) => tunnel.name === name);
        if (match) {
            return match;
        }
        // Check if we've seen all pages
        hasMorePages =
            resultInfo.page * resultInfo.per_page < resultInfo.total_count;
        page++;
    }
    // No matching tunnel found
    return null;
}
/**
 * Get tunnel token
 */
async function getTunnelToken(api, tunnelId) {
    const response = await api.get(`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/token`);
    if (!response.ok) {
        logger.error(`Failed to fetch token for tunnel ${tunnelId}: ${response.status} ${response.statusText}`);
        await handleApiError(response, "get token", "tunnel", tunnelId);
    }
    const data = (await response.json());
    if (!data.result) {
        logger.warn(`Could not retrieve token for tunnel ${tunnelId}`);
    }
    else {
        logger.log(`Successfully fetched token for tunnel ${tunnelId}`);
    }
    return data.result;
}
//# sourceMappingURL=tunnel.js.map