/**
 * Group expansion — turns a recipient string with group names into a flat
 * deduplicated address list. Shared between desktop (mailx-service) and
 * Android (mailx-store-web) so send paths produce the same result.
 *
 * Group source: contacts.jsonc → groups: Record<string, string[]>. Each
 * value is an array of either email addresses ("a@b" or "Name <a@b>") or
 * names of other groups (recursive aliasing). Cycles are detected; depth
 * is capped at 10 nested levels.
 */

/** Raw recipient string from a To/Cc/Bcc field; may contain group names. */
export type RecipientToken = string;

/** Map of group name → member list (each entry: address or another group name). */
export type GroupMap = Record<string, string[]>;

/** Result of expanding a recipient string. */
export interface ExpansionResult {
    /** Flat unique address list (preserving order of first occurrence). */
    addresses: string[];
    /** Group names that couldn't be resolved (typo / cycle) — surface in UI. */
    unresolved: string[];
}

/** Split a comma-or-semicolon recipient string into tokens. Respects
 *  angle-bracket address blocks ("Name, Suffix <a@b>") so a comma inside
 *  the display name doesn't split the entry. */
export function splitRecipients(raw: string): RecipientToken[] {
    const out: string[] = [];
    let buf = "";
    let depth = 0;
    for (let i = 0; i < raw.length; i++) {
        const c = raw[i];
        if (c === "<") depth++;
        else if (c === ">") depth = Math.max(0, depth - 1);
        if ((c === "," || c === ";") && depth === 0) {
            const t = buf.trim();
            if (t) out.push(t);
            buf = "";
        } else {
            buf += c;
        }
    }
    const t = buf.trim();
    if (t) out.push(t);
    return out;
}

/** Quick test: does the token look like an email address (with or without
 *  a display name)? Anything containing "@" with non-whitespace on both
 *  sides counts. */
export function isAddressToken(token: string): boolean {
    return /^[^@\s][^@]*@[^@\s]+(\s|$)|<[^>]+@[^>]+>/.test(token) || /^[^\s<>@]+@[^\s<>@]+$/.test(token);
}

/** Extract the bare email address from a token. Returns the original token
 *  lowercased if it has no angle-bracket form. */
export function extractAddress(token: string): string {
    const m = token.match(/<([^>]+)>/);
    if (m) return m[1].trim().toLowerCase();
    return token.trim().toLowerCase();
}

/** Expand a recipient string by resolving group names against `groups`.
 *  Group names take precedence: if a token matches a group name AND looks
 *  like an address, it's treated as a group. (In practice no one names a
 *  group "x@y.com", so this is rarely a real conflict.) */
export function expandRecipients(raw: string, groups: GroupMap): ExpansionResult {
    const tokens = splitRecipients(raw);
    const seen = new Set<string>();
    const addresses: string[] = [];
    const unresolved: string[] = [];

    const visit = (token: string, depth: number, visited: Set<string>): void => {
        if (depth > 10) {
            unresolved.push(`${token} (depth limit)`);
            return;
        }
        // Group name match — case-insensitive
        const groupKey = Object.keys(groups).find(k => k.toLowerCase() === token.trim().toLowerCase());
        if (groupKey) {
            if (visited.has(groupKey.toLowerCase())) {
                unresolved.push(`${groupKey} (cycle)`);
                return;
            }
            const next = new Set(visited);
            next.add(groupKey.toLowerCase());
            for (const member of groups[groupKey] || []) {
                visit(member, depth + 1, next);
            }
            return;
        }
        // Address — keep the original (with display name if present), but
        // dedupe by lowercased bare address.
        if (isAddressToken(token)) {
            const key = extractAddress(token);
            if (!seen.has(key)) {
                seen.add(key);
                addresses.push(token.trim());
            }
            return;
        }
        // Neither group nor address — flag.
        unresolved.push(token);
    };

    for (const t of tokens) {
        visit(t, 0, new Set());
    }
    return { addresses, unresolved };
}
