import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Org } from './entities/org.entity'
import { Repository } from 'typeorm'
import { PageRequest } from '../common/dto/page-request'
import { Pagination, paginate } from 'nestjs-typeorm-paginate'
import { OrgUser } from './entities/org-user.entity'
import { UsersService } from 'src/users/users.service'
import * as randomstring from 'randomstring'
import { Role } from '../model/role.enum'
import { KnowledgeBaseService } from 'src/knowledge-base/knowledge-base.service'
import { OpenAIApi, Configuration } from 'azure-openai'
import * as dedent from 'dedent'
import { chatReplyProcess } from '../chat/chatgpt'
import { fetchSSE } from '../chatapi/fetch-sse'
import { v4 as uuidv4 } from 'uuid'
import Redis from 'ioredis'

@Injectable()
export class OrgService {
    private readonly openai: OpenAIApi
    private readonly redis: Redis
    constructor(
        @InjectRepository(Org)
        private readonly orgRepository: Repository<Org>,
        @InjectRepository(OrgUser)
        private readonly orgUserRepository: Repository<OrgUser>,
        private readonly userService: UsersService,
        private readonly knowledgeService: KnowledgeBaseService
    ) {
        this.openai = new OpenAIApi(
            new Configuration({
                apiKey: process.env.AZURE_OPENAI_KEY,
                // add azure info into configuration
                azure: {
                    apiKey: process.env.AZURE_OPENAI_KEY,
                    endpoint: process.env.AZURE_OPENAI_ENDPOINT,
                    deploymentName: 'gpt-35-turbo-16k'
                }
            })
        )
        this.redis = new Redis(process.env.REDIS_URI)
    }

    async findByUrl(url: string): Promise<Org> {
        const match = /^(\w+)\.org\.gpt\.izouma\.com$/.exec(url)
        if (match) {
            const subdomain = match[1]
            return await this.orgRepository.findOneOrFail({
                where: {
                    subdomain
                }
            })
        } else {
            return await this.orgRepository.findOneOrFail({
                where: {
                    customDomain: url
                }
            })
        }
    }

    async findById(orgId: number): Promise<Org> {
        return await this.orgRepository.findOneOrFail({
            where: {
                id: orgId
            }
        })
    }

    async findAll(req: PageRequest<Org>): Promise<Pagination<Org>> {
        return await paginate<Org>(this.orgRepository, req.page, req.search)
    }

    async create(org: Org): Promise<Org> {
        return await this.orgRepository.save(org)
    }

    async update(org: Org): Promise<Org> {
        return await this.orgRepository.save(org)
    }

    async findAllUsers(req: PageRequest<OrgUser>): Promise<Pagination<OrgUser>> {
        return await paginate<OrgUser>(this.orgUserRepository, req.page, req.search)
    }

    async addUsers(orgUsers: OrgUser[]): Promise<void> {
        const users = await this.userService.findByPhone(orgUsers.map((x) => x.phone))
        for (let x of orgUsers) {
            let user = users.find((y) => y.phone === x.phone)
            if (!user) {
                const name = '0x' + randomstring.generate({ length: 8, charset: 'alphanumeric' })
                user = await this.userService.create({
                    name: name,
                    username: name,
                    phone: x.phone,
                    roles: [Role.User]
                })
            }
            await this.userService.updateUser(user.id, { orgId: x.orgId })
            x.userId = user.id
            await this.orgUserRepository.save(x)
        }
    }

    async removeUser(orgUserId): Promise<void> {
        const orgUser = await this.orgUserRepository.findOneOrFail({ where: { id: orgUserId } })
        await this.orgUserRepository.delete(orgUserId)
        await this.userService.updateUser(orgUser.userId, { orgId: null })
    }

