/*
 * Topic router 
 * Created by: Michael Pastushkov <michael@pastushkov.com>
 */

const utils = require('../utils/utils');
class TopicRouter {

    constructor({ embedModel = 'nomic-embed-text', labelModel = 'llama3:8b', threshold = 0.76 } = {}) {
        this.embedModel = embedModel;     // ollama pull nomic-embed-text
        this.labelModel = labelModel;     // used only to mint a short label
        this.threshold = threshold;       // similarity to attach to existing topic
        this.topics = new Map();          // key -> { centroid: number[], count: number, summary: '' }
        this._counter = 1;                // fallback id
    }

    getCentroid(key) {
        const t = this.topics.get(key);
        return t ? t.centroid : null;
    }

    async _mintLabelWithLLM(text) {
        // Single short key, language-agnostic (works for RU/EN/etc.)
        utils.log('_mintLabelWithLLM', 5);
        utils.log(`model: ${this.labelModel}`, 5);
        const sys = `Return a short topic key (1–3 words, lowercase, hyphenated). Only the key.`;
        const r = await fetch('http://127.0.0.1:11434/api/chat', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                model: this.labelModel,
                messages: [{ role: 'system', content: sys }, { role: 'user', content: text }],
                stream: false,
                // options: { // limited results with these, and no performance gains
                //     num_ctx: 1024,
                //     num_predict: 256,
                //     num_thread: 4,
                //     num_batch: 1024
                // }
            })
        });
        if (!r.ok)
            return null;
        const j = await r.json();
        const raw = (j.message?.content || '').trim().toLowerCase();
        const key = raw.replace(/[^a-z0-9\u0400-\u04FF\- ]/g, '').replace(/\s+/g, '-').slice(0, 48);
        utils.log('_mintLabelWithLLM done', 5);
        return key || null;
    }

    async similarityTo(topicKey, text) {
        const t = this.topics.get(topicKey);
        if (!t) return null;
        const v = await this.embed(text);
        return this.cos(v, t.centroid);
    }

    /**
     * Resolve prompt to a topic.
     * Returns { key, bestSim, isNew } and updates centroid if matched.
     */
    async resolve(text) {
        const v = await this.embed(text);

        let bestKey = null, bestSim = -1;
        for (const [key, t] of this.topics) {
            const sim = this.cos(v, t.centroid);
            if (sim > bestSim) { 
                bestSim = sim; 
                bestKey = key; 
            }
        }

        if (bestSim >= this.threshold && bestKey) {
            const t = this.topics.get(bestKey);
            const n = t.count + 1;
            t.centroid = t.centroid.map((c, i) => (c * t.count + v[i]) / n);
            t.count = n;
            return { key: bestKey, bestSim, isNew: false };
        }

        // New topic: mint a label via LLM; fallback to generic id
        let label = await this._mintLabelWithLLM(text);
        if (!label) label = `topic-${String(this._counter++).padStart(3, '0')}`;

        let unique = label, i = 2;
        while (this.topics.has(unique)) unique = `${label}-${i++}`;

        this.topics.set(unique, { centroid: v, count: 1, summary: '' });
        return { key: unique, bestSim, isNew: true };
    }

    async embed(text) {
        utils.log(`Embed ${text}`, 5);
        utils.log(`model: ${this.embedModel}`, 5);
        const r = await fetch('http://127.0.0.1:11434/api/embeddings', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ 
                model: this.embedModel, 
                prompt: text ,
            // options: { // limited results with these, and no performance gains
                    // num_ctx: 1024,
                    // num_predict: 256,
                    // num_thread: 4,
                    // num_batch: 1024
            // }
            })
        });
        utils.log('Embed done', 5);
        if (!r.ok)
            throw new Error(await r.text());
        const { embedding } = await r.json();
        return embedding;
    }

    cos(a, b) {
        let d = 0, na = 0, nb = 0;
        for (let i = 0; i < a.length; i++) {
            d += a[i] * b[i];
            na += a[i] * a[i];
            nb += b[i] * b[i]
        }
        return d / (Math.sqrt(na) * Math.sqrt(nb));
    }
}

module.exports = TopicRouter;

/* Testing * /

const router = new TopicRouter();
(async () => {
  const t1 = await router.resolve('Tell me how many cat breeds are there in the universe?');      // → cats / cats-...
  console.log(t1);
   const t2 = await router.resolve('Name major ones');                      // → same topic as t1
  console.log(t2);
  const t3 = await router.resolve('Configure Nginx reverse proxy');        // → nginx-...
  console.log(t3);
})();

/**/
