import { XMLParser } from "fast-xml-parser";
import { Resource } from "../../resource.js";
import { ignore } from "../../util/ignore.js";
import { logger } from "../../util/logger.js";
import { mergeTimeoutConfig, waitForResourceState, } from "../../util/timeout.js";
import { callEC2Api, createEC2Client } from "./utils.js";
/**
 * Wait for Route Table Association to reach a specific state
 */
async function waitForRouteTableAssociationState(client, routeTableId, associationId, targetState, timeoutConfig) {
    const checkFunction = async () => {
        const response = await callEC2Api(client, "DescribeRouteTables", parseRouteTableAssociationXmlResponse, convertDescribeRouteTablesParamsToAwsFormat({
            RouteTableIds: [routeTableId],
        }));
        const routeTable = response.RouteTables?.[0];
        if (!routeTable) {
            throw new Error(`Route table ${routeTableId} not found`);
        }
        const association = routeTable.Associations?.find((a) => a.RouteTableAssociationId === associationId);
        return association;
    };
    const isReady = (association) => {
        return association?.AssociationState?.State === targetState;
    };
    await waitForResourceState(checkFunction, isReady, timeoutConfig, associationId, "Route Table Association", `reach state ${targetState}`);
}
/**
 * Wait for Route Table Association to be deleted
 */
async function waitForRouteTableAssociationDeleted(client, routeTableId, associationId, timeoutConfig) {
    const checkFunction = async () => {
        try {
            const response = await callEC2Api(client, "DescribeRouteTables", parseRouteTableAssociationXmlResponse, convertDescribeRouteTablesParamsToAwsFormat({
                RouteTableIds: [routeTableId],
            }));
            const routeTable = response.RouteTables?.[0];
            if (!routeTable) {
                // Route table doesn't exist, so association is definitely gone
                return undefined;
            }
            const association = routeTable.Associations?.find((a) => a.RouteTableAssociationId === associationId);
            return association;
        }
        catch (error) {
            // If Route Table is not found, association is definitely gone
            if (error.code === "InvalidRouteTableID.NotFound" ||
                error.code === "InvalidRouteTable.NotFound") {
                return undefined;
            }
            throw error;
        }
    };
    const isReady = (association) => {
        // Association is deleted if it doesn't exist
        return !association;
    };
    await waitForResourceState(checkFunction, isReady, timeoutConfig, associationId, "Route Table Association", "be deleted");
}
/**
 * AWS Route Table Association Resource
 *
 * Creates and manages associations between route tables and subnets or gateways.
 * Each subnet can only be associated with one route table at a time. Route table
 * associations determine which route table controls the traffic for a subnet.
 *
 * @example
 * // Associate a subnet with a route table
 * const association = await RouteTableAssociation("subnet-association", {
 *   routeTable: myRouteTable,
 *   subnet: mySubnet
 * });
 *
 * @example
 * // Associate a subnet with a route table using IDs
 * const association = await RouteTableAssociation("subnet-association", {
 *   routeTable: "rtb-12345678",
 *   subnet: "subnet-12345678"
 * });
 *
 * @example
 * // Associate a gateway with a route table
 * const gatewayAssociation = await RouteTableAssociation("gateway-association", {
 *   routeTable: myRouteTable,
 *   gateway: "igw-12345678"
 * });
 *
 * @example
 * // Create association with custom timeout configuration
 * const customAssociation = await RouteTableAssociation("custom-association", {
 *   routeTable: myRouteTable,
 *   subnet: mySubnet,
 *   timeout: {
 *     maxAttempts: 60,
 *     delayMs: 2000
 *   }
 * });
 *
 * @example
 * // Associate multiple subnets with the same route table
 * const publicSubnets = [subnet1, subnet2, subnet3];
 * const associations = await Promise.all(
 *   publicSubnets.map((subnet, index) =>
 *     RouteTableAssociation(`public-association-${index}`, {
 *       routeTable: publicRouteTable,
 *       subnet: subnet
 *     })
 *   )
 * );
 *
 * @example
 * // Associate private subnets with a private route table
 * const privateAssociation1 = await RouteTableAssociation("private-association-1", {
 *   routeTable: privateRouteTable,
 *   subnet: privateSubnet1
 * });
 *
 * const privateAssociation2 = await RouteTableAssociation("private-association-2", {
 *   routeTable: privateRouteTable,
 *   subnet: privateSubnet2
 * });
 *
 * @example
 * // Handle association replacement scenarios
 * const newAssociation = await RouteTableAssociation("replaced-association", {
 *   routeTable: newRouteTable,
 *   subnet: existingSubnet // This will replace any existing association
 * });
 */
