// deno-lint-ignore-file no-explicit-any
import { io, ManagerOptions, Socket, SocketOptions } from 'socket.io-client'
import crypto from 'node:crypto'
import { CallMethod, ClientEvent, ClientEventCallback } from './ApiDeclare.ts'
import ApiCallbackMessage from './ApiCallbackMessage.ts'
import { CallableMethodBeforeAuth, randomUUID } from "lingchair-internal-shared"
import CallbackError from "./CallbackError.ts"

import Message from "./Message.ts"

export default class LingChairClient {
    declare client: Socket
    declare access_token: string
    declare server_url: string
    declare device_id: string
    declare refresh_token?: string
    declare auto_fresh_token: boolean
    declare auth_cache: {
        refresh_token?: string,
        access_token?: string,
        account?: string,
        password?: string,
    }
    constructor(args: {
        server_url: string
        device_id: string,
        io?: Partial<ManagerOptions & SocketOptions>
        auto_fresh_token?: boolean
    }) {
        this.server_url = args.server_url
        this.auto_fresh_token = args.auto_fresh_token || false
        this.device_id = args.device_id
        this.client = io(args.server_url, {
            transports: ["polling", "websocket", "webtransport"],
            ...args.io,
            auth: {
                ...args.io?.auth,
                device_id: this.device_id,
                session_id: randomUUID(),
            },
        })
        this.client.on("The_White_Silk", (name: ClientEvent, data: any, _callback: (ret: unknown) => void) => {
            try {
                if (name == null || data == null) return
                for (const v of (this.events[name] || []))
                    v(({
                        "Client.onMessage": {
                            message: new Message(this, data.msg)
                        }
                    })[name])
            } catch (e) {
                console.error(e)
            }
        })
    }
    events: { [K in ClientEvent]?: ClientEventCallback<K>[] } = {}
    on<K extends ClientEvent>(eventName: K, func: ClientEventCallback<K>) {
        if (this.events[eventName] == null)
            this.events[eventName] = []
        if (this.events[eventName].indexOf(func) == -1)
            this.events[eventName].push(func)
    }
    off<K extends ClientEvent>(eventName: K, func: ClientEventCallback<K>) {
        if (this.events[eventName] == null)
            this.events[eventName] = []
        const index = this.events[eventName].indexOf(func)
        if (index != -1)
            this.events[eventName].splice(index, 1)
    }
    connect() {
        this.client.connect()
    }
    disconnect() {
        this.client.disconnect()
    }
    reconnect() {
        this.disconnect()
        this.connect()
    }
    invoke(method: CallMethod, args: object = {}, timeout: number = 10000): Promise<ApiCallbackMessage> {
        return new Promise((resolve) => {
            this.client!.timeout(timeout).emit("The_White_Silk", method, args, (err: Error, res: ApiCallbackMessage) => {
                // 错误处理
                if (err) return resolve({
                    code: -1,
                    msg: err.message,
                })
                if (CallableMethodBeforeAuth.indexOf(method) == -1 && res.code == 401 && this.auto_fresh_token) {
                    if (this.auth_cache)
                        this.auth(this.auth_cache).then((re) => {
                            if (!re) resolve(res)
                            this.invoke(method, args, timeout).then((re) => resolve(re))
                        })
                    else
                        resolve(res)
                } else
                    resolve(res)
            })
        })
    }
    /** 
     * 建议在 auth 返回 true 时调用
     */
    getCachedAccessToken() {
        return this.access_token
    }
    /** 
     * 建议在 auth 返回 true 时调用
     */
    getCachedRefreshToken() {
        return this.refresh_token
    }
    /** 
     * 客户端上线
     * 
     * 使用验证方式优先级: 访问 > 刷新 > 账号密码
     * 
     * 不会逐一尝试
     */
    async auth(args: {
        refresh_token?: string,
        access_token?: string,
        account?: string,
        password?: string,
    }) {
        try {
            await this.authOrThrow(args)
            return true
        } catch (_) {
            return false
        }
    }
    /** 
     * 进行身份验证以接受客户端事件
     * 
     * 使用验证方式优先级: 访问 > 刷新 > 账号密码
     * 
     * 若传递了账号密码, 则同时缓存新的访问令牌和刷新令牌
     * 
     * 如只传递两个令牌的其一, 按照优先级并在成功验证后赋值
     * 
     * 多个验证方式不会逐一尝试
     */
    async authOrThrow(args: {
        refresh_token?: string
        access_token?: string
        account?: string
        password?: string
        ignore_all_empty?: boolean
    }) {
        if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password) && !args.ignore_all_empty)
            throw new Error('Access/Refresh token or account & password required, or ignore_all_empty=true')

        this.auth_cache = args

        let access_token = args.access_token
        if (!access_token && args.refresh_token) {
            const re = await this.invoke('User.refreshAccessToken', {
                refresh_token: args.refresh_token,
            })
            if (re.code == 200) {
                access_token = re.data!.access_token as string | undefined
                this.refresh_token = args.refresh_token
            } else
                throw new CallbackError(re)
        }

        if (!access_token && (args.account && args.password)) {
            const re = await this.invoke('User.login', {
                account: args.account,
                password: crypto.createHash('sha256').update(args.password).digest('hex'),
            })
            if (re.code == 200) {
                access_token = re.data!.access_token as string | undefined
                this.refresh_token = re.data!.refresh_token as string
            } else
                throw new CallbackError(re)
        }

        const re = await this.invoke('User.auth', {
            access_token: access_token
        })
        if (re.code == 200)
            this.access_token = access_token as string
        else
            throw new CallbackError(re)
    }
    getBaseHttpUrl() {
        const url = new URL(this.server_url)
        return (({
            'ws:': 'http:',
            'wss:': 'https:',
            'http:': 'http:',
            'https:': 'https:',
        })[url.protocol] || 'http:') + '//' + url.host
    }
    getUrlForFileByHash(file_hash?: string, defaultUrl?: string) {
        return file_hash ? (this.getBaseHttpUrl() + '/uploaded_files/' + file_hash) : defaultUrl
    }
    async register(args: {
        nickname: string,
        username?: string,
        password: string,
    }) {
        try {
            return await this.registerOrThrow(args)
        } catch (_) {
            return null
        }
    }
    async registerOrThrow({
        nickname,
        username,
        password,
    }: {
        nickname: string,
        username?: string,
        password: string,
    }) {
        const re = await this.invoke('User.register', {
            nickname,
            username,
            password: crypto.createHash('sha256').update(password).digest('hex'),
        })
        if (re.code != 200)
            throw new CallbackError(re)
        return re.data!.user_id as string
    }
    async uploadFile({
        chatId,
        fileData,
        fileName,
    }: { fileName: string, fileData: ArrayBuffer | Blob | Response, chatId?: string }) {
        const form = new FormData()
        form.append("file",
            fileData instanceof ArrayBuffer
                ? new File([fileData], fileName, { type: 'application/octet-stream' })
                : (
                    fileData instanceof Blob ? fileData :
                        new File([await fileData.arrayBuffer()], fileName, { type: 'application/octet-stream' })
                )
        )
        form.append('file_name', fileName)
        chatId && form.append('chat_id', chatId)

        const re = await fetch(this.getBaseHttpUrl() + '/upload_file', {
            method: 'POST',
            headers: {
                "Token": this.access_token,
                "Device-Id": this.device_id,
            } as HeadersInit,
            body: form,
            credentials: 'omit',
        })
        const text = await (await re.blob()).text()
        let json
        try {
            json = JSON.parse(text)
            // deno-lint-ignore no-empty
        } catch (_) { }
        if (!re.ok) throw new CallbackError({
            ...(json == null ? {
                msg: text
            } : json),
            code: re.status,
        } as ApiCallbackMessage)
        return json.data.file_hash as string
    }
}
