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.Friend;
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;

/// <summary>/guild/* — 22 endpoints. See docs/api-spec/endpoints/post-login/guild-*.md.</summary>
[Route("guild")]
public sealed class GuildController : SVSimController
{
    private readonly IGuildService _guild;
    private readonly IGameConfigService _configs;
    private readonly SVSimDbContext _db;
    private readonly IFriendService _friends;

    // 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, IFriendService friends)
    {
        _guild = guild;
        _configs = configs;
        _db = db;
        _friends = friends;
    }

    [HttpPost("info")]
    public async Task<ActionResult<GuildInfoResponse>> Info([FromBody] BaseRequest _, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var cfg = _configs.Get<GuildConfig>();
        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<ActionResult<EmptyResponse>> 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<ActionResult<EmptyResponse>> 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<ActionResult<GuildUpdateResponse>> 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<ActionResult<EmptyResponse>> 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<ActionResult<GuildUpdateEmblemResponse>> 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 async Task<ActionResult<GuildSearchGuildResponse>> SearchGuild([FromBody] GuildSearchGuildRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var v)) return Unauthorized();
        var entries = await _guild.SearchAsync(req.GuildName ?? "", req.Activity, req.JoinCondition, req.MemberConditionRange, ct);
        return new GuildSearchGuildResponse { List = entries.Select(e => ToDetailDto(e.Guild, e.MemberNum, e.LeaderName)).ToList() };
    }

    [HttpPost("emblem_list")]
    public async Task<ActionResult<GuildEmblemListResponse>> 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<ActionResult<GuildOthersInfoResponse>> 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 async Task<ActionResult<GuildFriendListResponse>> FriendList([FromBody] BaseRequest _, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();

        var friendInfo = await _friends.GetFriendsAsync(viewerId, ct);
        if (friendInfo.Friends.Count == 0)
            return new GuildFriendListResponse { Friends = new() };

        // For each friend, determine if they are already in a guild (is_join_guild).
        var friendViewerIds = friendInfo.Friends.Select(f => (long)f.ViewerId).ToList();
        var viewerRows = await _db.Viewers
            .AsNoTracking()
            .Where(v => friendViewerIds.Contains(v.Id))
            .Select(v => new { v.Id, v.GuildId })
            .ToDictionaryAsync(v => v.Id, v => v.GuildId, ct);

        var candidates = new List<GuildInviteCandidateDto>(friendInfo.Friends.Count);
        foreach (var f in friendInfo.Friends)
        {
            viewerRows.TryGetValue(f.ViewerId, out var guildId);
            candidates.Add(new GuildInviteCandidateDto
            {
                ViewerId    = f.ViewerId,
                Name        = f.Name,
                EmblemId    = f.EmblemId,
                CountryCode = f.CountryCode,
                Rank        = f.Rank,
                DegreeId    = f.DegreeId,
                IsJoinGuild = guildId is not null,
            });
        }

        return new GuildFriendListResponse { Friends = candidates };
    }

    [HttpPost("invite_user_list")]
    public async Task<ActionResult<GuildInviteUserListResponse>> InviteUserList([FromBody] BaseRequest _, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var entries = await _guild.ListOutgoingInvitesAsync(viewerId, ct);
        return new GuildInviteUserListResponse
        {
            Users = entries.Select(e => new GuildOutgoingInviteDto
            {
                ViewerId = e.InviteeViewerId,
                Name = e.Name,
                EmblemId = e.EmblemId,
                CountryCode = e.CountryCode,
                Rank = e.Rank,
                DegreeId = e.DegreeId,
                InviteId = e.InviteId,
                InviteTime = new DateTimeOffset(e.CreatedAt, TimeSpan.Zero).ToUnixTimeSeconds(),
            }).ToList(),
        };
    }

    [HttpPost("invited_guild_list")]
    public async Task<ActionResult<GuildInvitedGuildListResponse>> InvitedGuildList([FromBody] GuildInvitedGuildListRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var entries = await _guild.ListInvitedGuildsAsync(viewerId, ct);
        return new GuildInvitedGuildListResponse
        {
            List = entries.Select(e => new GuildReceivedInviteDto
            {
                GuildId = e.Guild.GuildId,
                GuildName = e.Guild.Name,
                Description = e.Guild.Description,
                GuildEmblemId = e.Guild.EmblemId,
                JoinCondition = (int)e.Guild.JoinCondition,
                Activity = (int)e.Guild.Activity,
                MemberNum = e.MemberNum,
                LeaderName = e.LeaderName,
                LeaderViewerId = e.Guild.LeaderViewerId,
                InviteId = e.InviteId,
            }).ToList(),
        };
    }

    [HttpPost("invite")]
    public async Task<ActionResult<EmptyResponse>> Invite([FromBody] GuildInviteRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.InviteAsync(viewerId, req.InvitedViewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("cancel_invite")]
    public async Task<ActionResult<EmptyResponse>> CancelInvite([FromBody] GuildCancelInviteRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.CancelInviteAsync(viewerId, req.InviteId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("reject_invite")]
    public async Task<ActionResult<EmptyResponse>> RejectInvite([FromBody] GuildRejectInviteRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.RejectInviteAsync(viewerId, req.InviteId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("join")]
    public async Task<ActionResult<GuildJoinResponse>> Join([FromBody] GuildJoinEndpointRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.JoinAsync(viewerId, req.GuildId, ct);
        if (!r.IsOk) return WireError();
        // guild_status is branch-specific: JOINING (2) for instant joins, APPLYING (1) for approval path.
        // GuildJoinTask.Parse() reads this directly: GuildStatus = (eGUILD_STATUS)data["guild_status"].ToInt()
        return new GuildJoinResponse { GuildStatus = r.GuildStatus ?? 2 };
    }

    [HttpPost("cancel_join_request")]
    public async Task<ActionResult<EmptyResponse>> CancelJoinRequest([FromBody] GuildCancelJoinRequestRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.CancelJoinRequestAsync(viewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("join_request_list")]
    public async Task<ActionResult<GuildJoinRequestListResponse>> JoinRequestList([FromBody] GuildJoinRequestListRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var entries = await _guild.ListPendingJoinRequestsForMyGuildAsync(viewerId, ct);
        return new GuildJoinRequestListResponse
        {
            Users = entries.Select(e => new GuildJoinRequestEntryDto
            {
                ViewerId = e.ApplicantViewerId,
                Name = e.Name,
                EmblemId = e.EmblemId,
                CountryCode = e.CountryCode,
                Rank = e.Rank,
                DegreeId = e.DegreeId,
                IsOfficialMarkDisplayed = e.IsOfficialMarkDisplayed ? 1 : 0,
                RequestTime = new DateTimeOffset(e.RequestedAt, TimeSpan.Zero).ToUnixTimeSeconds(),
            }).ToList(),
        };
    }

    [HttpPost("join_request_accept")]
    public async Task<ActionResult<EmptyResponse>> JoinRequestAccept([FromBody] GuildJoinRequestAcceptRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.AcceptJoinRequestAsync(viewerId, req.RequestViewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("reject_join_request")]
    public async Task<ActionResult<EmptyResponse>> RejectJoinRequest([FromBody] GuildRejectJoinRequestRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.RejectJoinRequestAsync(viewerId, req.RequestViewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("leave")]
    public async Task<ActionResult<EmptyResponse>> Leave([FromBody] BaseRequest _, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.LeaveAsync(viewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("remove")]
    public async Task<ActionResult<EmptyResponse>> Remove([FromBody] GuildRemoveRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.RemoveAsync(viewerId, req.RemoveViewerId, ct);
        return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
    }

    [HttpPost("change_role")]
    public async Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
    {
        if (!TryGetViewerId(out var viewerId)) return Unauthorized();
        var r = await _guild.ChangeRoleAsync(viewerId, req.TargetViewerId, req.RoleId, ct);
        if (!r.IsOk) return WireError();

        // Load caller's membership to get the guild id, then return the full updated member list.
        var membership = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct);
        if (membership is null) return WireError();

        var members = await _db.GuildMembers
            .Where(m => m.GuildId == membership.GuildId)
            .ToListAsync(ct);

        var memberDtos = await ToMemberDtoListAsync(members, viewerId, ct);
        return new GuildChangeRoleResponse { Members = memberDtos };
    }

    // ===== 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<List<GuildMemberInfoDto>> ToMemberDtoListAsync(
        IReadOnlyList<GuildMember> 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<GuildMemberInfoDto>(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,
    };

    /// <summary>
    /// 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 <c>result_code: 2</c>.
    /// </summary>
    private ActionResult<EmptyResponse> MapErrorToWire(GuildOpResult res)
        => Ok(new { result_code = GuildErrorResultCode });

    /// <summary>
    /// 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.
    /// </summary>
    private ObjectResult WireError() => Ok(new { result_code = GuildErrorResultCode });

    private static Task<ActionResult<EmptyResponse>> Stub() =>
        Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
}