export const RouteTableAssociation = Resource("aws::RouteTableAssociation", async function (_id, props) {
    const client = await createEC2Client();
    const timeoutConfig = mergeTimeoutConfig(ROUTE_TABLE_ASSOCIATION_TIMEOUT, props.timeout);
    const routeTableId = typeof props.routeTable === "string"
        ? props.routeTable
        : props.routeTable.routeTableId;
    const subnetId = props.subnet
        ? typeof props.subnet === "string"
            ? props.subnet
            : props.subnet.subnetId
        : undefined;
    const gatewayId = props.gateway;
    // Validate that exactly one of subnet or gateway is specified
    if ((!subnetId && !gatewayId) || (subnetId && gatewayId)) {
        throw new Error("Exactly one of subnet or gateway must be specified for route table association");
    }
    if (this.phase === "delete") {
        if (this.output?.associationId) {
            logger.log(`🗑️ Deleting Route Table Association: ${this.output.associationId}`);
            await ignore("InvalidAssociationID.NotFound", async () => {
                const params = convertDisassociateRouteTableParamsToAwsFormat({
                    AssociationId: this.output.associationId,
                });
                await callEC2Api(client, "DisassociateRouteTable", parseRouteTableAssociationXmlResponse, params);
            });
            // Wait for association to be fully deleted
            await waitForRouteTableAssociationDeleted(client, routeTableId, this.output.associationId, timeoutConfig);
            logger.log(`  ✅ Route Table Association ${this.output.associationId} deletion completed`);
        }
        return this.destroy();
    }
    let associationId;
    let isMain = false;
    let state = "associated";
    if (this.phase === "update" && this.output?.associationId) {
        // For updates, we need to check if the route table or target has changed
        const currentRouteTableId = this.output.routeTableId;
        const currentSubnetId = this.output.subnetId;
        const currentGatewayId = this.output.gatewayId;
        if (currentRouteTableId !== routeTableId ||
            currentSubnetId !== subnetId ||
            currentGatewayId !== gatewayId) {
            // Route table or target has changed, we need to replace the association
            const response = await callEC2Api(client, "ReplaceRouteTableAssociation", parseRouteTableAssociationXmlResponse, {
                AssociationId: this.output.associationId,
                RouteTableId: routeTableId,
            });
            associationId = response.NewAssociationId;
            state = response.AssociationState?.State || "associated";
            // Wait for the new association to be active
            await waitForRouteTableAssociationState(client, routeTableId, associationId, "associated", timeoutConfig);
        }
        else {
            // No changes needed, keep existing association
            associationId = this.output.associationId;
            state = this.output.state;
        }
    }
    else {
        // Create new association
        const params = convertAssociateRouteTableParamsToAwsFormat({
            RouteTableId: routeTableId,
            SubnetId: subnetId,
            GatewayId: gatewayId,
        });
        const response = await callEC2Api(client, "AssociateRouteTable", parseRouteTableAssociationXmlResponse, params);
        associationId = response.AssociationId;
        state = response.AssociationState?.State || "associated";
        // Wait for association to be active
        await waitForRouteTableAssociationState(client, routeTableId, associationId, "associated", timeoutConfig);
    }
    // Get the current association details to check if it's main
    const describeResponse = await callEC2Api(client, "DescribeRouteTables", parseRouteTableAssociationXmlResponse, convertDescribeRouteTablesParamsToAwsFormat({
        RouteTableIds: [routeTableId],
    }));
    const routeTable = describeResponse.RouteTables?.[0];
    if (routeTable) {
        const association = routeTable.Associations?.find((a) => a.RouteTableAssociationId === associationId);
        if (association) {
            isMain = association.Main;
            state = association.AssociationState.State;
        }
    }
    return {
        associationId,
        routeTableId,
        subnetId,
        gatewayId,
        isMain,
        state,
        ...props,
        routeTable: routeTableId,
        subnet: subnetId,
        gateway: gatewayId,
    };
});
/**
 * Route Table Association timeout constants
 */
export const ROUTE_TABLE_ASSOCIATION_TIMEOUT = {
    maxAttempts: 30,
    delayMs: 1000, // 1 second - Route Table Associations are fast resources
};
/**
 * Helper functions to convert typed parameters to AWS API format
 */
