using RpgRoller.Contracts;
using RpgRoller.Domain;

namespace RpgRoller.Services;

public sealed class GameCharacterService(GameStateStore stateStore, GamePersistenceService persistenceService)
{
    public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
    {
        if (string.IsNullOrWhiteSpace(name))
            return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");

        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.CampaignsById.ContainsKey(campaignId))
                return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");

            var character = new Character
            {
                Id = Guid.NewGuid(),
                OwnerUserId = user.Id,
                CampaignId = campaignId,
                Name = name.Trim()
            };

            stateStore.CharactersById[character.Id] = character;
            stateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
            stateStore.TouchRosterLocked(character.CampaignId);

            persistenceService.PersistStateLocked();
            return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(stateStore, character));
        }
    }

    public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name,
        Guid? campaignId, string? ownerUsername = null)
    {
        if (string.IsNullOrWhiteSpace(name))
            return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");

        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
                return ServiceResult<CharacterSummary>.Failure("character_not_found", "Character was not found.");

            Campaign? targetCampaign = null;
            if (campaignId.HasValue && !stateStore.CampaignsById.TryGetValue(campaignId.Value, out targetCampaign))
                return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");

            var isOwner = character.OwnerUserId == user.Id;
            var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin);
            var isSourceGm = character.CampaignId.HasValue &&
                             stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) &&
                             sourceCampaign.GmUserId == user.Id;
            var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
            if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
                return ServiceResult<CharacterSummary>.Failure("forbidden",
                    "Only the owner, GM, or admin can edit this character.");

            var sourceCampaignId = character.CampaignId;
            var previousOwnerUserId = character.OwnerUserId;
            character.Name = name.Trim();
            character.CampaignId = campaignId;

            if (!string.IsNullOrWhiteSpace(ownerUsername))
            {
                var trimmedOwnerUsername = ownerUsername.Trim();
                var normalizedOwnerUsername = NormalizeUsername(trimmedOwnerUsername);
                if (!stateStore.UserIdsByUsername.TryGetValue(normalizedOwnerUsername, out var targetOwnerUserId))
                    return ServiceResult<CharacterSummary>.Failure("owner_not_found", "Owner username was not found.");

                if (targetOwnerUserId != character.OwnerUserId && !isAdmin && !isSourceGm && !isTargetGm)
                    return ServiceResult<CharacterSummary>.Failure("forbidden",
                        "Only the GM or admin can change character owner.");

                character.OwnerUserId = targetOwnerUserId;
                if (character.OwnerUserId != previousOwnerUserId &&
                    stateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) &&
                    previousOwner.ActiveCharacterId == character.Id)
                    previousOwner.ActiveCharacterId = null;
            }

            if (sourceCampaignId != character.CampaignId)
            {
                stateStore.RemoveCharacterStateLocked(sourceCampaignId, character.Id);
                stateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
            }

            stateStore.TouchRosterLocked(sourceCampaignId);
            if (sourceCampaignId != character.CampaignId)
                stateStore.TouchRosterLocked(character.CampaignId);

            persistenceService.PersistStateLocked();
            return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(stateStore, character));
        }
    }

    public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
                return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");

            var isOwner = character.OwnerUserId == user.Id;
            var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin);
            if (!isOwner && !isAdmin)
                return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");

            DeleteCharacterLocked(characterId);
            persistenceService.PersistStateLocked();
            return ServiceResult<bool>.Success(true);
        }
    }

    public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");

            if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
                return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");

            if (character.OwnerUserId != user.Id)
            {
                if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign,
                        out var campaignError))
                    return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);

                if (campaign!.GmUserId != user.Id)
                    return ServiceResult<bool>.Failure("forbidden",
                        "You can activate only your own character unless you GM its campaign.");
            }

            user.ActiveCharacterId = character.Id;
            persistenceService.PersistStateLocked();
            return ServiceResult<bool>.Success(true);
        }
    }

    public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
    {
        lock (stateStore.Gate)
        {
            var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
            if (user is null)
                return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");

            var characters = stateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id)
                .OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase)
                .Select(character => GameDtoMapper.ToCharacterSummary(stateStore, character)).ToArray();

            return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
        }
    }

    private void DeleteCharacterLocked(Guid characterId)
    {
        if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
            return;

        var campaignId = character.CampaignId;
        stateStore.CharactersById.Remove(characterId);

        var skillGroupIds = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId)
            .Select(group => group.Id).ToHashSet();
        foreach (var skillGroupId in skillGroupIds)
            stateStore.SkillGroupsById.Remove(skillGroupId);

        var skillIds = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId)
            .Select(skill => skill.Id).ToHashSet();
        foreach (var skillId in skillIds)
            stateStore.SkillsById.Remove(skillId);

        stateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId));

        foreach (var user in stateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId))
            user.ActiveCharacterId = null;

        stateStore.RemoveCharacterStateLocked(campaignId, characterId);
        stateStore.TouchRosterLocked(campaignId);
    }

    private static string NormalizeUsername(string username)
    {
        return username.ToUpperInvariant();
    }
}