    async buildMessages(
        question: string,
        orgId: number,
        parentMessageId?: string,
        knowledgeId?: number,
        fileId?: number
    ): Promise<any[]> {
        const org = await this.findById(orgId)
        const context = await this.knowledgeService.askKnowledge(question, orgId, knowledgeId, fileId)
        if (org.questionTemplate) {
            question = org.questionTemplate.replace('${question}', question)
        }
        const messages = []
        if (org.systemPrompt) {
            messages.push({
                role: 'system',
                content: org.systemPrompt.replace('${context}', context.join('\n')).replace('${question}', question)
            })
        }
        if (parentMessageId) {
            let history = (await this.getChatHistory(parentMessageId)).map((i) => ({
                role: i.role,
                content: i.text
            }))
            messages.push(...history)
        }
        if (!/\$\{context\}/.test(org.systemPrompt)) {
            messages.push({
                role: 'user',
                content: (
                    org.contextTemplate ||
                    dedent`You are a helpful AI article assistant. 
                    The following are the relevant article content fragments found from the article. 
                    The relevance is sorted from high to low. 
                    You can only answer according to the following content:
                    \`\`\`
                    ${context}
                    \`\`\`
                    You need to carefully consider your answer to ensure that it is based on the context. 
                    If the context does not mention the content or it is uncertain whether it is correct, 
                    please answer "Current context cannot provide effective information.
                    You must use Chinese to respond.`
                )
                    .replace('${context}', context.join('\n'))
                    .replace('${question}', question)
            })
        }
        if (!/\$\{question\}/.test('' + org.systemPrompt + org.contextTemplate)) {
            messages.push({
                role: 'user',
                content: question
            })
        }
        return messages
    }

    async getChatHistory(parentMessageId, history = []) {
        let parent: any = await this.redis.get(parentMessageId)
        if (!parent) {
            return history
        }
        parent = JSON.parse(parent)
        history.unshift(parent)
        if (parent.parentMessageId) {
            return await this.getChatHistory(parent.parentMessageId, history)
        }
        return history
    }

    async ask(question: string, orgId: number, parentMessageId?: string, knowledgeId?: number, fileId?: number) {
        try {
            const response = await this.openai.createChatCompletion({
                model: 'gpt-35-turbo-16k',
                messages: await this.buildMessages(question, orgId, parentMessageId, knowledgeId, fileId),
                temperature: 0.1,
            })
            const id = uuidv4()
            await this.redis.set(
                id,
                JSON.stringify({
                    role: 'user',
                    id,
                    parentMessageId,
                    text: question
                })
            )
            await this.redis.set(
                response.data.id,
                JSON.stringify({
                    role: 'assistant',
                    id: response.data.id,
                    parentMessageId: id,
                    text: response.data.choices[0].message.content
                })
            )
            return {
                role: 'assistant',
                id: response.data.id,
                text: response.data.choices[0].message.content,
                detail: response.data
            }
            // return { answer: response.data.choices[0].message.content }
        } catch (error) {
            Logger.error(error.message)
            if (error.response) {
                Logger.error(error.response.data)
            }
            throw new InternalServerErrorException(error.message)
        }
    }

    async streamAsk(
        req,
        res,
        question: string,
        orgId: number,
        parentMessageId?: string,
        knowledgeId?: number,
        fileId?: number
    ) {
        res.setHeader('Content-type', 'application/octet-stream')
        try {
            try {
                const url = `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${process.env.AZURE_OPENAI_VERSION}`
                let firstChunk = true
                const result: any = {
                    role: 'assistant',
                    id: uuidv4(),
                    text: ''
                }
                await fetchSSE(url, {
                    body: JSON.stringify({
                        messages: await this.buildMessages(question, orgId, parentMessageId, knowledgeId, fileId),
                        stream: true
                    }),
                    headers: {
                        'Content-Type': 'application/json',
                        'api-key': `${process.env.AZURE_OPENAI_KEY}`
                    },
                    method: 'POST',
                    onMessage: (msg: string) => {
                        if (msg === '[DONE]') return
                        const response = JSON.parse(msg)

                        result.id = response.id
                        const delta = response.choices[0].delta
                        result.delta = delta.content
                        if (delta?.content) result.text += delta.content

                        if (delta.role) {
                            result.role = delta.role
                        }

                        result.detail = response
                        res.write(firstChunk ? JSON.stringify(result) : `\n${JSON.stringify(result)}`)
                        firstChunk = false
                    },
                    onError: (err) => {
                        res.write(JSON.stringify(err))
                    }
                })

                // const response = await this.openai.createChatCompletion({
                //     model: 'gpt35',
                //     messages: [
                //         { role: 'system', content: org.systemPrompt },
                //         { role: 'user', content },
                //         { role: 'user', content: question }
                //     ]
                // })
                // return { answer: response.data.choices[0].message.content }
            } catch (error) {
                Logger.error(error.message)
                if (error.response) {
                    Logger.error(error.response.data)
                }
                throw new InternalServerErrorException(error.message)
            }
        } catch (e) {
            res.write(JSON.stringify(e))
        } finally {
            res.end()
        }
    }
}
