import Foundation
import os

// MARK: - GroupService

/// Orchestrates multi-step group operations (create, join, leave, kick, members).
/// Uses PacketAwaiter for async request-response exchanges with the server.
@MainActor
final class GroupService {
    static let shared = GroupService()

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "GroupService")

    private let groupRepo = GroupRepository.shared
    private let dialogRepo = DialogRepository.shared
    private let proto = ProtocolManager.shared

    private init() {}

    // MARK: - Errors

    enum GroupError: Error, LocalizedError {
        case notAuthenticated
        case serverReturnedEmptyId
        case failedToConstructInvite
        case failedToEncryptInvite
        case joinRejected(GroupStatus)
        case invalidInviteString
        case timeout

        var errorDescription: String? {
            switch self {
            case .notAuthenticated: return "Not connected to server"
            case .serverReturnedEmptyId: return "Server returned empty group ID"
            case .failedToConstructInvite: return "Failed to construct invite string"
            case .failedToEncryptInvite: return "Failed to encrypt invite string"
            case .joinRejected(let status): return "Join rejected: \(status)"
            case .invalidInviteString: return "Invalid invite string"
            case .timeout: return "Server did not respond in time"
            }
        }
    }

    // MARK: - Create Group

    /// Creates a new group: requests ID from server, generates key, joins, persists locally.
    /// Returns a ChatRoute for navigating to the new group chat.
    func createGroup(title: String, description: String = "") async throws -> ChatRoute {
        let session = SessionManager.shared
        let account = session.currentPublicKey
        guard let privateKeyHex = session.privateKeyHex, !account.isEmpty else {
            throw GroupError.notAuthenticated
        }

        Self.logger.info("Creating group: \(title)")

        // Step 1: Request group ID from server.
        let createResponse: PacketCreateGroup = try await PacketAwaiter.send(
            PacketCreateGroup(),
            awaitResponse: 0x11,
            where: { $0.groupId.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 }
        )
        let groupId = createResponse.groupId.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !groupId.isEmpty else { throw GroupError.serverReturnedEmptyId }
        Self.logger.info("Server assigned groupId: \(groupId)")

        // Step 2: Generate group encryption key (Android parity: 32 random bytes → hex).
        let groupKey = groupRepo.generateGroupKey()

        // Step 3: Construct invite string.
        guard let inviteString = groupRepo.constructInviteString(
            groupId: groupId,
            title: title,
            encryptKey: groupKey,
            description: description
        ) else {
            throw GroupError.failedToConstructInvite
        }

        // Step 4: Encrypt invite with private key for server storage (sync to other devices).
        guard let encryptedGroupString = try? CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data(inviteString.utf8),
            password: privateKeyHex
        ) else {
            throw GroupError.failedToEncryptInvite
        }

        // Step 5: Join the group we just created.
        var joinPacket = PacketGroupJoin()
        joinPacket.groupId = groupId
        joinPacket.status = .notJoined
        joinPacket.groupString = encryptedGroupString

        let joinResponse: PacketGroupJoin = try await PacketAwaiter.send(
            joinPacket,
            awaitResponse: 0x14,
            where: { $0.groupId == groupId }
        )

        guard joinResponse.status == .joined else {
            throw GroupError.joinRejected(joinResponse.status)
        }

        // Step 6: Persist group locally.
        let dialogKey = groupRepo.persistGroup(
            account: account,
            privateKeyHex: privateKeyHex,
            groupId: groupId,
            title: title,
            description: description,
            encryptKey: groupKey
        ) ?? groupRepo.toGroupDialogKey(groupId)

        // Step 7: Ensure dialog exists in chat list.
        dialogRepo.ensureDialog(
            opponentKey: dialogKey,
            title: title,
            username: description,
            myPublicKey: account
        )

        // Step 8: Insert service message (Desktop parity: $a=Group created).
        MessageRepository.shared.insertLocalServiceMessage(
            text: "$a=Group created",
            dialogKey: dialogKey
        )
        dialogRepo.updateDialogFromMessages(opponentKey: dialogKey)

        Self.logger.info("Group created successfully: \(dialogKey)")
        return ChatRoute(groupDialogKey: dialogKey, title: title, description: description)
    }

    // MARK: - Join Group

    /// Joins an existing group from an invite string.
    func joinGroup(inviteString: String) async throws -> ChatRoute {
        let session = SessionManager.shared
        let account = session.currentPublicKey
        guard let privateKeyHex = session.privateKeyHex, !account.isEmpty else {
            throw GroupError.notAuthenticated
        }

        guard let parsed = groupRepo.parseInviteString(inviteString) else {
            throw GroupError.invalidInviteString
        }

        Self.logger.info("Joining group: \(parsed.groupId)")

        // Encrypt invite with private key for server sync.
        guard let encryptedGroupString = try? CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data(inviteString.utf8),
            password: privateKeyHex
        ) else {
            throw GroupError.failedToEncryptInvite
        }

        var joinPacket = PacketGroupJoin()
        joinPacket.groupId = parsed.groupId
        joinPacket.status = .notJoined
        joinPacket.groupString = encryptedGroupString

        let response: PacketGroupJoin = try await PacketAwaiter.send(
            joinPacket,
            awaitResponse: 0x14,
            where: { $0.groupId == parsed.groupId }
        )

        guard response.status == .joined else {
            throw GroupError.joinRejected(response.status)
        }

        let dialogKey = groupRepo.persistGroup(
            account: account,
            privateKeyHex: privateKeyHex,
            groupId: parsed.groupId,
            title: parsed.title,
            description: parsed.description,
            encryptKey: parsed.encryptKey
        ) ?? groupRepo.toGroupDialogKey(parsed.groupId)

        dialogRepo.ensureDialog(
            opponentKey: dialogKey,
            title: parsed.title,
            username: parsed.description,
            myPublicKey: account
        )

        // Insert service message (Desktop parity: $a=Group joined).
        MessageRepository.shared.insertLocalServiceMessage(
            text: "$a=Group joined",
            dialogKey: dialogKey
        )
        dialogRepo.updateDialogFromMessages(opponentKey: dialogKey)

        Self.logger.info("Joined group: \(dialogKey)")
        return ChatRoute(groupDialogKey: dialogKey, title: parsed.title, description: parsed.description)
    }

    // MARK: - Leave Group

    /// Leaves a group and deletes all local data.
    func leaveGroup(groupDialogKey: String) async throws {
        let session = SessionManager.shared
        let account = session.currentPublicKey
        guard !account.isEmpty else { throw GroupError.notAuthenticated }

        let groupId = groupRepo.normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return }

        Self.logger.info("Leaving group: \(groupId)")

        var leavePacket = PacketGroupLeave()
        leavePacket.groupId = groupId

        let _: PacketGroupLeave = try await PacketAwaiter.send(
            leavePacket,
            awaitResponse: 0x15,
            where: { $0.groupId == groupId }
        )

        // Delete local data.
        groupRepo.deleteGroup(account: account, groupDialogKey: groupDialogKey)
        dialogRepo.deleteDialog(opponentKey: groupDialogKey)

        Self.logger.info("Left group: \(groupId)")
    }

    // MARK: - Kick Member (Admin)

    /// Bans a member from the group (admin only — server validates).
    /// Server responds with updated PacketGroupInfo (0x12) after successful ban.
    func kickMember(groupDialogKey: String, memberPublicKey: String) async throws {
        let groupId = groupRepo.normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return }

        Self.logger.info("Kicking \(memberPublicKey.prefix(12)) from group \(groupId)")

        var banPacket = PacketGroupBan()
        banPacket.groupId = groupId
        banPacket.publicKey = memberPublicKey.trimmingCharacters(in: .whitespacesAndNewlines)

        let _: PacketGroupInfo = try await PacketAwaiter.send(
            banPacket,
            awaitResponse: 0x12,
            where: { $0.groupId == groupId }
        )
    }

    // MARK: - Request Members

    /// Fetches the member list for a group from the server.
    func requestMembers(groupDialogKey: String) async throws -> [String] {
        let groupId = groupRepo.normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return [] }

        var infoPacket = PacketGroupInfo()
        infoPacket.groupId = groupId

        let response: PacketGroupInfo = try await PacketAwaiter.send(
            infoPacket,
            awaitResponse: 0x12,
            where: { $0.groupId == groupId }
        )

        return response.members
    }

    // MARK: - Check Invite Status

    /// Checks if the current user can join a group (pre-join status check).
    func checkInviteStatus(groupId: String) async throws -> (memberCount: Int, status: GroupStatus) {
        let normalized = groupRepo.normalizeGroupId(groupId)
        guard !normalized.isEmpty else { return (0, .invalid) }

        var packet = PacketGroupInviteInfo()
        packet.groupId = normalized

        let response: PacketGroupInviteInfo = try await PacketAwaiter.send(
            packet,
            awaitResponse: 0x13,
            where: { $0.groupId == normalized }
        )

        return (response.membersCount, response.status)
    }

    // MARK: - Generate Invite String

    // MARK: - Create Group & Send Invites

    /// Creates a group and sends invite strings as DMs to the selected contacts.
    /// Invite delivery is best-effort — the group is created regardless of DM failures.
    func createGroupAndSendInvites(
        title: String,
        description: String = "",
        contactKeys: [String]
    ) async throws -> ChatRoute {
        let route = try await createGroup(title: title, description: description)

        // Only attempt invites if we have contacts and can generate the invite string
        guard !contactKeys.isEmpty,
              let inviteString = generateInviteString(groupDialogKey: route.publicKey)
        else {
            return route
        }

        // Send invite string as DM to each selected contact (fire-and-forget)
        for publicKey in contactKeys {
            do {
                try await SessionManager.shared.sendMessage(
                    text: inviteString,
                    toPublicKey: publicKey
                )
                Self.logger.info("Sent group invite to \(publicKey.prefix(8))...")
            } catch {
                Self.logger.error("Failed to send invite to \(publicKey.prefix(8))...: \(error)")
            }
        }

        return route
    }

    // MARK: - Generate Invite String

    /// Generates a shareable invite string for a group the user is admin of.
    func generateInviteString(groupDialogKey: String) -> String? {
        let session = SessionManager.shared
        let account = session.currentPublicKey
        guard let privateKeyHex = session.privateKeyHex, !account.isEmpty else { return nil }

        let groupId = groupRepo.normalizeGroupId(groupDialogKey)
        guard !groupId.isEmpty else { return nil }

        guard let groupKey = groupRepo.groupKey(
            account: account,
            privateKeyHex: privateKeyHex,
            groupDialogKey: groupDialogKey
        ) else { return nil }

        let metadata = groupRepo.groupMetadata(account: account, groupDialogKey: groupDialogKey)

        return groupRepo.constructInviteString(
            groupId: groupId,
            title: metadata?.title ?? "",
            encryptKey: groupKey,
            description: metadata?.description ?? ""
        )
    }
}
