import http from '@ohos.net.http'
import buffer from '@ohos.buffer';
import {
    Emitter,
    getCurrentMP,
} from '@dcloudio/uni-runtime'
import {
    RequestTask as UniRequestTask,
    RequestOptions as UniRequestOptions,
    RequestSuccess as UniRequestSuccess,
    Request,
    RequestTaskOnChunkReceivedCallback,
    RequestTaskOnChunkReceivedListenerResult,
    RequestTaskOnHeadersReceivedCallback,
    RequestTaskOnHeadersReceivedListenerResult,
} from '../../interface.uts'
import {
    API_REQUEST,
    RequestApiOptions,
    RequestApiProtocol,
} from '../../protocol.uts'
import {
    getClientCertificate,
    parseUrl,
} from './utils.uts'
import {
    getCookieSync,
    setCookieSync,
} from './cookie.uts'

interface IUniRequestEmitter {
    on: (eventName: string, callback: Function) => void
    once: (eventName: string, callback: Function) => void
    off: (eventName: string, callback?: Function | null) => void
    emit: (eventName: string, ...args: (Object | undefined | null)[]) => void
}

interface IRequestTask {
    abort: Function
    onChunkReceived: (listener: RequestTaskOnChunkReceivedCallback) => void
    offChunkReceived: (listener?: RequestTaskOnChunkReceivedCallback | null) => void
    onHeadersReceived: (listener: RequestTaskOnHeadersReceivedCallback) => void
    offHeadersReceived: (listener?: RequestTaskOnHeadersReceivedCallback | null) => void
}

class RequestTask implements UniRequestTask {
    __v_skip: boolean = true
    private _requestTask: IRequestTask
    private _requestOnChunkReceiveCallbackId: number = 0
    private _requestOnChunkReceiveCallbacks: Map<number, RequestTaskOnChunkReceivedCallback> = new Map()
    private _requestOnHeadersReceiveCallbackId: number = 0
    private _requestOnHeadersReceiveCallbacks: Map<number, RequestTaskOnHeadersReceivedCallback> = new Map()

    constructor(requestTask: IRequestTask) {
        this._requestTask = requestTask
    }
    abort() {
        this._requestTask.abort()
    }
    onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
        this._requestTask.onChunkReceived(callback)
        this._requestOnChunkReceiveCallbackId++
        this._requestOnChunkReceiveCallbacks.set(this._requestOnChunkReceiveCallbackId, callback)
        return this._requestOnChunkReceiveCallbackId
    }
    offChunkReceived(callbackId?: RequestTaskOnChunkReceivedCallback | number | null) {
        if (callbackId === undefined || callbackId === null) {
            this._requestTask.offChunkReceived()
            this._requestOnChunkReceiveCallbacks.clear()
            return
        }
        if (typeof callbackId === 'function') {
            this._requestOnChunkReceiveCallbacks.forEach((callback, id) => {
                if (callback === callbackId) {
                    this._requestOnChunkReceiveCallbacks.delete(id)
                    this._requestTask.offChunkReceived(callback)
                    return
                }
            })
            return
        }
        const callback = this._requestOnChunkReceiveCallbacks.get(callbackId)
        if (!callback) {
            return
        }
        this._requestOnChunkReceiveCallbacks.delete(callbackId)
        this._requestTask.offChunkReceived(callback)
    }
    onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
        this._requestTask.onHeadersReceived(callback)
        this._requestOnHeadersReceiveCallbackId++
        this._requestOnHeadersReceiveCallbacks.set(this._requestOnHeadersReceiveCallbackId, callback)
        return this._requestOnHeadersReceiveCallbackId
    }
    offHeadersReceived(callbackId?: RequestTaskOnHeadersReceivedCallback | number | null) {
        if (callbackId === undefined || callbackId === null) {
            this._requestTask.offHeadersReceived()
            this._requestOnHeadersReceiveCallbacks.clear()
            return
        }
        if (typeof callbackId === 'function') {
            this._requestOnHeadersReceiveCallbacks.forEach((callback, id) => {
                if (callback === callbackId) {
                    this._requestOnHeadersReceiveCallbacks.delete(id)
                    this._requestTask.offHeadersReceived(callback)
                    return
                }
            })
            return
        }
        const callback = this._requestOnHeadersReceiveCallbacks.get(callbackId)
        if (!callback) {
            return
        }
        this._requestOnHeadersReceiveCallbacks.delete(callbackId)
        this._requestTask.offHeadersReceived(callback)
    }
}

