// SPDX-License-Identifier: MIT OR Apache-2.0

use serde::{Deserialize, Serialize};

use p2panda_auth::Access;
use p2panda_auth::group::GroupMember;
use p2panda_auth::traits::{Conditions, Operation};
use p2panda_encryption::data_scheme::GroupOutput;

use crate::auth::message::AuthMessage;
use crate::message::SpaceMembershipMessage;
use crate::types::{AuthGroupAction, AuthGroupState, EncryptionGroupOutput};
use crate::utils::{added_members, removed_members, sort_members};
use crate::{ActorId, GroupId, MemberId, SpaceId};

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct GroupActor {
    id: ActorId,
    is_group: bool,
}

impl GroupActor {
    pub fn individual(id: MemberId) -> Self {
        Self {
            id,
            is_group: false,
        }
    }

    pub fn group(id: GroupId) -> Self {
        Self { id, is_group: true }
    }

    pub fn from_group_member(group_member: GroupMember<ActorId>) -> Self {
        match group_member {
            GroupMember::Individual(id) => GroupActor::individual(id),
            GroupMember::Group(id) => GroupActor::group(id),
        }
    }

    pub fn id(&self) -> ActorId {
        self.id
    }

    pub fn is_group(&self) -> bool {
        self.is_group
    }
}

/// Events emitted when system state changes or application messages are processed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum Event<C> {
    Application { space_id: SpaceId, data: Vec<u8> },
    // @TODO: Could maybe add field to show when the bundle is valid until?
    KeyBundle { author: MemberId },
    Group(GroupEvent<C>),
    Space(SpaceEvent),
}

/// Additional context attached to group events.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroupContext<C> {
    /// The actor who authored this action.
    pub author: ActorId,

    /// Root group actors, can be individuals or groups.
    pub group_actors: Vec<(GroupActor, Access<C>)>,

    /// Members of this group.
    pub members: Vec<(ActorId, Access<C>)>,
}

/// Additional context attached to space events.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpaceContext {
    /// The actor who authored this group change action.
    pub auth_author: MemberId,

    /// The actor who applied this action to the spaces state.
    ///
    /// Note: this can be different to the auth_actor in cases where concurrent auth changes which
    /// effect a space are applied later.
    pub spaces_author: MemberId,

    /// Id of the group associated with this space.
    pub group_id: GroupId,

    /// Members in the spaces' encryption context.
    pub members: Vec<MemberId>,
}

/// Events emitted when global auth state changes.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum GroupEvent<C> {
    /// A group was created.
    Created {
        /// Group id.
        group_id: GroupId,

        /// Initial group members.
        initial_members: Vec<(GroupActor, Access<C>)>,

        /// Additional event context and group state after any change occurred.
        context: GroupContext<C>,
    },

    /// A member was added to a group.
    Added {
        /// Group id.
        group_id: GroupId,

        /// Group actor that was added, can be individual or group.
        added: GroupActor,

        /// Access level assigned to the added members.
        access: Access<C>,

        /// Additional event context and group state after any change occurred.
        context: GroupContext<C>,
    },

    /// A member was removed from a group.
    Removed {
        /// Group id.
        group_id: GroupId,

        /// Group actor that was removed, can be individual or group.
        removed: GroupActor,

        /// Additional event context and group state after any change occurred.
        context: GroupContext<C>,
    },
}

/// Events emitted when space encryption group membership changes.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpaceEvent {
    /// A space was created.
    Created {
        /// Space id.
        space_id: SpaceId,

        /// Initial members in the encryption context.
        initial_members: Vec<MemberId>,

        /// Additional event context and space state after any change occurred.
        context: SpaceContext,
    },

    /// One or many individuals were added to the space.
    Added {
        /// Space id.
        space_id: SpaceId,

        /// Members added to the encryption context.
        added: Vec<MemberId>,

        /// Additional event context and space state after any change occurred.
        context: SpaceContext,
    },

    /// One or many individuals were removed from the space.
    Removed {
        /// Space id.
        space_id: SpaceId,

        /// Members removed from the encryption context.
        removed: Vec<MemberId>,

        /// Additional event context and space state after any change occurred.
        context: SpaceContext,
    },

    /// Local actor was removed from the space.
    Ejected {
        /// Space id.
        space_id: SpaceId,
    },
}

pub(crate) fn encryption_output_to_space_events<C>(
    space_id: &SpaceId,
    encryption_output: Vec<EncryptionGroupOutput>,
) -> Vec<Event<C>>
where
    C: Conditions,
{
    encryption_output
        .into_iter()
        .filter_map(|event| match event {
            EncryptionGroupOutput::Application { plaintext } => Some(Event::Application {
                space_id: *space_id,
                data: plaintext,
            }),
            GroupOutput::Control(_control_message) => {
                unreachable!()
            }
            // A removal of the local actor from a space could also be detected from observing
            // changes to the auth group state, we hook into the encryption output here though to
            // improve observability of the internal encryption state.
            GroupOutput::Removed => Some(Event::Space(SpaceEvent::Ejected {
                space_id: *space_id,
            })),
        })
        .collect()
}

pub(crate) fn auth_message_to_group_event<C>(
    auth_y: &AuthGroupState<C>,
    auth_message: &AuthMessage<C>,
) -> Event<C>
where
    C: Conditions,
{
    let group_id = auth_message.group_id();
    let mut group_actors: Vec<_> = auth_y
        .root_members(group_id)
        .into_iter()
        .map(|(member, access)| (GroupActor::from_group_member(member), access))
        .collect();
    sort_members(&mut group_actors);
    let mut members = auth_y.members(group_id);
    sort_members(&mut members);

    let context = GroupContext {
        author: auth_message.author(),
        members,
        group_actors,
    };

    let group_event = match auth_message.action() {
        AuthGroupAction::Create { .. } => GroupEvent::Created {
            group_id,
            initial_members: context.group_actors.clone(),
            context,
        },
        AuthGroupAction::Add { member, access } => GroupEvent::Added {
            group_id,
            added: GroupActor::from_group_member(member),
            access,
            context,
        },
        AuthGroupAction::Remove { member } => GroupEvent::Removed {
            group_id,
            removed: GroupActor::from_group_member(member),
            context,
        },
        AuthGroupAction::Promote { .. } => unimplemented!(),
        AuthGroupAction::Demote { .. } => unimplemented!(),
    };

    Event::Group(group_event)
}

pub(crate) fn space_message_to_space_event<C>(
    space_id: SpaceId,
    space_message: &SpaceMembershipMessage,
    auth_message: &AuthMessage<C>,
    current_members: Vec<MemberId>,
    next_members: Vec<MemberId>,
) -> Event<C>
where
    C: Conditions,
{
    let group_id = auth_message.group_id();

    let context = SpaceContext {
        auth_author: auth_message.author(),
        spaces_author: space_message.author,
        group_id,
        members: next_members.clone(),
    };

    let space_event = match auth_message.action() {
        AuthGroupAction::Create { .. } => SpaceEvent::Created {
            space_id,
            initial_members: next_members,
            context,
        },
        AuthGroupAction::Add { .. } => {
            let added = added_members(current_members, next_members.clone());
            SpaceEvent::Added {
                space_id,
                added,
                context,
            }
        }
        AuthGroupAction::Remove { .. } => {
            let removed = removed_members(current_members, next_members.clone());
            SpaceEvent::Removed {
                space_id,
                removed,
                context,
            }
        }
        AuthGroupAction::Promote { .. } => unimplemented!(),
        AuthGroupAction::Demote { .. } => unimplemented!(),
    };

    Event::Space(space_event)
}
