import Foundation
import GRDB
import os

// MARK: - Group Repository

/// Manages group chat metadata, invite strings, and encryption keys in SQLite.
/// Extracted from DatabaseManager — all group-related DB operations live here.
@MainActor
final class GroupRepository {
    static let shared = GroupRepository()

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "GroupRepo")
    private static let groupInvitePassword = "rosetta_group"
    private let db = DatabaseManager.shared

    /// In-memory cache: ["{account}:{groupId}"] → decrypted group key.
    /// Prevents GRDB reentrancy crash when groupKey() is called from within a db.read transaction
    /// (e.g., MessageRepository.lastDecryptedMessage → decryptRecord → tryReDecrypt → groupKey).
    private var keyCache: [String: String] = [:]
    private var metadataCache: [String: GroupMetadata] = [:]
    private var memberCache: [String: CachedGroupMembers] = [:]

    private init() {}

    /// Clears all in-memory caches. Call on logout/account switch.
    func clearCaches() {
        keyCache.removeAll()
        metadataCache.removeAll()
        memberCache.removeAll()
    }

    private func cacheKey(account: String, groupId: String) -> String {
        "\(account):\(groupId)"
    }

    // MARK: - Public Types

    struct GroupMetadata: Equatable {
        let title: String
        let description: String
    }

    struct CachedGroupMembers: Equatable {
        let memberKeys: [String]
        let adminKey: String
    }

    struct ParsedGroupInvite: Sendable {
        let groupId: String
        let title: String
        let encryptKey: String
        let description: String
    }

    // MARK: - Normalization

    func isGroupDialog(_ value: String) -> Bool {
        DatabaseManager.isGroupDialogKey(value)
    }

    func normalizeGroupId(_ value: String) -> String {
        Self.normalizeGroupIdPure(value)
    }

    /// Thread-safe group ID normalization — strips `#group:`, `group:`, `conversation:` prefixes.
    nonisolated static func normalizeGroupIdPure(_ value: String) -> String {
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        let lower = trimmed.lowercased()
        if lower.hasPrefix("#group:") {
            return String(trimmed.dropFirst("#group:".count))
                .trimmingCharacters(in: .whitespacesAndNewlines)
        }
        if lower.hasPrefix("group:") {
            return String(trimmed.dropFirst("group:".count))
                .trimmingCharacters(in: .whitespacesAndNewlines)
        }
        if lower.hasPrefix("conversation:") {
            return String(trimmed.dropFirst("conversation:".count))
                .trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return trimmed
    }

    func toGroupDialogKey(_ value: String) -> String {
        let normalized = normalizeGroupId(value)
        guard !normalized.isEmpty else {
            return value.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return "#group:\(normalized)"
    }

    // MARK: - Key Management

    func groupKey(account: String, privateKeyHex: String, groupDialogKey: String) -> String? {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return nil }

        // Check in-memory cache first (prevents GRDB reentrancy crash).
        let ck = cacheKey(account: account, groupId: groupId)
        if let cached = keyCache[ck] {
            return cached
        }

        do {
            let storedKey = try db.read { db in
                try String.fetchOne(
                    db,
                    sql: """
                        SELECT "key"
                        FROM groups
                        WHERE account = ? AND group_id = ?
                        LIMIT 1
                        """,
                    arguments: [account, groupId]
                )
            }
            guard let storedKey, !storedKey.isEmpty else { return nil }

            if let decrypted = try? CryptoManager.shared.decryptWithPassword(
                storedKey, password: privateKeyHex
            ),
               let plain = String(data: decrypted, encoding: .utf8),
               !plain.isEmpty {
                keyCache[ck] = plain
                return plain
            }

            // Backward compatibility: tolerate legacy plain stored keys.
            keyCache[ck] = storedKey
            return storedKey
        } catch {
            return nil
        }
    }

    func groupMetadata(account: String, groupDialogKey: String) -> GroupMetadata? {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return nil }

        let ck = cacheKey(account: account, groupId: groupId)
        if let cached = metadataCache[ck] {
            return cached
        }

        do {
            let result: GroupMetadata? = try db.read { db in
                guard let row = try Row.fetchOne(
                    db,
                    sql: """
                        SELECT title, description
                        FROM groups
                        WHERE account = ? AND group_id = ?
                        LIMIT 1
                        """,
                    arguments: [account, groupId]
                ) else {
                    return nil
                }
                return GroupMetadata(
                    title: row["title"],
                    description: row["description"]
                )
            }
            if let result {
                metadataCache[ck] = result
            }
            return result
        } catch {
            return nil
        }
    }

    // MARK: - Member Cache

    func cachedMembers(account: String, groupDialogKey: String) -> CachedGroupMembers? {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return nil }
        let ck = cacheKey(account: account, groupId: groupId)
        return memberCache[ck]
    }

    func updateMemberCache(account: String, groupDialogKey: String, memberKeys: [String]) {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty, !memberKeys.isEmpty else { return }
        let ck = cacheKey(account: account, groupId: groupId)
        let cached = CachedGroupMembers(memberKeys: memberKeys, adminKey: memberKeys[0])
        if memberCache[ck] != cached {
            memberCache[ck] = cached
        }
    }

    /// Returns true if the string looks like a group invite (starts with `#group:` and can be parsed).
    func isValidInviteString(_ value: String) -> Bool {
        parseInviteString(value) != nil
    }

    // MARK: - Invite String

    /// Constructs a shareable invite string: `#group:<encrypted(groupId:title:key[:desc])>`
    /// Uses `encryptWithPasswordDesktopCompat` for cross-platform compatibility (Android/Desktop).
    func constructInviteString(
        groupId: String,
        title: String,
        encryptKey: String,
        description: String = ""
    ) -> String? {
        let normalized = normalizeGroupId(groupId)
        guard !normalized.isEmpty, !title.isEmpty, !encryptKey.isEmpty else { return nil }

        var payload = "\(normalized):\(title):\(encryptKey)"
        if !description.isEmpty {
            payload += ":\(description)"
        }

        guard let encrypted = try? CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data(payload.utf8),
            password: Self.groupInvitePassword
        ) else {
            return nil
        }
        return "#group:\(encrypted)"
    }

    /// Parses an invite string into its components.
    func parseInviteString(_ inviteString: String) -> ParsedGroupInvite? {
        Self.parseInviteStringPure(inviteString)
    }

    /// Thread-safe (nonisolated) parsing — callable from background layout threads.
    nonisolated static func parseInviteStringPure(_ inviteString: String) -> ParsedGroupInvite? {
        let trimmed = inviteString.trimmingCharacters(in: .whitespacesAndNewlines)
        let lower = trimmed.lowercased()

        let rawPayload: String
        if lower.hasPrefix("#group:") {
            rawPayload = String(trimmed.dropFirst("#group:".count))
        } else if lower.hasPrefix("group:") {
            rawPayload = String(trimmed.dropFirst("group:".count))
        } else {
            return nil
        }
        // Strip internal whitespace/newlines that may be introduced when copying
        // from a chat bubble (message text wraps across multiple lines).
        let encodedPayload = rawPayload.components(separatedBy: .whitespacesAndNewlines).joined()
        guard !encodedPayload.isEmpty else { return nil }

        guard let decryptedPayload = try? CryptoManager.shared.decryptWithPassword(
            encodedPayload,
            password: groupInvitePassword
        ), let payload = String(data: decryptedPayload, encoding: .utf8) else {
            return nil
        }

        let parts = payload.split(separator: ":", omittingEmptySubsequences: false).map(String.init)
        guard parts.count >= 3 else { return nil }

        let groupId = normalizeGroupIdPure(parts[0])
        guard !groupId.isEmpty else { return nil }

        return ParsedGroupInvite(
            groupId: groupId,
            title: parts[1],
            encryptKey: parts[2],
            description: parts.dropFirst(3).joined(separator: ":")
        )
    }

    /// Fast local membership check — uses in-memory keyCache. MainActor only.
    func hasGroup(for groupId: String) -> Bool {
        let account = SessionManager.shared.currentPublicKey
        let key = cacheKey(account: account, groupId: groupId)
        return keyCache[key] != nil
    }

    // MARK: - Persistence

    /// Persists group from a joined `PacketGroupJoin` (server-pushed or self-initiated).
    @discardableResult
    func upsertFromGroupJoin(
        account: String,
        privateKeyHex: String,
        packet: PacketGroupJoin
    ) -> String? {
        guard packet.status == .joined else { return nil }
        guard !packet.groupString.isEmpty else { return nil }

        guard let decryptedInviteData = try? CryptoManager.shared.decryptWithPassword(
            packet.groupString,
            password: privateKeyHex
        ), let inviteString = String(data: decryptedInviteData, encoding: .utf8),
              let parsed = parseInviteString(inviteString)
        else {
            return nil
        }

        return persistGroup(
            account: account,
            privateKeyHex: privateKeyHex,
            groupId: parsed.groupId,
            title: parsed.title,
            description: parsed.description,
            encryptKey: parsed.encryptKey
        )
    }

    /// Persists a group directly from parsed invite components.
    @discardableResult
    func persistGroup(
        account: String,
        privateKeyHex: String,
        groupId: String,
        title: String,
        description: String,
        encryptKey: String
    ) -> String? {
        guard let encryptedGroupKey = try? CryptoManager.shared.encryptWithPassword(
            Data(encryptKey.utf8),
            password: privateKeyHex
        ) else {
            return nil
        }

        do {
            try db.writeSync { db in
                try db.execute(
                    sql: """
                        INSERT INTO groups (account, group_id, title, description, "key")
                        VALUES (?, ?, ?, ?, ?)
                        ON CONFLICT(account, group_id) DO UPDATE SET
                            title = excluded.title,
                            description = excluded.description,
                            "key" = excluded."key"
                        """,
                    arguments: [account, groupId, title, description, encryptedGroupKey]
                )
            }
        } catch {
            Self.logger.error("Failed to persist group \(groupId): \(error.localizedDescription)")
            return nil
        }

        // Populate in-memory caches so subsequent reads don't hit DB
        // (prevents GRDB reentrancy when called from within a transaction).
        let ck = cacheKey(account: account, groupId: groupId)
        keyCache[ck] = encryptKey
        metadataCache[ck] = GroupMetadata(title: title, description: description)

        return toGroupDialogKey(groupId)
    }

    /// Updates only title and description for an existing group (local-only, no server packet yet).
    func updateGroupMetadata(account: String, groupDialogKey: String, title: String, description: String) {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return }

        do {
            try db.writeSync { db in
                try db.execute(
                    sql: """
                        UPDATE groups SET title = ?, description = ?
                        WHERE account = ? AND group_id = ?
                        """,
                    arguments: [title, description, account, groupId]
                )
            }
        } catch {
            Self.logger.error("Failed to update group metadata \(groupId): \(error.localizedDescription)")
            return
        }

        let ck = cacheKey(account: account, groupId: groupId)
        metadataCache[ck] = GroupMetadata(title: title, description: description)
    }

    /// Deletes a group and all its local messages/dialog.
    func deleteGroup(account: String, groupDialogKey: String) {
        let groupId = normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return }
        let dialogKey = toGroupDialogKey(groupId)

        // Clear caches first.
        let ck = cacheKey(account: account, groupId: groupId)
        keyCache.removeValue(forKey: ck)
        metadataCache.removeValue(forKey: ck)

        do {
            try db.writeSync { db in
                try db.execute(
                    sql: "DELETE FROM groups WHERE account = ? AND group_id = ?",
                    arguments: [account, groupId]
                )
                try db.execute(
                    sql: "DELETE FROM messages WHERE account = ? AND dialog_key = ?",
                    arguments: [account, dialogKey]
                )
                try db.execute(
                    sql: "DELETE FROM dialogs WHERE account = ? AND opponent_key = ?",
                    arguments: [account, dialogKey]
                )
            }
            Self.logger.info("Deleted group \(groupId) and its local data")
        } catch {
            Self.logger.error("Failed to delete group \(groupId): \(error.localizedDescription)")
        }
    }

    /// Returns all group IDs for the given account.
    func allGroupIds(account: String) -> [String] {
        do {
            return try db.read { db in
                try String.fetchAll(
                    db,
                    sql: "SELECT group_id FROM groups WHERE account = ?",
                    arguments: [account]
                )
            }
        } catch {
            return []
        }
    }

    // MARK: - Key Generation (Android parity: 32 random bytes → hex)

    /// Generates a random 32-byte group encryption key as a 64-char hex string.
    /// Matches Android `GroupRepository.generateGroupKey()`.
    func generateGroupKey() -> String {
        var bytes = [UInt8](repeating: 0, count: 32)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return bytes.map { String(format: "%02x", $0) }.joined()
    }
}
