import Foundation
import Observation
import os

// MARK: - AccountManager

/// Manages user accounts: creation, import, persistence, switching, and retrieval.
/// Desktop parity: `AccountProvider.tsx` with `allAccounts`, `loginedAccount`, `loginDiceAccount`.
///
/// Supports multiple accounts stored as `[Account]` in Keychain.
/// Active account tracked via UserDefaults key.
/// Migrates from legacy single-account format on first access.
@Observable
@MainActor
final class AccountManager {

    static let shared = AccountManager()

    /// The currently active account (selected for unlock/session).
    /// Desktop parity: `loginedAccount` in `AccountProvider.tsx`.
    private(set) var currentAccount: Account?

    /// All stored accounts (encrypted, not yet unlocked).
    /// Desktop parity: `allAccounts` in `AccountProvider.tsx`.
    private(set) var allAccounts: [Account] = []

    private let crypto = CryptoManager.shared
    private let keychain = KeychainManager.shared
    nonisolated private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "AccountManager")

    private init() {
        migrateFromSingleAccount()
        allAccounts = loadAllAccounts()

        // Restore active account from UserDefaults (desktop: localStorage.last_logined_account)
        let activeKey = UserDefaults.standard.string(forKey: Account.KeychainKey.activeAccountKey)
        currentAccount = allAccounts.first(where: { $0.publicKey == activeKey })
            ?? allAccounts.first
    }

    // MARK: - Account Creation

    /// Creates a new account from a BIP39 mnemonic and password.
    /// Derives secp256k1 key pair, encrypts private key and seed phrase, saves to Keychain.
    /// Desktop parity: `createAccount()` in `AccountProvider.tsx` — INSERT into accounts table.
    func createAccount(seedPhrase: [String], password: String) async throws -> Account {
        let account = try await Task.detached(priority: .userInitiated) { [crypto] in
            let (privateKey, publicKey) = try crypto.deriveKeyPair(from: seedPhrase)

            let privateKeyEncrypted = try crypto.encryptWithPassword(privateKey, password: password)
            let seedEncrypted = try crypto.encryptWithPassword(
                Data(seedPhrase.joined(separator: " ").utf8),
                password: password
            )

            return Account(
                publicKey: publicKey.hexString,
                privateKeyEncrypted: privateKeyEncrypted,
                seedPhraseEncrypted: seedEncrypted
            )
        }.value

        addAccount(account)
        setActiveAccount(publicKey: account.publicKey)
        return account
    }

    // MARK: - Account Import

    /// Imports an existing account from a BIP39 mnemonic and new password.
    func importAccount(seedPhrase: [String], password: String) async throws -> Account {
        // Import uses the same derivation as create — just different UI flow
        try await createAccount(seedPhrase: seedPhrase, password: password)
    }

    // MARK: - Account Unlock

    /// Decrypts and verifies the stored account with the given password.
    /// Returns `true` if the password is correct.
    func unlock(password: String) async throws -> Bool {
        guard let account = currentAccount else { return false }
        _ = try await Task.detached(priority: .userInitiated) { [crypto] in
            try crypto.decryptWithPassword(account.privateKeyEncrypted, password: password)
        }.value
        return true
    }

    /// Decrypts the private key and returns the raw hex string.
    /// Used to derive the handshake hash for server authentication.
    func decryptPrivateKey(password: String) async throws -> String {
        guard let account = currentAccount else {
            Self.logger.error("No account found for decryption")
            throw CryptoError.decryptionFailed
        }
        let expectedPublicKey = account.publicKey
        Self.logger.info("Attempting to decrypt private key, expected pubkey: \(expectedPublicKey.prefix(20))...")
        let privateKeyData = try await Task.detached(priority: .userInitiated) { [crypto] in
            let data = try crypto.decryptWithPassword(account.privateKeyEncrypted, password: password)
            Self.logger.info("Decrypted data length: \(data.count) bytes")
            guard data.count == 32 else {
                Self.logger.error("Wrong password: decrypted data is \(data.count) bytes, expected 32")
                throw CryptoError.decryptionFailed
            }
            let derivedPublicKey = try crypto.deriveCompressedPublicKey(from: data)
            Self.logger.info("Derived pubkey: \(derivedPublicKey.hexString.prefix(20))...")
            guard derivedPublicKey.hexString == expectedPublicKey else {
                Self.logger.error("Wrong password: derived pubkey doesn't match stored pubkey")
                throw CryptoError.decryptionFailed
            }
            Self.logger.info("Password validation passed")
            return data
        }.value
        return privateKeyData.hexString
    }

    // MARK: - Profile Update

    /// Updates the display name and username on the current account.
    func updateProfile(displayName: String?, username: String?) {
        guard var account = currentAccount else { return }
        if let displayName { account.displayName = displayName }
        if let username { account.username = username }
        currentAccount = account
        addAccount(account) // Updates in array + persists to Keychain
    }

    // MARK: - Multi-Account Management

    /// Adds an account to the stored array. Deduplicates by publicKey.
    /// Desktop parity: `INSERT INTO accounts` in `AccountProvider.tsx`.
    func addAccount(_ account: Account) {
        var accounts = loadAllAccounts()
        accounts.removeAll { $0.publicKey == account.publicKey }
        accounts.append(account)
        try? keychain.saveCodable(accounts, forKey: Account.KeychainKey.allAccounts)
        allAccounts = accounts

        // Update legacy key for backward compatibility
        if account.publicKey == activeAccountPublicKey {
            try? keychain.saveCodable(account, forKey: Account.KeychainKey.account)
        }
    }

    /// Sets which account is active (shown on unlock, used for session).
    /// Desktop parity: `selectAccountToLoginDice()` + `localStorage.last_logined_account`.
    func setActiveAccount(publicKey: String) {
        UserDefaults.standard.set(publicKey, forKey: Account.KeychainKey.activeAccountKey)
        currentAccount = allAccounts.first(where: { $0.publicKey == publicKey })

        // Update legacy key for backward compatibility
        if let account = currentAccount {
            try? keychain.saveCodable(account, forKey: Account.KeychainKey.account)
        }
    }

    /// The active account's public key from UserDefaults.
    var activeAccountPublicKey: String? {
        UserDefaults.standard.string(forKey: Account.KeychainKey.activeAccountKey)
    }

    /// Removes a specific account from the stored array.
    /// Desktop parity: `DELETE FROM accounts WHERE public_key = ?` — local only, no server packet.
    func removeAccount(publicKey: String) {
        var accounts = loadAllAccounts()
        accounts.removeAll { $0.publicKey == publicKey }
        try? keychain.saveCodable(accounts, forKey: Account.KeychainKey.allAccounts)
        allAccounts = accounts

        // If removing the active account, switch to the next available
        if activeAccountPublicKey == publicKey {
            if let next = accounts.first {
                setActiveAccount(publicKey: next.publicKey)
            } else {
                UserDefaults.standard.removeObject(forKey: Account.KeychainKey.activeAccountKey)
                currentAccount = nil
                try? keychain.delete(forKey: Account.KeychainKey.account)
            }
        }
    }

    // MARK: - Queries

    /// Whether any accounts are stored.
    var hasAccount: Bool {
        !allAccounts.isEmpty
    }

    /// Whether multiple accounts are stored (enables account picker on unlock).
    var hasMultipleAccounts: Bool {
        allAccounts.count > 1
    }

    // MARK: - Legacy Compatibility

    /// Saves account to both legacy and new format.
    func saveAccount(_ account: Account) throws {
        addAccount(account)
    }

    /// Deletes the current (active) account.
    /// Backward-compatible wrapper around `removeAccount(publicKey:)`.
    func deleteAccount() throws {
        guard let key = currentAccount?.publicKey else { return }
        removeAccount(publicKey: key)
    }

    // MARK: - Private

    private func loadAllAccounts() -> [Account] {
        (try? keychain.loadCodable([Account].self, forKey: Account.KeychainKey.allAccounts)) ?? []
    }

    /// One-time migration: if legacy `"currentAccount"` exists but `"allAccounts"` does not,
    /// move the single account into the array format.
    /// Desktop parity: desktop always used array in SQLite; iOS started with single entry.
    private func migrateFromSingleAccount() {
        // Already migrated
        if keychain.contains(key: Account.KeychainKey.allAccounts) { return }

        // Check for legacy single account
        guard let legacy = try? keychain.loadCodable(Account.self, forKey: Account.KeychainKey.account) else { return }

        Self.logger.info("Migrating single account to multi-account format")

        // Migrate to array format
        try? keychain.saveCodable([legacy], forKey: Account.KeychainKey.allAccounts)

        // Set as active account
        UserDefaults.standard.set(legacy.publicKey, forKey: Account.KeychainKey.activeAccountKey)
    }
}
