namespace WxAgent.Core; public sealed record WechatDatabaseContact(string Username, string? NickName, string? Remark, string? AvatarUrl) { public string DisplayName => !string.IsNullOrWhiteSpace(Remark) ? Remark : !string.IsNullOrWhiteSpace(NickName) ? NickName : Username; } public sealed record WechatContactPage(IReadOnlyList Contacts, int? NextOffset) { public bool HasMore => NextOffset is not null; } public static class WechatContactQuery { // Stable IDs, not display names, define ordering and identity. No UI fallback. public const string Sql = """ SELECT username, nick_name, remark, small_head_url FROM contact WHERE ($contains IS NULL OR instr(username, $contains) > 0 OR instr(nick_name, $contains) > 0 OR instr(remark, $contains) > 0) AND ($groups IS NULL OR (substr(username, -9) = '@chatroom') = $groups) ORDER BY username COLLATE BINARY LIMIT $limit OFFSET $offset; """; public static IReadOnlyList> Parameters(int limit, int offset, string? contains, bool? groupsOnly) { if (limit is < 1 or > 10000 || offset < 0 || offset > int.MaxValue - limit) throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Contact limit must be 1-10000 and offset must leave room for the next page."); return new KeyValuePair[] { new("$limit", limit + 1), new("$offset", offset), new("$contains", contains), new("$groups", groupsOnly is null ? null : groupsOnly.Value ? 1 : 0) }; } public static WechatContactPage Page(IReadOnlyList rows, int limit, int offset) { _ = Parameters(limit, offset, null, null); return new(rows.Take(limit).ToArray(), rows.Count > limit ? offset + limit : null); } }