function convertAssociateRouteTableParamsToAwsFormat(params) {
    const awsParams = {
        RouteTableId: params.RouteTableId,
    };
    if (params.SubnetId) {
        awsParams.SubnetId = params.SubnetId;
    }
    if (params.GatewayId) {
        awsParams.GatewayId = params.GatewayId;
    }
    if (params.DryRun !== undefined) {
        awsParams.DryRun = params.DryRun.toString();
    }
    return awsParams;
}
function convertDisassociateRouteTableParamsToAwsFormat(params) {
    const awsParams = {
        AssociationId: params.AssociationId,
    };
    if (params.DryRun !== undefined) {
        awsParams.DryRun = params.DryRun.toString();
    }
    return awsParams;
}
function convertDescribeRouteTablesParamsToAwsFormat(params) {
    const awsParams = {};
    if (params.RouteTableIds) {
        params.RouteTableIds.forEach((routeTableId, index) => {
            awsParams[`RouteTableId.${index + 1}`] = routeTableId;
        });
    }
    if (params.Filters) {
        params.Filters.forEach((filter, filterIndex) => {
            const filterNum = filterIndex + 1;
            awsParams[`Filter.${filterNum}.Name`] = filter.Name;
            filter.Values.forEach((value, valueIndex) => {
                awsParams[`Filter.${filterNum}.Value.${valueIndex + 1}`] = value;
            });
        });
    }
    if (params.DryRun !== undefined) {
        awsParams.DryRun = params.DryRun.toString();
    }
    if (params.NextToken) {
        awsParams.NextToken = params.NextToken;
    }
    if (params.MaxResults !== undefined) {
        awsParams.MaxResults = params.MaxResults.toString();
    }
    return awsParams;
}
/**
 * Parse XML responses specifically for Route Table Association operations
 */
function parseRouteTableAssociationXmlResponse(xmlText) {
    const parser = new XMLParser({
        ignoreAttributes: true,
        parseAttributeValue: true,
        parseTagValue: true,
        trimValues: true,
    });
    const parsed = parser.parse(xmlText);
    const result = {};
    // Parse AssociateRouteTableResponse
    if (parsed.AssociateRouteTableResponse) {
        const response = parsed.AssociateRouteTableResponse;
        result.AssociationId = response.associationId;
        if (response.associationState) {
            result.AssociationState = {
                State: response.associationState.state,
                StatusMessage: response.associationState.statusMessage,
            };
        }
    }
    // Parse DisassociateRouteTableResponse
    if (parsed.DisassociateRouteTableResponse) {
        const response = parsed.DisassociateRouteTableResponse;
        result.Return = response.return;
    }
    // Parse ReplaceRouteTableAssociationResponse
    if (parsed.ReplaceRouteTableAssociationResponse) {
        const response = parsed.ReplaceRouteTableAssociationResponse;
        result.NewAssociationId = response.newAssociationId;
        if (response.associationState) {
            result.AssociationState = {
                State: response.associationState.state,
                StatusMessage: response.associationState.statusMessage,
            };
        }
    }
    // Parse DescribeRouteTablesResponse
    if (parsed.DescribeRouteTablesResponse) {
        const response = parsed.DescribeRouteTablesResponse;
        const routeTableSet = response.routeTableSet;
        if (routeTableSet?.item) {
            const routeTables = Array.isArray(routeTableSet.item)
                ? routeTableSet.item
                : [routeTableSet.item];
            result.RouteTables = routeTables.map((routeTable) => {
                const associations = routeTable.associationSet?.item
                    ? Array.isArray(routeTable.associationSet.item)
                        ? routeTable.associationSet.item
                        : [routeTable.associationSet.item]
                    : [];
                return {
                    RouteTableId: routeTable.routeTableId,
                    VpcId: routeTable.vpcId,
                    Associations: associations.map((association) => {
                        return {
                            RouteTableAssociationId: association.routeTableAssociationId,
                            RouteTableId: association.routeTableId,
                            SubnetId: association.subnetId,
                            GatewayId: association.gatewayId,
                            Main: association.main,
                            AssociationState: {
                                State: association.associationState?.state ?? "associated",
                                StatusMessage: association.associationState?.statusMessage,
                            },
                        };
                    }),
                };
            });
        }
        else {
            result.RouteTables = [];
        }
    }
    return result;
}
//# sourceMappingURL=route-table-association.js.map