import { createTypeSpecLibrary, paramMessage, createDiagnosticCollector, navigateType } from '@typespec/compiler';
import { useStateSet, useStateMap } from '@typespec/compiler/utils';

const $lib = createTypeSpecLibrary({
    name: "@typespec/events",
    diagnostics: {
        "invalid-content-type-target": {
            severity: "error",
            messages: {
                default: `@contentType can only be specified on the top-level event envelope, or the event payload marked with @data`,
            },
        },
        "multiple-event-payloads": {
            severity: "error",
            messages: {
                default: paramMessage `Event payload already applied to ${"dataPath"} but also exists under ${"currentPath"}`,
                payloadInIndexedModel: paramMessage `Event payload applied from inside a Record or Array at ${"dataPath"}`,
            },
        },
    },
    state: {
        events: { description: "State for the @events decorator." },
        contentType: { description: "State for the @contentType decorator." },
        data: { description: "State for the @data decorator." },
    },
});
const { reportDiagnostic, createDiagnostic, stateKeys: EventsStateKeys } = $lib;

const [isEvents, setEvents] = useStateSet(EventsStateKeys.events);
const $eventsDecorator = (context, target) => {
    setEvents(context.program, target);
};
const [getContentType, setContentType] = useStateMap(EventsStateKeys.contentType);
const $contentTypeDecorator = (context, target, contentType) => {
    setContentType(context.program, target, contentType);
};
const [isEventData, setEventData] = useStateSet(EventsStateKeys.data);
const $dataDecorator = (context, target) => {
    setEventData(context.program, target);
};

function validateContentType(program, target) {
    const contentType = getContentType(program, target);
    if (!contentType || isEventData(program, target))
        return;
    return createDiagnostic({
        code: "invalid-content-type-target",
        target,
    });
}
function stringifyPropertyPath(path) {
    return path
        .map((p) => {
        switch (p.kind) {
            case "ModelProperty":
                return p.name;
            case "Tuple":
                return "[]";
            case "Model":
                return;
        }
    })
        .filter(Boolean)
        .join(".");
}
function getEventDefinition(program, target) {
    const diagnostics = createDiagnosticCollector();
    // If the variant has a name, that may be used for out-of-band event types.
    const eventType = typeof target.name === "string" ? target.name : undefined;
    const envelopeContentType = getContentType(program, target);
    let payloadType;
    let payloadContentType;
    let pathToPayload = "";
    // Keeps track of if we're in a Record or Array.
    let indexerDepth = 0;
    // Keeps track of the path from the target to the current model property.
    const currentPropertyPath = [];
    // Keeps track of the types that have a descendant that leads to the event payload.
    const typesThatChainToPayload = new Set();
    // Look for a model property that is decorated with `@data`.
    // This property will be used as the event payload.
    // It can only be applied once within an event, and if it is not found,
    // the target type is the payload.
    navigateType(target.type, {
        modelProperty(prop) {
            currentPropertyPath.push(prop);
            const contentTypeDiagnostic = validateContentType(program, prop);
            if (contentTypeDiagnostic) {
                diagnostics.add(contentTypeDiagnostic);
            }
            // This detects the scenario where a model that contains a `@data` property
            // is referenced multiple times directly or indirectly by the target event.
            if (typesThatChainToPayload.has(prop.type)) {
                diagnostics.add(createDiagnostic({
                    code: "multiple-event-payloads",
                    format: {
                        dataPath: pathToPayload,
                        currentPath: stringifyPropertyPath(currentPropertyPath),
                    },
                    target,
                }));
                return;
            }
            // Haven't found a payload property yet, move on.
            if (!isEventData(program, prop))
                return;
            // Mark all the types in the current path as having a descendant that leads to the payload.
            currentPropertyPath.forEach((p) => {
                if (p.kind === "ModelProperty") {
                    typesThatChainToPayload.add(p.type);
                }
                else if (p.kind === "Model" || p.kind === "Tuple") {
                    typesThatChainToPayload.add(p);
                }
            });
            // Found the payload property but it's referenced indirectly by
            // a Record or Array, which implies there would be more than 1 payload.
            if (indexerDepth > 0) {
                diagnostics.add(createDiagnostic({
                    code: "multiple-event-payloads",
                    messageId: "payloadInIndexedModel",
                    format: {
                        dataPath: stringifyPropertyPath(currentPropertyPath.slice(0, -1)),
                    },
                    target,
                }));
                return;
            }
            // A payload type was previously found, but only one is allowed.
            if (payloadType) {
                diagnostics.add(createDiagnostic({
                    code: "multiple-event-payloads",
                    format: {
                        dataPath: pathToPayload,
                        currentPath: stringifyPropertyPath(currentPropertyPath),
                    },
                    target,
                }));
                return;
            }
            // A payload property was found.
            payloadType = prop.type;
            payloadContentType = getContentType(program, prop);
            pathToPayload = stringifyPropertyPath(currentPropertyPath);
        },
        exitModelProperty() {
            currentPropertyPath.pop();
        },
        tuple(tuple) {
            currentPropertyPath.push(tuple);
            // Check if any of the tuple values have already been marked as a payload/payload ancestor.
            // They won't be visited again, so need to create diagnostic here.
            tuple.values.forEach((value) => {
                if (typesThatChainToPayload.has(value)) {
                    diagnostics.add(createDiagnostic({
                        code: "multiple-event-payloads",
                        format: {
                            dataPath: pathToPayload,
                            currentPath: stringifyPropertyPath(currentPropertyPath),
                        },
                        target,
                    }));
                    return;
                }
            });
        },
        exitTuple() {
            currentPropertyPath.pop();
        },
        model(model) {
            if (model.indexer) {
                indexerDepth++;
            }
            currentPropertyPath.push(model);
        },
        exitModel(model) {
            if (model.indexer) {
                indexerDepth--;
            }
            currentPropertyPath.pop();
        },
    }, {});
    const eventDefinition = {
        eventType,
        root: target,
        isEventEnvelope: !!payloadType,
        type: target.type,
        contentType: envelopeContentType,
        payloadType: payloadType ?? target.type,
        payloadContentType: payloadType ? payloadContentType : envelopeContentType,
    };
    return diagnostics.wrap(eventDefinition);
}
function getEventDefinitions(program, target) {
    const diagnostics = createDiagnosticCollector();
    // 1. Iterate over each variant of the action
    const events = [];
    target.variants.forEach((variant) => {
        events.push(diagnostics.pipe(getEventDefinition(program, variant)));
    });
    return diagnostics.wrap(events);
}

export { $contentTypeDecorator as $, EventsStateKeys as E, $dataDecorator as a, $eventsDecorator as b, $lib as c, getContentType as d, isEvents as e, getEventDefinitions as g, isEventData as i };
