import { Index } from 'flexsearch'
import crypto from 'crypto'
import sodium from 'libsodium-wrappers'

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 || process.env.NOTES_ENCRYPTION_KEY
        this._sodiumReady = false

        this.index = new Index({
            tokenize: 'tolerant',
            resolution: 9,
        })
    }

    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 = Buffer.from(this.encryptionKey, 'hex')
        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(
            Buffer.from(message),
            nonce,
            key,
        )

        const combined = Buffer.concat([nonce, ciphertext])
        return combined.toString('base64')
    }

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

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

        let combined
        try {
            combined = Buffer.from(encryptedData, 'base64')
        } 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 = Buffer.from(decrypted).toString('utf8')

        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()

        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)
                this.index.add(note.id, note.title + '\n' + note.content)
            } catch (error) {
                console.error('Failed to decrypt note:', error)
            }
        }
    }

    /* -----------------------
         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) {
        return Array.from(this.notesCache.values())
            .filter((n) => n.category === categoryName)
            .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
    }

    getNote(id) {
        return this.notesCache.get(id) ?? null
    }

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

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

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

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

        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, content) {
        const note = this.notesCache.get(id)
        if (!note) throw new Error('Note not found')

        note.content = content
        note.updatedAt = new Date().toISOString()

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

        this.index.update(id, note.title + '\n' + content)

        await this.adapter.update(encryptedNote)

        return note
    }

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

        const allowedFields = ['title', 'category']
        for (const key of Object.keys(updates)) {
            if (!allowedFields.includes(key)) {
                throw new Error(`Invalid metadata field: ${key}`)
            }
        }

        if (updates.title !== undefined) {
            note.title = updates.title
        }

        if (updates.category !== undefined) {
            note.category = updates.category
        }

        note.updatedAt = new Date().toISOString()

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

        this.index.update(id, note.title + '\n' + note.content)

        await this.adapter.update(encryptedNote)

        return note
    }

    search(query) {
        const ids = this.index.search(query)
        return ids.map((id) => this.notesCache.get(id))
    }
}
