pragma Singleton
pragma ComponentBehavior: Bound

import "root:/modules/common/functions/string_utils.js" as StringUtils
import "root:/modules/common"
import Quickshell;
import Quickshell.Io;
import Qt.labs.platform
import QtQuick;

Singleton {
    id: root

    readonly property string xdgConfigHome: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
    readonly property string interfaceRole: "interface"
    property Component aiMessageComponent: AiMessageData {}
    property var messages: []
    readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}

    // Model properties:
    // - name: Name of the model
    // - icon: Icon name of the model
    // - description: Description of the model
    // - endpoint: Endpoint of the model
    // - model: Model name of the model
    // - requires_key: Whether the model requires an API key
    // - key_id: The identifier of the API key. Use the same identifier for models that can be accessed with the same key.
    // - key_get_link: Link to get the API key
    property var models: {
        "gemini-2.0-flash": {
            "name": "Gemini 2.0 Flash",
            "icon": "google-gemini-symbolic",
            "description": "Online | Google's model",
            "homepage": "https://aistudio.google.com",
            "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
            "model": "gemini-2.0-flash",
            "requires_key": true,
            "key_id": "gemini",
            "key_get_link": "https://aistudio.google.com/app/apikey",
        },
    }
    property var modelList: Object.keys(root.models)
    property var currentModel: Object.keys(root.models)[0]

    Component.onCompleted: {
        setModel(currentModel, false); // Do necessary setup for model
        getOllamaModels.running = true
    }

    function guessModelLogo(model) {
        if (model.includes("llama")) return "ollama-symbolic";
        if (model.includes("gemma")) return "google-gemini-symbolic";
        if (model.includes("deepseek")) return "deepseek-symbolic";
        if (/^phi\d*:/i.test(model)) return "microsoft-symbolic";
        return "ollama-symbolic";
    }

    function guessModelName(model) {
        const replaced = model.replace(/-/g, ' ').replace(/:/g, ' ');
        let words = replaced.split(' ');
        words[words.length - 1] = words[words.length - 1].replace(/(\d+)b$/, (_, num) => `${num}B`)
        words = words.map((word) => {
            return (word.charAt(0).toUpperCase() + word.slice(1))
        });
        words[words.length - 1] = `[${words[words.length - 1]}]`; // Surround the last word with square brackets
        const result = words.join(' ');
        return result;
    }

    Process {
        id: getOllamaModels
        command: ["bash", "-c", `${xdgConfigHome}/quickshell/scripts/ai/show-installed-ollama-models.sh`.replace(/file:\/\//, "")]
        stdout: SplitParser {
            onRead: data => {
                try {
                    if (data.length === 0) return;
                    const dataJson = JSON.parse(data);
                    root.modelList = [...root.modelList, ...dataJson];
                    dataJson.forEach(model => {
                        root.models[model] = {
                            "name": guessModelName(model),
                            "icon": guessModelLogo(model),
                            "description": `Local Ollama model: ${model}`,
                            "homepage": `https://ollama.com/library/${model}`,
                            "endpoint": "http://localhost:11434/v1/chat/completions",
                            "model": model,
                        }
                    });

                    root.modelList = Object.keys(root.models);

                } catch (e) {
                    console.log("Could not fetch Ollama models:", e);
                }
            }
        }
    }

    function addMessage(message, role) {
        if (message.length === 0) return;
        const aiMessage = aiMessageComponent.createObject(root, {
            "role": role,
            "content": message,
            "thinking": false,
            "done": true,
        });
        root.messages = [...root.messages, aiMessage];
    }

    function setModel(model, feedback = true) {
        if (!model) model = ""
        model = model.toLowerCase()
        if (modelList.indexOf(model) !== -1) {
            currentModel = model
            if (feedback) root.addMessage("Model set to " + models[model].name, Ai.interfaceRole)
        } else {
            if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n- ") + modelList.join("\n- "), Ai.interfaceRole)
        }
        if (models[model].requires_key) {
            KeyringStorage.fetchKeyringData();
        } 
    }

    function setApiKey(key) {
        const model = models[currentModel];
        if (!model.requires_key) {
            root.addMessage(`${model.name} does not require an API key`, Ai.interfaceRole);
            return;
        }
        if (!key || key.length === 0) {
            root.addMessage(
                StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command<br/><br/>For {0}, you can grab one at:\n\n{1}'), 
                    models[currentModel].name, models[currentModel].key_get_link), 
                Ai.interfaceRole
            );
            return;
        }
        KeyringStorage.setNestedField(["apiKeys", model.key_id], key);
        root.addMessage("API key set for " + model.name, Ai.interfaceRole);
    }

    function printApiKey() {
        const model = models[currentModel];
        if (model.requires_key) {
            const key = root.apiKeys[model.key_id];
            if (key) {
                root.addMessage(StringUtils.format(qsTr("API key:\n\n`{0}`"), key), Ai.interfaceRole);
            } else {
                root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole);
            }
        } else {
            root.addMessage(`This model (${model.name}) does not require an API key`, Ai.interfaceRole);
        }
    }

    function clearMessages() {
        messages = [];
    }

    Process {
        id: requester
        property var baseCommand: ["bash", "-c"]
        property var message

        function makeRequest() {
            const model = models[currentModel];
            let endpoint = model.endpoint;

            /* Build request data and headers */
            let baseData = {
                "model": model.model,
                "messages": root.messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
                    return {
                        "role": message.role,
                        "content": message.content,
                    }
                }),
                "stream": true,
            };
            let data = model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
            
            let requestHeaders = {
                "Content-Type": "application/json",
            }

            /* Put API key in environment variable */
            if (model.requires_key) requester.environment = ({
                "API_KEY": root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : "",
            })
            
            /* Create message object for local storage */
            requester.message = root.aiMessageComponent.createObject(root, {
                "role": "assistant",
                "model": currentModel,
                "content": "",
                "thinking": true,
                "done": false,
            });
            root.messages = [...root.messages, requester.message];

            /* Build header string for curl */ 
            let headerString = Object.entries(requestHeaders)
                .filter(([k, v]) => v && v.length > 0)
                .map(([k, v]) => `-H '${k}: ${v}'`)
                .join(' ');

            // console.log("Request headers: ", JSON.stringify(requestHeaders));
            // console.log("Header string: ", headerString);

            /* Create command string */
            const requestCommandString = `curl --no-buffer '${endpoint}'`
                + ` ${headerString}`
                + ' -H "Authorization: Bearer ${API_KEY}"'
                + ` -d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
            // console.log("Request command: ", requestCommandString);
            requester.command = baseCommand.concat([requestCommandString]);
            requester.running = true
        }

        stdout: SplitParser {
            onRead: data => {
                if (data.length === 0) return;

                // Remove 'data: ' prefix if present and trim whitespace
                let cleanData = data.trim();
                if (cleanData.startsWith("data:")) {
                    cleanData = cleanData.slice(5).trim();
                }
                // console.log("Clean data: ", cleanData);
                if (!cleanData) return;

                if (requester.message.thinking) requester.message.thinking = false;
                try {
                    if (cleanData === "[DONE]") {
                        requester.message.done = true;
                        return;
                    }
                    const dataJson = JSON.parse(cleanData);
                    const newContent = 
                        (dataJson.message?.content) ?? // Ollama
                        (dataJson.choices[0]?.delta?.content) ?? // Normal 
                        (dataJson.choices[0]?.delta?.reasoning_content) // Deepseek thinking

                    requester.message.content += newContent;

                    if (dataJson.done) requester.message.done = true;
                } catch (e) {
                    requester.message.content += cleanData;
                }
            }
        }

        onExited: (exitCode, exitStatus) => {
            try { // to parse full response into json
                // console.log("Full response: ", requester.message.content + "]"); 
                const parsedResponse = JSON.parse(requester.message.content + "]");
                requester.message.content = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``;
            } catch (e) { 
                console.log("Could not parse response: ", e);
            }
        }
    }

    function sendUserMessage(message) {
        if (message.length === 0) return;

        const userMessage = aiMessageComponent.createObject(root, {
            "role": "user",
            "content": message,
            "thinking": false,
            "done": true,
        });
        root.messages = [...root.messages, userMessage];

        requester.makeRequest();
    }

}
