export type RoleMappingMode = "fixedRoles" | "mappingBuilder" | "rawExpression";

export type MappingBuilderRule = {
    /** Stable React list key; not used when compiling JMESPath. */
    id?: string;
    matchValue: string;
    roleNames: string[];
};

function newMappingBuilderRuleId(): string {
    if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
        return crypto.randomUUID();
    }
    return `rule-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}

export function createMappingBuilderRule(): MappingBuilderRule {
    return {
        id: newMappingBuilderRuleId(),
        matchValue: "",
        roleNames: []
    };
}

/** Ensures every rule has a stable id (e.g. after loading from the API). */
export function ensureMappingBuilderRuleIds(
    rules: MappingBuilderRule[]
): MappingBuilderRule[] {
    return rules.map((rule) =>
        rule.id ? rule : { ...rule, id: newMappingBuilderRuleId() }
    );
}

export type MappingBuilderConfig = {
    claimPath: string;
    rules: MappingBuilderRule[];
};

export type RoleMappingConfig = {
    mode: RoleMappingMode;
    fixedRoleNames: string[];
    mappingBuilder: MappingBuilderConfig;
    rawExpression: string;
};

const SINGLE_QUOTED_ROLE_REGEX = /^'([^']+)'$/;
const QUOTED_ROLE_ARRAY_REGEX = /^\[(.*)\]$/;

/** Stored role mappings created by the mapping builder are prefixed so the UI can restore the builder. */
export const PANGOLIN_ROLE_MAP_BUILDER_PREFIX = "__PANGOLIN_ROLE_MAP_BUILDER_V1__";

const BUILDER_METADATA_SEPARATOR = "\n---\n";

export type UnwrappedRoleMapping = {
    /** Expression passed to JMESPath (no builder wrapper). */
    evaluationExpression: string;
    /** Present when the stored value was saved from the mapping builder. */
    builderState: { claimPath: string; rules: MappingBuilderRule[] } | null;
};

/**
 * Split stored DB value into evaluation expression and optional builder metadata.
 * Legacy values (no prefix) are returned as-is for evaluation.
 */
export function unwrapRoleMapping(
    stored: string | null | undefined
): UnwrappedRoleMapping {
    const trimmed = stored?.trim() ?? "";
    if (!trimmed.startsWith(PANGOLIN_ROLE_MAP_BUILDER_PREFIX)) {
        return {
            evaluationExpression: trimmed,
            builderState: null
        };
    }

    let rest = trimmed.slice(PANGOLIN_ROLE_MAP_BUILDER_PREFIX.length);
    if (rest.startsWith("\n")) {
        rest = rest.slice(1);
    }

    const sepIdx = rest.indexOf(BUILDER_METADATA_SEPARATOR);
    if (sepIdx === -1) {
        return {
            evaluationExpression: trimmed,
            builderState: null
        };
    }

    const jsonPart = rest.slice(0, sepIdx).trim();
    const inner = rest.slice(sepIdx + BUILDER_METADATA_SEPARATOR.length).trim();

    try {
        const meta = JSON.parse(jsonPart) as {
            claimPath?: unknown;
            rules?: unknown;
        };
        if (
            typeof meta.claimPath === "string" &&
            Array.isArray(meta.rules)
        ) {
            const rules: MappingBuilderRule[] = meta.rules.map(
                (r: unknown) => {
                    const row = r as {
                        matchValue?: unknown;
                        roleNames?: unknown;
                    };
                    return {
                        matchValue:
                            typeof row.matchValue === "string"
                                ? row.matchValue
                                : "",
                        roleNames: Array.isArray(row.roleNames)
                            ? row.roleNames.filter(
                                  (n): n is string => typeof n === "string"
                              )
                            : []
                    };
                }
            );
            return {
                evaluationExpression: inner,
                builderState: {
                    claimPath: meta.claimPath,
                    rules: ensureMappingBuilderRuleIds(rules)
                }
            };
        }
    } catch {
        /* fall through */
    }

    return {
        evaluationExpression: inner.length ? inner : trimmed,
        builderState: null
    };
}

function escapeSingleQuotes(value: string): string {
    return value.replace(/'/g, "\\'");
}

export function compileRoleMappingExpression(config: RoleMappingConfig): string {
    if (config.mode === "rawExpression") {
        return config.rawExpression.trim();
    }

    if (config.mode === "fixedRoles") {
        const roleNames = dedupeNonEmpty(config.fixedRoleNames);
        if (!roleNames.length) {
            return "";
        }

        if (roleNames.length === 1) {
            return `'${escapeSingleQuotes(roleNames[0])}'`;
        }

        return `[${roleNames.map((name) => `'${escapeSingleQuotes(name)}'`).join(", ")}]`;
    }

    const claimPath = config.mappingBuilder.claimPath.trim();
    const rules = config.mappingBuilder.rules
        .map((rule) => ({
            matchValue: rule.matchValue.trim(),
            roleNames: dedupeNonEmpty(rule.roleNames)
        }))
        .filter((rule) => Boolean(rule.matchValue) && rule.roleNames.length > 0);

    if (!claimPath || !rules.length) {
        return "";
    }

    const compiledRules = rules.map((rule) => {
        const mappedRoles = `[${rule.roleNames
            .map((name) => `'${escapeSingleQuotes(name)}'`)
            .join(", ")}]`;
        return `contains(${claimPath}, '${escapeSingleQuotes(rule.matchValue)}') && ${mappedRoles} || []`;
    });

    const inner = `[${compiledRules.join(", ")}][]`;
    const metadata = {
        claimPath,
        rules: rules.map((r) => ({
            matchValue: r.matchValue,
            roleNames: r.roleNames
        }))
    };

    return `${PANGOLIN_ROLE_MAP_BUILDER_PREFIX}\n${JSON.stringify(metadata)}${BUILDER_METADATA_SEPARATOR}${inner}`;
}

export function detectRoleMappingConfig(
    expression: string | null | undefined
): RoleMappingConfig {
    const stored = expression?.trim() || "";

    if (!stored) {
        return defaultRoleMappingConfig();
    }

    const { evaluationExpression, builderState } = unwrapRoleMapping(stored);

    if (builderState) {
        return {
            mode: "mappingBuilder",
            fixedRoleNames: [],
            mappingBuilder: {
                claimPath: builderState.claimPath,
                rules: builderState.rules
            },
            rawExpression: evaluationExpression
        };
    }

    const tail = evaluationExpression.trim();

    const singleMatch = tail.match(SINGLE_QUOTED_ROLE_REGEX);
    if (singleMatch?.[1]) {
        return {
            mode: "fixedRoles",
            fixedRoleNames: [singleMatch[1]],
            mappingBuilder: defaultRoleMappingConfig().mappingBuilder,
            rawExpression: tail
        };
    }

    const arrayMatch = tail.match(QUOTED_ROLE_ARRAY_REGEX);
    if (arrayMatch?.[1]) {
        const roleNames = arrayMatch[1]
            .split(",")
            .map((entry) => entry.trim())
            .map((entry) => entry.match(SINGLE_QUOTED_ROLE_REGEX)?.[1] || "")
            .filter(Boolean);

        if (roleNames.length > 0) {
            return {
                mode: "fixedRoles",
                fixedRoleNames: roleNames,
                mappingBuilder: defaultRoleMappingConfig().mappingBuilder,
                rawExpression: tail
            };
        }
    }

    return {
        mode: "rawExpression",
        fixedRoleNames: [],
        mappingBuilder: defaultRoleMappingConfig().mappingBuilder,
        rawExpression: tail
    };
}

export function defaultRoleMappingConfig(): RoleMappingConfig {
    return {
        mode: "fixedRoles",
        fixedRoleNames: [],
        mappingBuilder: {
            claimPath: "groups",
            rules: [createMappingBuilderRule()]
        },
        rawExpression: ""
    };
}

function dedupeNonEmpty(values: string[]): string[] {
    return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
}
