import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import {
    getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
    openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
} from '../utils/index.js'
import { redis } from '../client.js'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'

// Define interface for model response to improve type safety
interface ModelResponse {
    status: 'success' | 'error'
    reply: string
    metadata?: {
        timestamp: string
        self_sentiment: number
        user_sentiment: { [userId: string]: number }
        redis_ops: Array<{ action: 'set' | 'get'; key: string; value?: number }>
        need_help: boolean
    }
}

/**
 * Max Message length for free users is 2000 characters (bot or not).
 * Bot supports infinite lengths for normal messages.
 *
 * @param message the message received from the channel
 */
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => {
    const clientId = client.user!.id
    let cleanedMessage = clean(message.content, clientId)
    log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`)

    // Check if message mentions the bot or passes random chance (30%)
    const isFromBot = message.author.bot && message.author.id !== clientId;
    const isMentioned = message.mentions.has(clientId)
    const isCommand = message.content.startsWith('/')

    if (isFromBot) {
        // Check interaction key to prevent rapid back-and-forth
        const otherBotId = message.author.id
        const interactionKey = `bot_interaction:${[clientId, otherBotId].sort().join(':')}`
        const interactionExists = await redis.exists(interactionKey)
        if (interactionExists) {
            log('Interaction cooldown active, not responding')
            return
        }
    
        // Determine probability
        let respondProbability = 0
        if (isMentioned) {
            respondProbability = 0.9  // 90% chance if mentioned
        } else {
            respondProbability = 0.2  // 20% chance if not mentioned
        }
    
        const shouldRespond = Math.random() < respondProbability
        if (!shouldRespond) return
    
        // Set interaction key with 60s expiration
        await redis.set(interactionKey, '1', 'EX', 60)
    } else if (!message.author.bot) {
        // Human message
        const randomChance = Math.random() < 0.30
        if (!isMentioned && (isCommand || !randomChance)) return
    } else {
        // Message from self: ignore
        return
    }

    // Log response trigger
    log(isMentioned ? 'Responding to mention' : 'Responding due to random chance')

    // Log response trigger
    log(isFromBot ? 'Responding to bot message' : (isMentioned ? 'Responding to mention' : 'Responding due to random chance'))
    
    // Load and process bot’s own history
    const historyFile = `${message.channelId}-${client.user.username}.json`

    // Default stream to false
    let shouldStream = false

    // Params for Preferences Fetching
    const maxRetries = 3
    const delay = 1000 // in milliseconds

    try {
        // Retrieve Server/Guild Preferences
        let attempt = 0
        while (attempt < maxRetries) {
            try {
                await new Promise((resolve, reject) => {
                    getServerConfig(`${message.guildId}-config.json`, (config) => {
                        if (config === undefined) {
                            openConfig(`${message.guildId}-config.json`, 'toggle-chat', true)
                            reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...'))
                        } else if (!config.options['toggle-chat']) {
                            reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).'))
                        } else {
                            resolve(config)
                        }
                    })
                })
                break
            } catch (error) {
                ++attempt
                if (attempt < maxRetries) {
                    log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`)
                    await new Promise(ret => setTimeout(ret, delay))
                } else {
                    throw new Error(`Could not retrieve Server Preferences, please try chatting again...`)
                }
            }
        }

        // Retrieve User Preferences
        attempt = 0
        let userConfig: UserConfig | undefined
        while (attempt < maxRetries) {
            try {
                userConfig = await new Promise((resolve, reject) => {
                    getUserConfig(`${message.author.username}-config.json`, (config) => {
                        if (config === undefined) {
                            openConfig(`${message.author.username}-config.json`, 'message-style', false)
                            openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel)
                            reject(new Error('No User Preferences is set up.\n\nCreating preferences file with `message-style` set as `false` for regular message style.\nPlease try chatting again.'))
                            return
                        }

                        if (typeof config.options['modify-capacity'] === 'number') {
                            log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`)
                            msgHist.capacity = config.options['modify-capacity']
                        } else {
                            log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`)
                        }

                        shouldStream = config.options['message-stream'] as boolean || false

                        if (typeof config.options['switch-model'] !== 'string') {
                            reject(new Error(`No Model was set. Please set a model by running \`/switch-model <model of choice>\`.\n\nIf you do not have any models. Run \`/pull-model <model name>\`.`))
                        }

                        resolve(config)
                    })
                })
                break
            } catch (error) {
                ++attempt
                if (attempt < maxRetries) {
                    log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`)
                    await new Promise(ret => setTimeout(ret, delay))
                } else {
                    throw new Error(`Could not retrieve User Preferences, please try chatting again...`)
                }
            }
        }

        // Retrieve Channel Messages
        let chatMessages: UserMessage[] = await new Promise((resolve) => {
            getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
                if (channelInfo?.messages) {
                    resolve(channelInfo.messages)
                } else {
                    log(`Channel/Thread ${message.channelId}-${message.author.username} does not exist. File will be created shortly...`)
                    resolve([])
                }
            })
        })

        if (chatMessages.length === 0) {
            chatMessages = await new Promise((resolve, reject) => {
                openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag)
                getChannelInfo(`${message.channelId}-${message.author.username}.json`, (config) => {
                    if (config?.messages) {
                        resolve(config.messages)
                    } else {
                        reject(new Error(`Failed to find ${message.author.username}'s history. Try chatting again.`))
                    }
                })
            })
        }

        if (!userConfig) {
            throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`)
        }

        // Get message attachment if exists
        const attachment = message.attachments.first()
        let messageAttachment: string[] = []
        if (attachment && attachment.name?.endsWith(".txt")) {
            cleanedMessage += await getTextFileAttachmentData(attachment)
        } else if (attachment) {
            messageAttachment = await getAttachmentData(attachment)
        }

        const model: string = userConfig.options['switch-model']

        // Load personality
        let personality: string
        try {
            const __filename = fileURLToPath(import.meta.url)
            const __dirname = path.dirname(__filename)
            const personalityPath = path.join(__dirname, '../../src/personality.json')
            const personalityData = await fs.readFile(personalityPath, 'utf-8')
            const personalityJson = JSON.parse(personalityData)
            personality = personalityJson.character || 'You are a friendly and helpful AI assistant.'
        } catch (error) {
            log(`Failed to load personality.json: ${error}`)
            personality = 'You are a friendly and helpful AI assistant.'
        }

        // Get user and bot sentiment from Redis
        const userSentimentKey = `user:${message.author.id}:sentiment`
        const botSentimentKey = `bot:self_sentiment`
        let userSentiment: number
        let botSentiment: number

        try {
            const userSentimentRaw = await redis.get(userSentimentKey)
            userSentiment = parseFloat(userSentimentRaw || '0.50')
            if (isNaN(userSentiment) || userSentiment < 0 || userSentiment > 1) { 
                log(`Invalid user sentiment for ${message.author.id}: ${userSentimentRaw}. Using default 0.50.`)
                userSentiment = 0.50
                await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`))
            }
        } catch (error) {
            log(`Failed to get user sentiment from Redis: ${error}`)
            userSentiment = 0.50
            await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`))
        }

        try {
            const botSentimentRaw = await redis.get(botSentimentKey)
            botSentiment = parseFloat(botSentimentRaw || '0.50')
            if (isNaN(botSentiment) || botSentiment < 0 || botSentiment > 1) {
                log(`Invalid bot sentiment for ${botSentimentKey}: ${botSentimentRaw}. Using default 0.50.`)
                botSentiment = 0.50
                await redis.set(botSentimentKey, '0.50')
            }
        } catch (error) {
            log(`Failed to get bot sentiment from Redis: ${error}`)
            botSentiment = 0.50
            await redis.set(botSentimentKey, '0.50')
        }
        
        // Construct sentiment data with bot ID
        const sentimentData = `User ${message.author.id} sentiment: ${userSentiment.toFixed(2)}, Bot ${clientId} sentiment: ${botSentiment.toFixed(2)}`

        // Log initial sentiments with two decimals
        log(`Initial sentiments - User ${message.author.id}: ${userSentiment.toFixed(2)}, Bot: ${botSentiment.toFixed(2)}`)

        // Construct sentiment data for prompt
        //const sentimentData = `User ${message.author.id} sentiment: ${userSentiment.toFixed(2)}, Bot sentiment: ${botSentiment.toFixed(2)}`

        // Construct prompt with [CHARACTER] and [SENTIMENT]
        const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[USER_INPUT]\n${cleanedMessage}`

        // Set up message history queue
        msgHist.setQueue(chatMessages)
        while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()

        // Add user message to history
        msgHist.enqueue({
            role: 'user',
            content: cleanedMessage,
            images: messageAttachment || []
        })

        // Call Ollama
        const response = await ollama.chat({
            model,
            messages: [{ role: 'user', content: prompt }],
            stream: shouldStream
        })

        // Parse JSON response
        let jsonResponse: ModelResponse
        try {
            // Log raw response for debugging
            log(`Raw model response: ${response.message.content}`)
            // Strip Markdown code fences if present
            let content = response.message.content
            content = content.replace(/^```json\n|```$/g, '').trim()
            jsonResponse = JSON.parse(content)
            if (!jsonResponse.status || !jsonResponse.reply) {
                throw new Error('Missing status or reply in model response')
            }
        } catch (error) {
            log(`Failed to parse model response: ${error}`)
            message.reply('Sorry, I’m having trouble thinking right now. Try again?')
            msgHist.pop()
            return
        }

        if (jsonResponse.status === 'error') {
            message.reply(jsonResponse.reply)
            msgHist.pop()
            return
        }

        // Execute redis_ops
        if (jsonResponse.metadata?.redis_ops) {
            for (const op of jsonResponse.metadata.redis_ops) {
                try {
                    const key = op.key.replace('<bot_id>', clientId)
                    if (op.action === 'set' && op.value !== undefined) {
                        const value = parseFloat(op.value.toString())
                        if (isNaN(value) || value < 0 || value > 1) {
                            log(`Invalid sentiment value for ${key}: ${op.value}. Skipping.`)
                            continue
                        }
                        await redis.set(key, value.toFixed(2))
                        log(`Set ${key} to ${value.toFixed(2)}`)
                    } else if (op.action === 'get' && op.key) {
                        const value = await redis.get(key)
                        log(`Got ${key}: ${value}`)
                    } else {
                        log(`Invalid redis_op: ${JSON.stringify(op)}. Skipping.`)
                    }
                } catch (error) {
                    log(`Redis operation failed for ${op.key}: ${error}`)
                }
            }
        }

        // Log updated sentiments with two decimals
        if (jsonResponse.metadata) {
            log(`Updated sentiments - Self: ${(jsonResponse.metadata.self_sentiment || 0).toFixed(2)}, User ${message.author.id}: ${(jsonResponse.metadata.user_sentiment[message.author.id] || 0).toFixed(2)}`)
        }

        // Send reply to Discord
        const reply = jsonResponse.reply || 'Sorry, I didn’t get that. Can you try again?'
        await message.reply(reply)

        // Update message history
        while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
        msgHist.enqueue({
            role: 'assistant',
            content: reply,
            images: messageAttachment || []
        })

        // Save updated history
        openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, msgHist.getItems())
    } catch (error: any) {
        msgHist.pop()
        message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`)
    }
})
