using SharpChat.Snowflake;

namespace SharpChat.Channels;

public class ChannelsContext(RandomSnowflake snowflake) {
    private readonly Dictionary<long, Channel> Channels = [];
    private Channel? DefaultChannel = null;
    private readonly Lock @lock = new();

    public bool ChannelExists(Func<Channel, bool> predicate) {
        lock(@lock)
            return Channels.Values.Any(predicate);
    }

    public bool ChannelExists(string name) {
        return ChannelExists(c => c.NameEquals(name));
    }

    public Channel GetDefaultChannel() {
        lock(@lock) {
            DefaultChannel ??= GetChannel(c => c.IsPublic && !c.IsTemporary) ?? throw new NoDefaultChannelException();
            return DefaultChannel;
        }
    }

    public void SetDefaultChannel(Channel channel)
        => SetDefaultChannel(channel.Name);

    public void SetDefaultChannel(string channelName) {
        lock(@lock)
            DefaultChannel = GetChannel(channelName) ?? throw new ChannelNotFoundException(nameof(channelName));
    }

    public Channel? GetChannel(Func<Channel, bool> predicate) {
        lock(@lock)
            return Channels.Values.FirstOrDefault(predicate);
    }

    public Channel? GetChannel(string name) {
        return GetChannel(c => c.NameEquals(name));
    }

    public Channel? GetChannel(long id) {
        lock(@lock)
            return Channels.TryGetValue(id, out Channel? channel) ? channel : null;
    }

    public IEnumerable<Channel> GetChannels() {
        lock(@lock)
            return [.. Channels.Values];
    }

    public IEnumerable<Channel> GetChannels(IEnumerable<long> ids) {
        return [.. ids.Select(GetChannel).Where(c => c is not null).Cast<Channel>()];
    }

    public IEnumerable<Channel> GetChannels(Func<Channel, bool> predicate) {
        lock(@lock)
            return [.. Channels.Values.Where(predicate)];
    }

    public IEnumerable<Channel> GetChannels(IEnumerable<string> names) {
        return GetChannels(c => names.Any(n => c.NameEquals(n)));
    }

    public IEnumerable<Channel> GetChannels(int maxRank) {
        return GetChannels(c => c.Rank <= maxRank);
    }

    public Channel CreateChannel(
        string name,
        string password = "",
        bool temporary = false,
        int rank = 0,
        string ownerId = "",
        long? id = null
    ) {
        if(!Channel.CheckName(name))
            throw new ChannelNameFormatException(nameof(name));

        lock(@lock) {
            if(ChannelExists(name))
                throw new ChannelExistsException(nameof(name));

            id ??= snowflake.Next();
            if(Channels.ContainsKey(id.Value))
                throw new ChannelExistsException(nameof(id));

            Channel channel = new(id.Value, name, password, temporary, rank, ownerId);
            Channels.Add(id.Value, channel);

            return channel;
        }
    }

    public ChannelDiff UpdateChannel(
        long id,
        string? name = null,
        string? password = null,
        bool? temporary = null,
        int? rank = null,
        string? ownerId = null
    ) => UpdateChannelInternal(
        GetChannel(id) ?? throw new ChannelNotFoundException(nameof(id)),
        name, password, temporary, rank, ownerId
    );

    public ChannelDiff UpdateChannel(
        string currentName,
        string? name = null,
        string? password = null,
        bool? temporary = null,
        int? rank = null,
        string? ownerId = null
    ) => UpdateChannelInternal(
        GetChannel(currentName) ?? throw new ChannelNotFoundException(nameof(currentName)),
        name, password, temporary, rank, ownerId
    );

    public ChannelDiff UpdateChannel(
        Channel channel,
        string? name = null,
        string? password = null,
        bool? temporary = null,
        int? rank = null,
        string? ownerId = null
    ) => UpdateChannel(channel.Id, name, password, temporary, rank, ownerId);

    private ChannelDiff UpdateChannelInternal(
        Channel channel,
        string? name = null,
        string? password = null,
        bool? temporary = null,
        int? rank = null,
        string? ownerId = null
    ) {
        lock(@lock) {
            StringDiff nameDiff = new(channel.Name, name);
            if(nameDiff.Changed) {
                if(!Channel.CheckName(nameDiff.After))
                    throw new ChannelNameFormatException(nameof(name));
                if(ChannelExists(nameDiff.After))
                    throw new ChannelExistsException(nameof(name));

                channel.Name = nameDiff.After;
            }

            StringDiff passwordDiff = new(channel.Password, password);
            if(passwordDiff.Changed)
                channel.Password = passwordDiff.After;

            ValueDiff<bool> temporaryDiff = new(channel.IsTemporary, temporary);
            if(temporaryDiff.Changed)
                channel.IsTemporary = temporaryDiff.After;

            ValueDiff<int> rankDiff = new(channel.Rank, rank);
            if(rankDiff.Changed)
                channel.Rank = rankDiff.After;

            StringDiff ownerIdDiff = new(channel.OwnerId, ownerId);
            if(ownerIdDiff.Changed)
                channel.OwnerId = ownerIdDiff.After;

            return new(
                channel,
                nameDiff,
                passwordDiff,
                temporaryDiff,
                rankDiff,
                ownerIdDiff
            );
        }
    }

    public void RemoveChannel(long id)
        => RemoveChannelInternal(GetChannel(id) ?? throw new ChannelNotFoundException(nameof(id)), nameof(id));

    public void RemoveChannel(Channel channel)
        => RemoveChannel(channel.Name);

    public void RemoveChannel(string name)
        => RemoveChannelInternal(GetChannel(name) ?? throw new ChannelNotFoundException(nameof(name)), nameof(name));

    private void RemoveChannelInternal(Channel channel, string argName) {
        lock(@lock) {
            Channel defaultChannel = GetDefaultChannel();
            if(channel == defaultChannel || defaultChannel.NameEquals(channel.Name))
                throw new ChannelIsDefaultException(argName);

            Channels.Remove(channel.Id);
        }
    }
}