import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as eventsTargets from "aws-cdk-lib/aws-events-targets";
import { Queue } from "./Queue.js";
import { getFunctionRef, isCDKConstruct } from "./Construct.js";
import { Function as Fn, } from "./Function.js";
/////////////////////
// Construct
/////////////////////
/**
 * The `EventBus` construct is a higher level CDK construct that makes it easy to create an EventBridge Event Bus.
 *
 * @example
 *
 * ```js
 * import { EventBus } from "@serverless-stack/resources";
 *
 * new EventBus(stack, "Bus", {
 *   rules: {
 *     myRule: {
 *       pattern: { source: ["myevent"] },
 *       targets: {
 *         myTarget1: "src/function1.handler",
 *         myTarget2: "src/function2.handler"
 *       },
 *     },
 *   },
 * });
 * ```
 */
export class EventBus extends Construct {
    constructor(scope, id, props) {
        super(scope, props?.cdk?.id || id);
        this.id = id;
        this.props = props || {};
        this.cdk = {};
        this.rulesData = {};
        this.targetsData = {};
        this.permissionsAttachedForAllTargets = [];
        this.createEventBus();
        this.addRules(this, props?.rules || {});
    }
    /**
     * The ARN of the internally created `EventBus` instance.
     */
    get eventBusArn() {
        return this.cdk.eventBus.eventBusArn;
    }
    /**
     * The name of the internally created `EventBus` instance.
     */
    get eventBusName() {
        return this.cdk.eventBus.eventBusName;
    }
    /**
     * Add rules after the EventBus has been created.
     *
     * @example
     * ```js
     * bus.addRules(stack, {
     *   myRule2: {
     *     pattern: { source: ["myevent"] },
     *       targets: {
     *         myTarget3: "src/function3.handler"
     *         myTarget4: "src/function4.handler"
     *       },
     *   },
     * });
     * ```
     */
    addRules(scope, rules) {
        Object.entries(rules).forEach(([ruleKey, rule]) => this.addRule(scope, ruleKey, rule));
    }
    /**
     * Get a rule
     *
     * @example
     * ```js
     * bus.getRule("myRule");
     * ```
     */
    getRule(key) {
        return this.rulesData[key];
    }
    /**
     * Add permissions to all event targets in this EventBus.
     *
     * @example
     * ```js {10}
     * bus.attachPermissions(["s3"]);
     * ```
     */
    attachPermissions(permissions) {
        Object.values(this.targetsData).forEach((rule) => Object.values(rule)
            .filter((target) => target instanceof Fn)
            .forEach((target) => target.attachPermissions(permissions)));
        this.permissionsAttachedForAllTargets.push(permissions);
    }
    /**
     * Add permissions to a specific event bus rule target
     *
     * @example
     * ```js {10}
     * const bus = new EventBus(stack, "Bus", {
     *   rules: {
     *     myRule: {
     *       pattern: { source: ["myevent"] },
     *       targets: {
     *         myTarget1: "src/function1.handler"
     *         myTarget2: "src/function2.handler"
     *       },
     *     },
     *   },
     * });
     *
     * bus.attachPermissionsToTarget("myRule", 0, ["s3"]);
     * ```
     */
    attachPermissionsToTarget(ruleKey, targetName, permissions) {
        const rule = this.targetsData[ruleKey];
        if (!rule) {
            throw new Error(`Cannot find the rule "${ruleKey}" in the "${this.node.id}" EventBus.`);
        }
        const target = rule[targetName];
        if (!(target instanceof Fn)) {
            throw new Error(`Cannot attach permissions to the "${this.node.id}" EventBus target because it's not a Lambda function`);
        }
        target.attachPermissions(permissions);
    }
    getConstructMetadata() {
        return {
            type: "EventBus",
            data: {
                eventBusName: this.cdk.eventBus.eventBusName,
                rules: Object.entries(this.targetsData).map(([ruleName, rule]) => ({
                    key: ruleName,
                    targets: Object.values(rule).map(getFunctionRef),
                    targetNames: Object.keys(rule),
                })),
            },
        };
    }
    /** @internal */
    getFunctionBinding() {
        return {
            clientPackage: "event-bus",
            variables: {
                eventBusName: {
                    environment: this.eventBusName,
                    parameter: this.eventBusName,
                },
            },
            permissions: {
                "events:*": [this.eventBusArn],
            },
        };
    }
    createEventBus() {
        const app = this.node.root;
        const id = this.node.id;
        const { cdk } = this.props;
        if (isCDKConstruct(cdk?.eventBus)) {
            this.cdk.eventBus = cdk?.eventBus;
        }
        else {
            const ebProps = (cdk?.eventBus || {});
            this.cdk.eventBus = new events.EventBus(this, "EventBus", {
                // Note: Set default eventBusName only if eventSourceName is not configured.
                //       This is because both cannot be configured at the same time.
                eventBusName: ebProps.eventSourceName
                    ? undefined
                    : app.logicalPrefixedName(id),
                ...ebProps,
            });
        }
    }
    addRule(scope, ruleKey, rule) {
        // Validate input
        // @ts-expect-error "eventBus" is not a prop
        if (rule.cdk?.rule.eventBus) {
            throw new Error(`Cannot configure the "rule.cdk.rule.eventBus" in the "${this.node.id}" EventBus`);
        }
        // Validate rule not redefined
        if (this.targetsData[ruleKey]) {
            throw new Error(`A rule already exists for "${ruleKey}"`);
        }
        // Create Rule
        const root = this.node.root;
        const eventsRule = new events.Rule(scope, ruleKey, {
            ruleName: root.logicalPrefixedName(ruleKey),
            ...rule.cdk?.rule,
            eventPattern: rule.pattern
                ? { ...rule.pattern }
                : rule.cdk?.rule?.eventPattern,
            eventBus: this.cdk.eventBus,
            targets: [],
        });
        this.rulesData[ruleKey] = eventsRule;
        // Create Targets
        Object.entries(rule.targets || {}).forEach(([targetName, target]) => this.addTarget(scope, ruleKey, eventsRule, targetName, target));
    }
    addTarget(scope, ruleKey, eventsRule, targetName, target) {
        this.targetsData[ruleKey] = this.targetsData[ruleKey] || {};
        if (target instanceof Queue || target.queue) {
            target = target;
            this.addQueueTarget(scope, ruleKey, eventsRule, targetName, target);
        }
        else if (target.cdk?.function) {
            target = target;
            this.addCdkFunctionTarget(scope, ruleKey, eventsRule, targetName, target);
        }
        else {
            target = target;
            this.addFunctionTarget(scope, ruleKey, eventsRule, targetName, target);
        }
    }
    addQueueTarget(scope, ruleKey, eventsRule, targetName, target) {
        // Parse target props
        let targetProps;
        let queue;
        if (target instanceof Queue) {
            target = target;
            queue = target;
        }
        else {
            target = target;
            targetProps = target.cdk?.target;
            queue = target.queue;
        }
        this.targetsData[ruleKey][targetName] = queue;
        // Create target
        eventsRule.addTarget(new eventsTargets.SqsQueue(queue.cdk.queue, targetProps));
    }
    addCdkFunctionTarget(scope, ruleKey, eventsRule, targetName, target) {
        // Parse target props
        const targetProps = target.cdk.target;
        const fn = target.cdk.function;
        this.targetsData[ruleKey][targetName] = fn;
        // Create target
        eventsRule.addTarget(new eventsTargets.LambdaFunction(fn, targetProps));
    }
    addFunctionTarget(scope, ruleKey, eventsRule, targetName, target) {
        // Parse target props
        let targetProps;
        let functionDefinition;
        if (target.function) {
            target = target;
            targetProps = target.cdk?.target;
            functionDefinition = target.function;
        }
        else {
            target = target;
            functionDefinition = target;
        }
        // Create function
        const fn = Fn.fromDefinition(scope, `Target_${this.node.id}_${ruleKey}_${targetName}`, functionDefinition, this.props.defaults?.function, `The "defaults.function" cannot be applied if an instance of a Function construct is passed in. Make sure to define all the targets using FunctionProps, so the EventBus construct can apply the "defaults.function" to them.`);
        this.targetsData[ruleKey][targetName] = fn;
        // Create target
        eventsRule.addTarget(new eventsTargets.LambdaFunction(fn, targetProps));
        // Attach existing permissions
        this.permissionsAttachedForAllTargets.forEach((permissions) => fn.attachPermissions(permissions));
    }
}
