import { ForbiddenException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { Observable, interval } from 'rxjs'
import { ChatGPTAPI, ChatMessage } from '../chatapi'
import type { RequestProps } from './types'
import { chatReplyProcess } from './chatgpt'
import { In, Repository, MoreThanOrEqual, LessThanOrEqual, And } from 'typeorm'
import { ChatHistory } from './entities/chat.entity'
import { InjectRepository } from '@nestjs/typeorm'
import { TokenUsage } from './entities/token-usage.entity'
import { MembershipService } from '../membership/membership.service'
import { MemberType } from '../membership/entities/membership.entity'
import { get_encoding } from '@dqbd/tiktoken'
import { fetchSSE } from '../chatapi/fetch-sse'
import { HttpService } from '@nestjs/axios'
import * as types from '../chatapi/types'
import { SysConfigService } from '../sys-config/sys-config.service'
import { ChatPdfService } from '../chat-pdf/chat-pdf.service'
import { UsersService } from '../users/users.service'
import { Role } from '../model/role.enum'
import { startOfDay, endOfDay, addDays, format } from 'date-fns'

@Injectable()
export class ChatService {
    tokenizer = get_encoding('cl100k_base')
    constructor(
        @InjectRepository(ChatHistory)
        private readonly chatHistoryRepository: Repository<ChatHistory>,
        @InjectRepository(TokenUsage)
        private readonly tokenUsageRepository: Repository<TokenUsage>,
        private readonly membershipService: MembershipService,
        private readonly httpService: HttpService,
        private readonly sysConfigService: SysConfigService,
        private readonly chatPdfService: ChatPdfService,
        private readonly usersService: UsersService
    ) {}

    public chat(req, res): Observable<any> {
        res.setHeader('Content-Type', 'application/octet-stream')
        return new Observable((observer) => {
            const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
            let firstChunk = true
            chatReplyProcess({
                message: prompt,
                lastContext: options,
                process: (chat: ChatMessage) => {
                    // observer.next(
                    //     new MessageEvent('message', {
                    //         data: firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`
                    //     })
                    // )
                    observer.next(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
                    firstChunk = false
                },
                systemMessage,
                temperature,
                top_p
            })
                .then(() => {})
                .catch((error) => {
                    observer.error(error)
                })
                .finally(() => {
                    observer.complete()
                })
        })
    }

    public async chat1(req, res) {
        res.setHeader('Content-type', 'application/octet-stream')
        const defSysMsg = (await this.sysConfigService.findByName('system_message'))?.value
        const membership = await this.membershipService.getMembership(req.user.id)
        const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
        if (!membership) {
            throw new ForbiddenException('请先成为会员')
        }
        if (membership.memberType == MemberType.Trial && membership.tokenLeft <= 0) {
            throw new ForbiddenException('您的试用额度已用完，请升级会员')
        }
        if (membership.isExpired) {
            throw new ForbiddenException('您的会员已过期，请即时续费')
        }
        try {
            const promptTime = new Date()
            let firstChunk = true
            const result = await chatReplyProcess({
                message: prompt,
                lastContext: options,
                process: (chat: ChatMessage) => {
                    res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
                    firstChunk = false
                },
                systemMessage: systemMessage || defSysMsg,
                temperature,
                top_p
            })
            let chatMessage = result.data as ChatMessage

            this.chatHistoryRepository
                .save(
                    new ChatHistory({
                        messageId: chatMessage.parentMessageId,
                        parentMessageId: options.parentMessageId,
                        userId: req.user.id,
                        message: prompt,
                        role: 'user',
                        token: chatMessage.detail.usage.prompt_tokens,
                        time: promptTime
                    })
                )
                .catch((e) => {
                    Logger.error(e, 'SAVE CHAT HISTORY')
                })
            this.chatHistoryRepository
                .save(
                    new ChatHistory({
                        messageId: chatMessage.id,
                        parentMessageId: chatMessage.parentMessageId,
                        userId: req.user.id,
                        message: chatMessage.text,
                        role: 'assistant',
                        token: chatMessage.detail.usage.completion_tokens,
                        time: new Date()
                    })
                )
                .catch((e) => {
                    Logger.error(e, 'SAVE CHAT HISTORY')
                })
            this.saveUsage(req.user.id, chatMessage.detail.usage.total_tokens)
        } catch (error) {
            res.write(JSON.stringify(error))
        } finally {
            res.end()
        }
    }

    public async sendMessage(prompt: string, parentMessageId: string, message: string): Promise<ChatHistory> {
        const options = { parentMessageId: parentMessageId }
        console.log('options:' + options.parentMessageId)
        try {
            const result = await chatReplyProcess({
                message: prompt,
                lastContext: options,
                systemMessage: message,
                process: (chat: ChatMessage) => {}
            })
            const chatMessage = result.data as ChatMessage
            this.chatHistoryRepository.save(
                new ChatHistory({
                    messageId: chatMessage.parentMessageId,
                    parentMessageId: null,
                    userId: 0,
                    message: message,
                    role: 'system',
                    token: chatMessage.detail.usage.prompt_tokens,
                    time: new Date()
                })
            )
            const chatHistory = this.chatHistoryRepository.save(
                new ChatHistory({
                    messageId: chatMessage.id,
                    parentMessageId: chatMessage.parentMessageId,
                    userId: 0,
                    message: chatMessage.text,
                    role: 'assistant',
                    token: chatMessage.detail.usage.completion_tokens,
                    time: new Date()
                })
            )

            Logger.log(`机器人回答：${chatMessage.text}`, 'SendMessage')
            return chatHistory
        } catch (error) {
            Logger.error(error, 'SendMessage')
        }
    }

    public async chatProxy(req) {
        const url = `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${process.env.AZURE_OPENAI_VERSION}`
        req.body.stream = false
        try {
            const { data } = await this.httpService.axiosRef.post(url, req.body, {
                headers: {
                    'Content-Type': 'application/json',
                    'api-key': `${process.env.AZURE_OPENAI_KEY}`
                }
            })
            this.saveUsage(req.user.id, data.usage.total_tokens, false)
            return data
        } catch (e) {
            throw new InternalServerErrorException(e.response.data)
        }
    }

    public streamChatProxy(req) {
        const url = `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${process.env.AZURE_OPENAI_VERSION}`
        req.body.stream = true
        return new Observable((subscriber) => {
            let text = ''
            fetchSSE(url, {
                body: JSON.stringify(req.body),
                headers: {
                    'Content-Type': 'application/json',
                    'api-key': `${process.env.AZURE_OPENAI_KEY}`
                },
                method: 'POST',
                onMessage: (msg: string) => {
                    subscriber.next(msg)
                    if ('[DONE]' === msg) {
                        Logger.log('done', 'CHAT PROXY')
                        this.tiktokenAndSave(
                            req.user.id,
                            req.body.messages.map((message) => `${message.role}:\n${message.content}`).join('\n\n') +
                                '\n\nassistant:\n' +
                                text
                        )
                        return subscriber.complete()
                    }
                    const response: types.openai.CreateChatCompletionDeltaResponse = JSON.parse(msg)
                    if (response.choices?.length) {
                        const delta = response.choices[0].delta
                        if (delta?.content) text += delta.content
                    }
                },
                onError: (err) => {
                    Logger.error(err, 'CHAT PROXY')
                    subscriber.error(err)
                    subscriber.complete()
                }
            }).catch((e) => {
                Logger.error(e, 'CHAT PROXY')
                subscriber.error(e)
            })
        })
    }

    public async tiktokenAndSave(userId: number, text: string) {
        // TODO: use a better fix in the tokenizer
        text = text.replace(/<\|endoftext\|>/g, '')
        const tokenizer = get_encoding('cl100k_base')
        const token = tokenizer.encode(text).length
        await this.saveUsage(userId, token, false)
    }

    public async saveUsage(userId: number, usage: number, member = true) {
        const date = format(new Date(), 'yyyy-MM-dd')
        const tokenUsage = await this.tokenUsageRepository.findOneBy({
            userId,
            date
        })
        if (tokenUsage) {
            tokenUsage.usage += usage
            await this.tokenUsageRepository.save(tokenUsage)
        } else {
            await this.tokenUsageRepository.save(new TokenUsage({ userId, usage, date }))
        }

        if (member) {
            this.membershipService.saveUsage(userId, usage)
        }
    }

    public async getUsage(userId: number, start?: string, end?: string): Promise<TokenUsage[]> {
        const date = format(new Date(), 'yyyy-MM-dd')
        return await this.tokenUsageRepository.findBy({
            userId,
            date: And(MoreThanOrEqual(start || '2023-04-01'), LessThanOrEqual(end || date))
        })
    }

    public async getCount(apiUserId: number, roles?: Role, date?: string) {
        const userIds = await this.usersService.getUsers(apiUserId, roles)

        return this.chatHistoryRepository.countBy({
            userId: In(userIds),
            role: 'user',
            time: date
                ? And(MoreThanOrEqual(startOfDay(new Date(date))), LessThanOrEqual(endOfDay(new Date(date))))
                : undefined
        })
    }

    public async getWeekCount(apiUserId: number) {
        const date1 = new Date()
        let dayInfo = {}
        for (let i = 0; i < 7; i++) {
            let date2 = format(addDays(date1, 0 - i), 'yyyy-MM-dd')
            let api = await this.getCount(apiUserId, Role.Api, date2)
            let user = await this.getCount(apiUserId, Role.User, date2)
            dayInfo[format(addDays(date1, 0 - i), 'MM-dd')] = {
                api: api,
                user: user
            }
        }

        return dayInfo
    }
}
