import sodium from 'libsodium-wrappers'
import { v4 as uuidv4 } from 'uuid'
import { Index } from 'flexsearch'
import * as uint from '@/libs/uint'

export default class NotesAPI {
    constructor(adapter, encryptionKey = null) {
        if (!adapter) {
            throw new Error('NotesAPI requires a storage adapter')
        }

        this.adapter = adapter
        this.notesCache = new Map()
        this.encryptionKey = encryptionKey
        this._sodiumReady = false

        this.index = new Index({
            tokenize: 'forward',
        })
    }

    async _initSodium() {
        if (!this._sodiumReady) {
            await sodium.ready
            this._sodiumReady = true
        }
    }

    _encrypt(note) {
        if (!this.encryptionKey) {
            throw new Error('Encryption key not set')
        }

        const key = uint.hexToUint8Array(this.encryptionKey)
        if (key.length !== 32) {
            throw new Error(
                'Encryption key must be 64 hex characters (32 bytes)',
            )
        }

        const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
        const message = JSON.stringify(note)

        const ciphertext = sodium.crypto_secretbox_easy(
            new TextEncoder().encode(message),
            nonce,
            key,
        )

        const combined = uint.concatUint8Arrays(nonce, ciphertext)
        return uint.uint8ArrayToBase64(combined)
    }

    _decrypt(encryptedData) {
        if (!this.encryptionKey) {
            throw new Error('Encryption key not set')
        }

        const key = uint.hexToUint8Array(this.encryptionKey)
        if (key.length !== 32) {
            throw new Error(
                'Encryption key must be 64 hex characters (32 bytes)',
            )
        }

        let combined
        try {
            combined = uint.base64ToUint8Array(encryptedData)
        } catch (e) {
            throw new Error('Invalid encrypted data: not valid base64')
        }

        if (
            combined.length <
            sodium.crypto_secretbox_NONCEBYTES +
                sodium.crypto_secretbox_MACBYTES
        ) {
            throw new Error('Invalid encrypted data: too short')
        }

        const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES)
        const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES)

        let decrypted
        try {
            decrypted = sodium.crypto_secretbox_open_easy(
                ciphertext,
                nonce,
                key,
            )
        } catch (e) {
            throw new Error('Decryption failed: wrong key or corrupted data')
        }

        if (!decrypted) {
            throw new Error('Decryption failed: no data returned')
        }

        const decryptedStr = new TextDecoder().decode(decrypted)

        try {
            return JSON.parse(decryptedStr)
        } catch (e) {
            throw new Error(
                `Decryption succeeded but invalid JSON: ${decryptedStr}`,
            )
        }
    }

    async init() {
        await this._initSodium()
        await this.adapter.init()
        this.notesCache.clear()

        const encryptedNotes = await this.adapter.getAll()

        for (const encryptedNote of encryptedNotes) {
            try {
                const note = this._decrypt(encryptedNote.data || encryptedNote)

                this.notesCache.set(note.id, note)
                const searchText =
                    note.plainText || this._extractPlainText(note.content)
                this.index.add(note.id, note.title + '\n' + searchText)
            } catch (error) {
                console.error('Failed to decrypt note:', error)
            }
        }
    }

    _extractPlainText(content) {
        if (!content) return ''
        if (typeof content === 'string') return content

        const extractText = (node) => {
            if (typeof node === 'string') return node
            if (!node || !node.content) return ''
            return node.content.map(extractText).join(' ')
        }

        return extractText(content)
    }

    /* -----------------------
         Public API
        ------------------------*/
    getCategories() {
        const categories = new Set()

        for (const note of this.notesCache.values()) {
            if (note.category) {
                categories.add(note.category)
            }
        }

        return Array.from(categories).sort()
    }

    getCategoryNotes(categoryName = null) {
        return Array.from(this.notesCache.values())
            .filter((n) => n.category === categoryName)
            .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
            .map((n) => ({ ...n }))
    }

    getNote(id) {
        const note = this.notesCache.get(id)
        return note ? { ...note } : null
    }

    async createNote(metadata = {}, content = '', plainText = '') {
        const id = uuidv4()
        const now = new Date().toISOString()

        const note = {
            id,
            title: metadata.title || 'Untitled',
            category: metadata.category || null,
            createdAt: now,
            updatedAt: now,
            content,
            plainText,
        }

        const encryptedNote = {
            id: note.id,
            data: this._encrypt(note),
        }

        this.notesCache.set(id, note)
        this.index.add(id, note.title + '\n' + plainText)

        await this.adapter.create(encryptedNote)

        return note
    }

    async deleteNote(id) {
        await this.adapter.delete(id)

        this.notesCache.delete(id)
        this.index.remove(id)
    }

    async updateNote(id, updates = {}) {
        const note = this.notesCache.get(id)
        if (!note) throw new Error('Note not found')

        const allowedFields = ['title', 'category', 'content', 'plainText']

        for (const key of Object.keys(updates)) {
            if (!allowedFields.includes(key)) {
                throw new Error(`Invalid update field: ${key}`)
            }
        }

        const updatedNote = {
            ...note,
            ...updates,
            updatedAt: new Date().toISOString(),
        }

        const encryptedNote = {
            id: updatedNote.id,
            data: this._encrypt(updatedNote),
        }

        this.notesCache.set(id, updatedNote)

        const searchText =
            updatedNote.plainText || this._extractPlainText(updatedNote.content)

        this.index.update(id, updatedNote.title + '\n' + searchText)

        await this.adapter.update(encryptedNote)

        return updatedNote
    }

    search(query) {
        const ids = this.index.search(query, {
            limit: 50,
            suggest: true,
        })
        return ids.map((id) => this.notesCache.get(id))
    }
}
