namespace DodoSSH.Domain;

/// <summary>
/// A group of users who can share vaults.
/// </summary>
/// <remarks>
/// The tables exist from the first migration although team features ship in M3. Adding them
/// later would mean altering <see cref="Vault"/> to introduce a foreign key on a live table, and
/// the cost of carrying two unused tables is far lower than that.
/// </remarks>
public sealed class Team
{
    /// <summary>Primary key.</summary>
    public Guid Id { get; set; }

    /// <summary>Display name.</summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>URL-safe unique identifier.</summary>
    public string Slug { get; set; } = string.Empty;

    /// <summary>Optional description.</summary>
    public string? Description { get; set; }

    /// <summary>Who created it.</summary>
    public Guid CreatedByUserId { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Soft-delete marker.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }

    /// <summary>Members.</summary>
    public ICollection<TeamMembership> Memberships { get; } = [];

    /// <summary>Invitations to addresses that have no account here yet.</summary>
    public ICollection<TeamInvitation> Invitations { get; } = [];
}

/// <summary>
/// A user's membership of a team.
/// </summary>
/// <remarks>
/// Revoked memberships are retained rather than deleted, so historic audit entries remain
/// resolvable to a person.
/// </remarks>
public sealed class TeamMembership
{
    /// <summary>Primary key.</summary>
    public Guid Id { get; set; }

    /// <summary>The team.</summary>
    public Guid TeamId { get; set; }

    /// <summary>The team.</summary>
    public Team? Team { get; set; }

    /// <summary>The member.</summary>
    public Guid UserId { get; set; }

    /// <summary>The member.</summary>
    public UserAccount? User { get; set; }

    /// <summary>Role within the team.</summary>
    public TeamRole Role { get; set; }

    /// <summary>Membership state.</summary>
    public MembershipStatus Status { get; set; }

    /// <summary>Who invited them.</summary>
    public Guid? InvitedByUserId { get; set; }

    /// <summary>When the invitation was accepted.</summary>
    public DateTimeOffset? JoinedAtUtc { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>Soft-delete marker.</summary>
    public DateTimeOffset? DeletedAtUtc { get; set; }
}

/// <summary>
/// A standing offer of membership to an email address that has no account here yet.
/// </summary>
/// <remarks>
/// <para>
/// <b>Its own table rather than a <see cref="TeamMembership"/> with
/// <see cref="MembershipStatus.Invited"/>.</b> A membership names an account —
/// <c>team_membership.user_id</c> is not nullable and carries a foreign key to
/// <see cref="UserAccount"/> — so an invitee who has never signed in has nothing for that row to
/// point at. Widening that column would make the unique index on (team, user) meaningless, because
/// PostgreSQL counts every NULL as distinct, and would silently change what every
/// <c>m.UserId == user.Id</c> query in the server means.
/// </para>
/// <para>
/// <b>There is no token.</b> Nothing is sent, because this server has no outbound mail path; the row
/// is an instruction to the sign-in path rather than a secret somebody presents. That is why it is
/// keyed on the address and why the address has to be one the identity provider marks verified before
/// the claim is honoured — an unclaimable invitation is an inconvenience, but one claimable by
/// anybody who can assert an address is a way into the team.
/// </para>
/// <para>
/// Revoked and accepted rows are retained rather than deleted, as <see cref="VaultKeyGrant"/> is and
/// for the same reason: the uniqueness that matters is among <em>pending</em> invitations, and the
/// history of who invited whom stays resolvable.
/// </para>
/// </remarks>
public sealed class TeamInvitation
{
    /// <summary>Primary key.</summary>
    public Guid Id { get; set; }

    /// <summary>The team.</summary>
    public Guid TeamId { get; set; }

    /// <summary>The team.</summary>
    public Team? Team { get; set; }

    /// <summary>The address invited. Case-insensitive.</summary>
    public string Email { get; set; } = string.Empty;

    /// <summary>Role the membership will carry when it is claimed.</summary>
    public TeamRole Role { get; set; }

    /// <summary>Who issued it.</summary>
    public Guid InvitedByUserId { get; set; }

    /// <summary>Creation timestamp.</summary>
    public DateTimeOffset CreatedAtUtc { get; set; }

    /// <summary>When it stops being claimable.</summary>
    public DateTimeOffset ExpiresAtUtc { get; set; }

    /// <summary>When an account with this address signed in and took it up.</summary>
    public DateTimeOffset? AcceptedAtUtc { get; set; }

    /// <summary>Which account took it up.</summary>
    public Guid? AcceptedByUserId { get; set; }

    /// <summary>Revocation timestamp.</summary>
    public DateTimeOffset? RevokedAtUtc { get; set; }
}
