// BIG FAT WARNING: to avoid the complexity of npm, this typescript is compiled in the browser
// there's currently no static type checking

import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.0/lib/marked.esm.js'
const convElement = document.getElementById('conversation')

const promptInput = document.getElementById('prompt-input') as HTMLInputElement
const spinner = document.getElementById('spinner')

// stream the response and render messages as each chunk is received
// data is sent as newline-delimited JSON
async function onFetchResponse(response: Response): Promise<void> {
    let text = ''
    let decoder = new TextDecoder()
    if (response.ok) {
        const reader = response.body.getReader()
        while (true) {
            const { done, value } = await reader.read()
            if (done) {
                break
            }
            text += decoder.decode(value)
            addMessages(text)
            spinner.classList.remove('active')
        }
        addMessages(text)
        promptInput.disabled = false
        promptInput.focus()
    } else {
        const text = await response.text()
        console.error(`Unexpected response: ${response.status}`, { response, text })
        throw new Error(`Unexpected response: ${response.status}`)
    }
}

// The format of messages, this matches pydantic-ai both for brevity and understanding
// in production, you might not want to keep this format all the way to the frontend
interface Message {
    role: string
    content: string
    timestamp: string
}

// take raw response text and render messages into the `#conversation` element
// Message timestamp is assumed to be a unique identifier of a message, and is used to deduplicate
// hence you can send data about the same message multiple times, and it will be updated
// instead of creating a new message elements
function addMessages(responseText: string) {
    const lines = responseText.split('\n')
    const messages: Message[] = lines.filter(line => line.length > 1).map(j => JSON.parse(j))
    for (const message of messages) {
        // we use the timestamp as a crude element id
        const { timestamp, role, content } = message
        const id = `msg-${timestamp}`
        let msgDiv = document.getElementById(id)
        if (!msgDiv) {
            msgDiv = document.createElement('div')
            msgDiv.id = id
            msgDiv.title = `${role} at ${timestamp}`
            msgDiv.classList.add('border-top', 'pt-2', role)
            convElement.appendChild(msgDiv)
        }
        msgDiv.innerHTML = marked.parse(content)
    }
    window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
}

function onError(error: any) {
    console.error(error)
    document.getElementById('error').classList.remove('d-none')
    document.getElementById('spinner').classList.remove('active')
}

async function onSubmit(e: SubmitEvent): Promise<void> {
    e.preventDefault()
    spinner.classList.add('active')
    const body = new FormData(e.target as HTMLFormElement)

    promptInput.value = ''
    promptInput.disabled = true

    const response = await fetch('/chat/', { method: 'POST', body })
    await onFetchResponse(response)
}

// call onSubmit when the form is submitted (e.g. user clicks the send button or hits Enter)
document.querySelector('form').addEventListener('submit', (e) => onSubmit(e).catch(onError))

// load messages on page load
fetch('/chat/').then(onFetchResponse).catch(onError)