// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

import Foundation
import GRDB
import SessionUtilitiesKit

public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
    public static var databaseTableName: String { "openGroup" }
    internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
    private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
    private static let members = hasMany(GroupMember.self, using: GroupMember.openGroupForeignKey)
    
    public typealias Columns = CodingKeys
    public enum CodingKeys: String, CodingKey, ColumnExpression {
        case threadId
        case server
        case roomToken
        case publicKey
        case name
        case isActive
        case roomDescription = "description"
        case imageId
        case imageData
        case userCount
        case infoUpdates
        case sequenceNumber
        case inboxLatestMessageId
        case outboxLatestMessageId
        case pollFailureCount
        case permissions
    }
    
    public struct Permissions: OptionSet, Codable, DatabaseValueConvertible, Hashable {
        public let rawValue: UInt16
        
        public init(rawValue: UInt16) {
            self.rawValue = rawValue
        }
        
        public init(roomInfo: OpenGroupAPI.RoomPollInfo) {
            var permissions: Permissions = []
            
            if roomInfo.read { permissions.insert(.read) }
            if roomInfo.write { permissions.insert(.write) }
            if roomInfo.upload { permissions.insert(.upload) }
            
            self.init(rawValue: permissions.rawValue)
        }
        
        public func toString() -> String {
            return ""
                .appending(self.contains(.read) ? "r" : "-")
                .appending(self.contains(.write) ? "w" : "-")
                .appending(self.contains(.upload) ? "u" : "-")
        }

        static let read: Permissions = Permissions(rawValue: 1 << 0)
        static let write: Permissions = Permissions(rawValue: 1 << 1)
        static let upload: Permissions = Permissions(rawValue: 1 << 2)
        
        static let all: Permissions = [ .read, .write, .upload ]
    }
    
    public var id: String { threadId }  // Identifiable
    
    /// The id for the thread this open group belongs to
    ///
    /// **Note:** This value will always be `\(server).\(room)` (This needs it’s own column to
    /// allow for db joining to the Thread table)
    public let threadId: String
    
    /// The server for the group
    public let server: String
    
    /// The specific room on the server for the group
    ///
    /// **Note:** In order to support the default open group query we need an OpenGroup entry in
    /// the database, for this entry the `roomToken` value will be an empty string so we can ignore
    /// it when polling
    public let roomToken: String
    
    /// The public key for the group
    public let publicKey: String
    
    /// Flag indicating whether this is an OpenGroup the user has actively joined (we store inactive
    /// open groups so we can display them in the UI but they won't be polled for)
    public let isActive: Bool
    
    /// The name for the group
    public let name: String
    
    /// The description for the room
    public let roomDescription: String?
    
    /// The ID with which the image can be retrieved from the server
    public let imageId: String?
    
    /// The image for the group
    public let imageData: Data?
    
    /// The number of users in the group
    public let userCount: Int64
    
    /// Monotonic room information counter that increases each time the room's metadata changes
    public let infoUpdates: Int64
    
    /// Sequence number for the most recently received message from the open group
    public let sequenceNumber: Int64
    
    /// The id of the most recently received inbox message
    ///
    /// **Note:** This value is unique per server rather than per room (ie. all rooms in the same server will be
    /// updated whenever this value changes)
    public let inboxLatestMessageId: Int64
    
    /// The id of the most recently received outbox message
    ///
    /// **Note:** This value is unique per server rather than per room (ie. all rooms in the same server will be
    /// updated whenever this value changes)
    public let outboxLatestMessageId: Int64
    
    /// The number of times this room has failed to poll since the last successful poll
    public let pollFailureCount: Int64
    
    /// The permissions this room has for current user
    public let permissions: Permissions?

    // MARK: - Relationships
    
    public var thread: QueryInterfaceRequest<SessionThread> {
        request(for: OpenGroup.thread)
    }

    public var moderatorIds: QueryInterfaceRequest<GroupMember> {
        request(for: OpenGroup.members)
            .filter(GroupMember.Columns.role == GroupMember.Role.moderator)
    }
    
    public var adminIds: QueryInterfaceRequest<GroupMember> {
        request(for: OpenGroup.members)
            .filter(GroupMember.Columns.role == GroupMember.Role.admin)
    }
    
    // MARK: - Initialization
    
    public init(
        server: String,
        roomToken: String,
        publicKey: String,
        isActive: Bool,
        name: String,
        roomDescription: String? = nil,
        imageId: String? = nil,
        imageData: Data? = nil,
        userCount: Int64,
        infoUpdates: Int64,
        sequenceNumber: Int64 = 0,
        inboxLatestMessageId: Int64 = 0,
        outboxLatestMessageId: Int64 = 0,
        pollFailureCount: Int64 = 0,
        permissions: Permissions? = nil
    ) {
        self.threadId = OpenGroup.idFor(roomToken: roomToken, server: server)
        self.server = server.lowercased()
        self.roomToken = roomToken
        self.publicKey = publicKey
        self.isActive = isActive
        self.name = name
        self.roomDescription = roomDescription
        self.imageId = imageId
        self.imageData = imageData
        self.userCount = userCount
        self.infoUpdates = infoUpdates
        self.sequenceNumber = sequenceNumber
        self.inboxLatestMessageId = inboxLatestMessageId
        self.outboxLatestMessageId = outboxLatestMessageId
        self.pollFailureCount = pollFailureCount
        self.permissions = permissions
    }
}

// MARK: - GRDB Interactions

public extension OpenGroup {
    static func fetchOrCreate(
        _ db: Database,
        server: String,
        roomToken: String,
        publicKey: String
    ) -> OpenGroup {
        guard let existingGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) else {
            return OpenGroup(
                server: server,
                roomToken: roomToken,
                publicKey: publicKey,
                isActive: false,
                name: roomToken,    // Default the name to the `roomToken` until we get retrieve the actual name
                roomDescription: nil,
                imageId: nil,
                imageData: nil,
                userCount: 0,
                infoUpdates: 0,
                sequenceNumber: 0,
                inboxLatestMessageId: 0,
                outboxLatestMessageId: 0,
                pollFailureCount: 0,
                permissions: nil
            )
        }
        
        return existingGroup
    }
}

// MARK: - Convenience

public extension OpenGroup {
    static func idFor(roomToken: String, server: String) -> String {
        // Always force the server to lowercase
        return "\(server.lowercased()).\(roomToken)"
    }
    
    static func urlFor(server: String, roomToken: String, publicKey: String) -> String {
        return "\(server)/\(roomToken)?public_key=\(publicKey)"
    }
}

extension OpenGroup: CustomStringConvertible, CustomDebugStringConvertible {
    public var description: String { "\(name) (Server: \(server), Room: \(roomToken))" }
    public var debugDescription: String {
        [
            "OpenGroup(server: \"\(server)\"",
            "roomToken: \"\(roomToken)\"",
            "id: \"\(id)\"",
            "publicKey: \"\(publicKey)\"",
            "isActive: \(isActive)",
            "name: \"\(name)\"",
            "roomDescription: \(roomDescription.map { "\"\($0)\"" } ?? "null")",
            "imageId: \(imageId ?? "null")",
            "userCount: \(userCount)",
            "infoUpdates: \(infoUpdates)",
            "sequenceNumber: \(sequenceNumber)",
            "inboxLatestMessageId: \(inboxLatestMessageId)",
            "outboxLatestMessageId: \(outboxLatestMessageId)",
            "pollFailureCount: \(pollFailureCount)",
            "permissions: \(permissions?.toString() ?? "---"))"
        ].joined(separator: ", ")
    }
}
