using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Entities.Guild;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
using SVSim.Database.Services;
using SVSim.Database.Services.Guild;
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild;
using SVSim.EmulatedEntrypoint.Models.Dtos.Guild;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
namespace SVSim.EmulatedEntrypoint.Controllers;
/// /guild/* — 22 endpoints. See docs/api-spec/endpoints/post-login/guild-*.md.
[Route("guild")]
public sealed class GuildController : SVSimController
{
private readonly IGuildService _guild;
private readonly IGameConfigService _configs;
private readonly SVSimDbContext _db;
// Wire error code returned when guild operations fail (non-1 result_code).
private const int GuildErrorResultCode = 2;
public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db)
{
_guild = guild;
_configs = configs;
_db = db;
}
[HttpPost("info")]
public async Task> Info([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var cfg = _configs.Get();
var view = await _guild.GetMyGuildAsync(viewerId, ct);
var resp = new GuildInfoResponse
{
MaxMemberNum = cfg.MaxMemberNum,
MaxSubLeaderNum = cfg.MaxSubLeaderNum,
UsableStampList = cfg.UsableStampList.ConvertAll(i => i.ToString()),
};
if (view is null)
{
resp.GuildStatus = 0; // NOT_JOINING
}
else
{
resp.GuildStatus = 2; // JOINING
resp.JoinRequestCount = view.JoinRequestCount;
resp.InviteCount = view.InviteCount;
var memberDtos = await ToMemberDtoListAsync(view.Members, viewerId, ct);
var leaderName = memberDtos.FirstOrDefault(m => m.ViewerId == view.Guild.LeaderViewerId)?.Name ?? "";
resp.Guild = new GuildBundle
{
Detail = ToDetailDto(view.Guild, view.Members.Count, leaderName),
Members = memberDtos,
};
}
return resp;
}
// ===== 21 remaining stubs — each returns the response DTO with defaults =====
[HttpPost("create")]
public async Task> Create([FromBody] GuildCreateRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var res = await _guild.CreateAsync(viewerId, new(req.GuildName, req.Activity, req.JoinCondition), ct);
if (res.IsOk) return new EmptyResponse();
return MapErrorToWire(res);
}
[HttpPost("breakup")]
public async Task> Breakup([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var v)) return Unauthorized();
var r = await _guild.BreakupAsync(v, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("update")]
public async Task> Update([FromBody] GuildUpdateRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
int? activity = req.Activity != 0 ? req.Activity : null;
int? joinCondition = req.JoinCondition != 0 ? req.JoinCondition : null;
string? name = string.IsNullOrWhiteSpace(req.GuildName) ? null : req.GuildName;
var r = await _guild.UpdateAsync(viewerId, new(activity, joinCondition, name), ct);
if (!r.IsOk) return WireError();
var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct);
if (m is null) return WireError();
var guild = await _guild.GetActiveAsync(m.GuildId, ct);
if (guild is null) return WireError();
var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct);
var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct);
// GuildUpdateTask.Parse() reads data["guild"] directly as GuildDetailInfo — flat, no "detail" wrapper.
return new GuildUpdateResponse { Guild = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") };
}
[HttpPost("update_description")]
public async Task> UpdateDescription([FromBody] GuildUpdateDescriptionRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.UpdateDescriptionAsync(viewerId, req.Description, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("update_emblem")]
public async Task> UpdateEmblem([FromBody] GuildUpdateEmblemRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.UpdateEmblemAsync(viewerId, req.EmblemId, ct);
if (!r.IsOk) return WireError();
var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct);
if (m is null) return WireError();
var guild = await _guild.GetActiveAsync(m.GuildId, ct);
if (guild is null) return WireError();
var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct);
var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct);
// GuildEmblemUpdateTask.Parse() reads data["guild"]["detail"] — nested wrapper required.
return new GuildUpdateEmblemResponse
{
Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") }
};
}
[HttpPost("search_guild")]
public Task> SearchGuild([FromBody] GuildSearchGuildRequest req, CancellationToken ct)
=> Task.FromResult>(new GuildSearchGuildResponse { List = new() });
[HttpPost("emblem_list")]
public async Task> EmblemList([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var viewer = await _db.Viewers
.AsNoTracking()
.Include(v => v.Emblems)
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
var emblems = viewer?.Emblems ?? new();
return new GuildEmblemListResponse
{
EmblemList = emblems.Select(e => new GuildEmblemEntry { EmblemId = e.Id }).ToList()
};
}
[HttpPost("others_info")]
public async Task> OthersInfo([FromBody] GuildOthersInfoRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out _)) return Unauthorized();
var guild = await _guild.GetActiveAsync(req.GuildId, ct);
if (guild is null) return new GuildOthersInfoResponse();
var memberCount = await _db.GuildMembers.CountAsync(m => m.GuildId == guild.GuildId, ct);
var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct);
return new GuildOthersInfoResponse
{
Guild = new GuildDetailSubTree
{
Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "")
}
};
}
[HttpPost("friend_list")]
public Task> FriendList([FromBody] BaseRequest _, CancellationToken ct)
=> Task.FromResult>(new GuildFriendListResponse { Friends = new() });
[HttpPost("invite_user_list")]
public Task> InviteUserList([FromBody] BaseRequest _, CancellationToken ct)
=> Task.FromResult>(new GuildInviteUserListResponse { Users = new() });
[HttpPost("invited_guild_list")]
public Task> InvitedGuildList([FromBody] GuildInvitedGuildListRequest req, CancellationToken ct)
=> Task.FromResult>(new GuildInvitedGuildListResponse { List = new() });
[HttpPost("invite")]
public Task> Invite([FromBody] GuildInviteRequest req, CancellationToken ct) => Stub();
[HttpPost("cancel_invite")]
public Task> CancelInvite([FromBody] GuildCancelInviteRequest req, CancellationToken ct) => Stub();
[HttpPost("reject_invite")]
public Task> RejectInvite([FromBody] GuildRejectInviteRequest req, CancellationToken ct) => Stub();
[HttpPost("join")]
public Task> Join([FromBody] GuildJoinEndpointRequest req, CancellationToken ct)
=> Task.FromResult>(new GuildJoinResponse { GuildStatus = 0 });
[HttpPost("cancel_join_request")]
public Task> CancelJoinRequest([FromBody] GuildCancelJoinRequestRequest req, CancellationToken ct) => Stub();
[HttpPost("join_request_list")]
public Task> JoinRequestList([FromBody] GuildJoinRequestListRequest req, CancellationToken ct)
=> Task.FromResult>(new GuildJoinRequestListResponse { Users = new() });
[HttpPost("join_request_accept")]
public Task> JoinRequestAccept([FromBody] GuildJoinRequestAcceptRequest req, CancellationToken ct) => Stub();
[HttpPost("reject_join_request")]
public Task> RejectJoinRequest([FromBody] GuildRejectJoinRequestRequest req, CancellationToken ct) => Stub();
[HttpPost("leave")]
public Task> Leave([FromBody] BaseRequest _, CancellationToken ct) => Stub();
[HttpPost("remove")]
public Task> Remove([FromBody] GuildRemoveRequest req, CancellationToken ct) => Stub();
[HttpPost("change_role")]
public Task> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
=> Task.FromResult>(new GuildChangeRoleResponse { Members = new() });
// ===== Private helpers =====
private static GuildDetailDto ToDetailDto(SVSim.Database.Entities.Guild.Guild guild, int memberCount, string leaderName = "") => new()
{
GuildId = guild.GuildId,
GuildName = guild.Name,
Description = guild.Description,
GuildEmblemId = guild.EmblemId,
JoinCondition = (int)guild.JoinCondition,
Activity = (int)guild.Activity,
MemberNum = memberCount,
LeaderViewerId = guild.LeaderViewerId,
LeaderName = leaderName,
};
private async Task> ToMemberDtoListAsync(
IReadOnlyList members,
long callerViewerId,
CancellationToken ct)
{
if (members.Count == 0) return new();
var viewerIds = members.Select(m => m.ViewerId).ToList();
// Batch-load viewer rows with Info + SelectedEmblem + SelectedDegree.
var viewers = await _db.Viewers
.AsNoTracking()
.Include(v => v.Info.SelectedEmblem)
.Include(v => v.Info.SelectedDegree)
.Where(v => viewerIds.Contains(v.Id))
.ToDictionaryAsync(v => v.Id, ct);
var result = new List(members.Count);
foreach (var m in members)
{
viewers.TryGetValue(m.ViewerId, out var v);
result.Add(ToMemberDto(m, v));
}
return result;
}
private static GuildMemberInfoDto ToMemberDto(GuildMember member, Viewer? viewer) => new()
{
ViewerId = member.ViewerId,
Name = viewer?.DisplayName ?? "",
EmblemId = viewer?.Info?.SelectedEmblem?.Id is > 0 ? viewer.Info.SelectedEmblem.Id : 100_000_000L,
CountryCode = viewer?.Info?.CountryCode ?? "",
Rank = 1, // TODO: populate from actual rank data when rank tracking lands
DegreeId = viewer?.Info?.SelectedDegree?.Id ?? 0,
IsOfficialMarkDisplayed = viewer?.Info?.IsOfficialMarkDisplayed == true ? 1 : 0,
Role = (int)member.Role,
};
///
/// Maps a failed GuildOpResult to the wire error envelope convention used throughout the
/// codebase (see CampaignController.Fail(), AchievementController, MissionController).
/// Returns HTTP 200 with a JSON body carrying result_code: 2.
///
private ActionResult MapErrorToWire(GuildOpResult res)
=> Ok(new { result_code = GuildErrorResultCode });
///
/// Returns a wire-error for endpoints that return a non-EmptyResponse type.
/// Identical result_code=2 body, but typed as ObjectResult so it fits any TResult.
///
private ObjectResult WireError() => Ok(new { result_code = GuildErrorResultCode });
private static Task> Stub() =>
Task.FromResult>(new EmptyResponse());
}