/**
 * http.request有5MB响应体大小限制
 * rcp.Session.fetch无响应体大小限制，但是headers、cookies处理不够完善，另外rcp需要从@hms引用，不是openharmony标准库
 * 为突破http.request的限制，现在使用http.requestInStream
 */

export const request = defineTaskApi<UniRequestOptions<Object>, UniRequestSuccess<Object>, UniRequestTask>(
    API_REQUEST,
    (args: UniRequestOptions<Object>, exec: ApiExecutor<UniRequestSuccess<Object>>) => {
        let { header, method, dataType, timeout, url, responseType } = args
        let data: ESObject = args.data
        header = header || {} as ESObject
        if (!header!['Cookie'] && !header!['cookie']) {
            header!['Cookie'] = getCookieSync(url);
        }

        let contentType = ''

        // header
        const headers = {} as Record<string, Object>
        const headerRecord = header as Object as Record<string, string>
        const headerKeys = Object.keys(headerRecord)
        for (let i = 0; i < headerKeys.length; i++) {
            const name = headerKeys[i];
            if (name.toLowerCase() === 'content-type') {
                contentType = headerRecord[name] as string
            }
            headers[name.toLowerCase()] = headerRecord[name]
        }

        if (!contentType && method === 'POST') {
            headers['Content-Type'] = 'application/json'
            contentType = 'application/json'
        }

        // url data
        if (method === 'GET' && data && typeof data === 'object') {
            const dataRecord = data as Record<string, Object>
            const query = Object.keys(dataRecord)
                .map((key) => {
                    return (
                        encodeURIComponent(key) +
                        '=' +
                        encodeURIComponent(dataRecord[key] as string | number | boolean)
                    )
                })
                .join('&')
            url += query ?
                (url.indexOf('?') > -1 ? '&' : '?') + query :
                ''
            data = null
        } else if (
            method !== 'GET' &&
            contentType &&
            contentType.indexOf('application/json') === 0 &&
            data &&
            typeof data !== 'string'
        ) {
            data = JSON.stringify(data)
        } else if (
            method !== 'GET' &&
            contentType &&
            contentType.indexOf('application/x-www-form-urlencoded') === 0 &&
            data &&
            typeof data === 'object'
        ) {
            const dataRecord = data as Record<string, Object>
            data = Object.keys(dataRecord)
                .map((key) => {
                    return (
                        encodeURIComponent(key) +
                        '=' +
                        encodeURIComponent(dataRecord[key] as number | string | boolean)
                    )
                })
                .join('&')
        }

        const httpRequest = http.createHttp()
        const mp = getCurrentMP()
        const userAgent = mp.userAgent.fullUserAgent
        if (userAgent && headers && !headers!['User-Agent'] && !headers!['user-agent']) {
            headers!['User-Agent'] = userAgent
        }
        const emitter = new Emitter() as IUniRequestEmitter
        const requestTask: IRequestTask = {
            abort() {
                emitter.off('headersReceive')
                httpRequest.destroy()
            },
            onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
                emitter.on('headersReceive', callback)
            },
            offHeadersReceived(callback?: RequestTaskOnHeadersReceivedCallback | null) {
                emitter.off('headersReceive', callback || undefined)
            },
            onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
                emitter.on('dataReceive', callback)
            },
            offChunkReceived(callback?: RequestTaskOnChunkReceivedCallback | null) {
                emitter.off('dataReceive', callback || undefined)
            },
        }
        const realRequestTask = new RequestTask(requestTask)

        function destroy() {
            emitter.off('headersReceive')
            httpRequest.destroy()
        }
        mp.on('beforeClose', destroy)

        let latestHeaders: Object | null = null
        let lastUrl = url
        httpRequest.on('headersReceive', (headers: Object) => {
            const realHeaders = headers as Record<string, string | string[]>
            const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie']
            if (setCookieHeader) {
                setCookieSync(lastUrl, setCookieHeader as string[])
            }
            latestHeaders = headers
            const location = realHeaders['location'] || realHeaders['Location']
            if (location) {
                lastUrl = location as string
            }
            // TODO headersReceive存在bug，暂不支持回调给用户。
            // emitter.emit('headersReceive', headers);
        })

        let headersReceiveTriggered = false
        const bufs = [] as buffer.Buffer[]
        httpRequest.on('dataReceive', (data) => {
            if (!headersReceiveTriggered) {
                headersReceiveTriggered = true
                // 注意重定向时会多次触发，但是只需要给用户回调最后一次headers
                const headers = {} as UTSJSONObject
                const realHeaders = latestHeaders as Record<string, string | string[]>
                let cookies = [] as string[]
                // 有用户反馈部分情况下headers会为null，无法复现，增加判断逻辑容错
                if (realHeaders) {
                    const keys = Object.keys(realHeaders)
                    for (let i = 0; i < keys.length; i++) {
                        const key = keys[i]
                        if (key === 'set-cookie' || key === 'Set-Cookie') {
                            const setCookieHeader = realHeaders[key] as string[];
                            // TODO 待确认为什么鸿蒙会返回重复的Set-Cookie，不合常理。使用 https://httpbin.org/cookies/set/nnn/123 这个接口测试
                            for (let i = setCookieHeader.length - 1; i >= 0; i--) {
                                const cookie = setCookieHeader[i]
                                if (setCookieHeader.indexOf(cookie) !== i) {
                                    setCookieHeader.splice(i, 1)
                                }
                            }
                            cookies = setCookieHeader as string[];
                            headers[key] = Array.isArray(setCookieHeader) ? setCookieHeader.join(',') : setCookieHeader;
                        } else {
                            headers[key] = realHeaders[key];
                        }
                    }
                }
                emitter.emit('headersReceive', {
                    header: headers,
                    // 鸿蒙不支持在headersReceive时机获取到状态码
                    statusCode: NaN,
                    cookies: cookies,
                } as RequestTaskOnHeadersReceivedListenerResult);
            }
            bufs.push(buffer.from(data))
            if (args.enableChunked) {
                emitter.emit('dataReceive', {
                    data: data
                } as RequestTaskOnChunkReceivedListenerResult)
            }
        })

        httpRequest.requestInStream(
            parseUrl(url),
            {
                header: headers,
                method: (method || 'GET').toUpperCase() as http.RequestMethod, // 仅OPTIONS不支持
                extraData: data || undefined, // 传空字符串会报错
                connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout
                readTimeout: timeout ? timeout : undefined,
                clientCert: getClientCertificate(url)
            } as http.HttpRequestOptions,
            (err, statusCode) => {
                if (err) {
                    /**
                     * TODO abort后此处收到如下错误，待确认是否直接将此错误码转为abort错误
                     * {"code":2300023,"message":"Failed writing received data to disk/application"}
                     *
                     * reject方法第二个参数可以传errCode，reject(err.message, { errCode: -1 })
                     */
                    exec.reject(err.message)
                } else {
                    const responseData = buffer.concat(bufs)
                    let data: ArrayBuffer | string | object = ''
                    if (responseType === 'arraybuffer') {
                        data = responseData.buffer
                    } else {
                        data = responseData.toString('utf8')
                        if (dataType === 'json') {
                            try {
                                // #ifdef UNI-APP-X
                                data = globalThis.UTS.JSON.parse(data) || data;
                                // #endif
                                // #ifndef UNI-APP-X
                                data = JSON.parse(data as string);
                                // #endif
                            } catch (e) {
                                // 与微信保持一致，不抛出异常
                            }
                        }
                    }
                    const headers = latestHeaders as Record<string, string | string[]>
                    // 有用户反馈部分情况下headers会为null，无法复现，增加判断逻辑容错
                    const oldCookies = headers ? (headers['Set-Cookie'] || headers['set-cookie'] || []) as string[] : [] as string[]
                    let newCookies = oldCookies.join(',')
                    if (newCookies) {
                        if (headers['Set-Cookie']) {
                            headers['Set-Cookie'] = newCookies
                        } else {
                            headers['set-cookie'] = newCookies
                        }
                    }
                    exec.resolve({
                        data,
                        statusCode,
                        header: latestHeaders!,
                        cookies: oldCookies,
                    } as UniRequestSuccess<Object>)
                }
                realRequestTask.offChunkReceived()
                realRequestTask.offHeadersReceived()
                httpRequest.destroy() // 调用完毕后必须调用destroy方法
                mp.off('beforeClose', destroy)
            }
        )
        return realRequestTask
    },
    RequestApiProtocol,
    RequestApiOptions
) as Request<Object>
