import http from '@ohos.net.http'
import fs from '@ohos.file.fs'
import harmonyUrl from '@ohos.url'
import {
    getEnv,
    Emitter,
    getCurrentMP,
    getRealPath,
} from '@dcloudio/uni-runtime'
import {
    DownloadTask as UniDownloadTask,
    DownloadFileOptions as UniDownloadFileOptions,
    DownloadFileSuccess as UniDownloadFileSuccess,
    OnProgressDownloadResult,
    DownloadFile
} from '../../interface.uts'
import {
    API_DOWNLOAD_FILE,
    DownloadFileApiOptions,
    DownloadFileApiProtocol,
} from '../../protocol.uts'
import {
    lookupExt
} from './mime.uts'
import {
    getClientCertificate,
    parseUrl,
} from './utils.uts'
import {
    getCookieSync,
    setCookieSync
} from './cookie.uts'
import { BusinessError } from '@ohos.base'

interface IUniDownloadFileEmitter {
    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 IDownloadTask {
    abort: Function
    onHeadersReceived: Function
    offHeadersReceived: Function
    onProgressUpdate: Function
    offProgressUpdate: Function
}

function getContentFileName(contentDisposition: string): string {
    const contentDispositionFileNameMatches = contentDisposition.match(/filename=['"]?(.*?)(['"]|$)/);
    const contentDispositionFileName = contentDispositionFileNameMatches ? contentDispositionFileNameMatches[1] : '';
    return contentDispositionFileName || '';
}

interface GetDownloadFilePathOptions {
    dirName: string
    fileName: string
    contentType: string
    contentDisposition: string
    url: string
    downloadPath: string
}

function fileExists(filePath: string): boolean {
    try {
        fs.statSync(filePath)
        return true
    } catch (error) {
        return false
    }
}

function getDownloadFilePath(options: GetDownloadFilePathOptions): string {
    const { dirName, fileName, contentType, contentDisposition, url, downloadPath } = options
    if (dirName && fileName) {
        const realFilePath = dirName + '/' + fileName
        if (fileExists(realFilePath)) {
            fs.unlinkSync(realFilePath)
        }
        if (!fileExists(dirName)) {
            fs.mkdirSync(dirName, true)
        }
        return realFilePath
    }
    let realDirName = dirName || downloadPath
    const contentDispositionFileName = getContentFileName(contentDisposition);
    const urlFileName = harmonyUrl.URL.parseURL(url).pathname.split('/').pop()
    let realFileName: string = contentDispositionFileName || urlFileName || ''
    if (!realFileName) {
        const ext = lookupExt(contentType) || ''
        const now = Date.now()
        realFileName = ext ? '' + now + '.' + ext : '' + now
    }
    const lastIndexOfDot = realFileName.lastIndexOf('.')
    const realFileNameExt = realFileName.substring(lastIndexOfDot + 1)
    const realFileNameWithoutExt = realFileName.substring(0, lastIndexOfDot)

    let realFilePath: string = realDirName + '/' + realFileName
    let number = 1
    while (fileExists(realFilePath)) {
        realFilePath = realDirName + '/' + realFileNameWithoutExt + '(' + number + ').' + realFileNameExt
        number++
    }
    if (!fileExists(realDirName)) {
        fs.mkdirSync(realDirName, true)
    }
    return decodeURIComponent(realFilePath) 
}

/**
 * TODO 鸿蒙的downloadFile接口也需要传filePath，仍然会遇到content-type -> extension的问题
 */

class DownloadTask implements UniDownloadTask {
    __v_skip: boolean = true
    private _downloadTask: IDownloadTask
    constructor(downloadTask: IDownloadTask) {
        this._downloadTask = downloadTask
    }

    abort() {
        this._downloadTask.abort()
    }

    onProgressUpdate(callback: Function) {
        this._downloadTask.onProgressUpdate(callback)
    }

    offProgressUpdate(callback?: Function) {
        this._downloadTask.offProgressUpdate(callback)
    }

    onHeadersReceived(callback: Function) {
        this._downloadTask.onHeadersReceived(callback)
    }

    offHeadersReceived(callback?: Function) {
        this._downloadTask.offHeadersReceived(callback)
    }
}

export const downloadFile = defineTaskApi<UniDownloadFileOptions, UniDownloadFileSuccess, UniDownloadTask>(
    API_DOWNLOAD_FILE,
    (args: UniDownloadFileOptions, exec: ApiExecutor<UniDownloadFileSuccess>) => {
        let { url, timeout, header, filePath } = args
        let dirName = '';
        let fileName = ''
        if (filePath) {
            const realPath = getRealPath(filePath) as string
            if (filePath.endsWith('/')) {
                dirName = realPath.endsWith('/') ? realPath.slice(0, -1) : realPath;
            } else {
                dirName = realPath.substring(0, realPath.lastIndexOf('/'));
                fileName = realPath.substring(realPath.lastIndexOf('/') + 1);
            }
        }
        header = header || {} as ESObject
        if (!header!['Cookie'] && !header!['cookie']) {
            header!['Cookie'] = getCookieSync(url);
        }

        const httpRequest = http.createHttp()
        const mp = getCurrentMP()
        const userAgent = mp.userAgent.fullUserAgent
        if (userAgent && !header!['User-Agent'] && !header!['user-agent']) {
            header!['User-Agent'] = userAgent
        }
        const emitter = new Emitter() as IUniDownloadFileEmitter
        const downloadTask: IDownloadTask = {
            abort() {
                emitter.off('headersReceive')
                emitter.off('progress')
                httpRequest.destroy()
            },
            onHeadersReceived(callback: Function) {
                emitter.on('headersReceive', callback)
            },
            offHeadersReceived(callback?: Function) {
                emitter.off('headersReceive', callback)
            },
            onProgressUpdate(callback: Function) {
                emitter.on('progress', callback)
            },
            offProgressUpdate(callback?: Function) {
                emitter.off('progress', callback)
            },
        }

        function destroy() {
            downloadTask.abort()
        }
        mp.on('beforeClose', destroy)

        let responseContentType = ''
        let responseContentDisposition = ''
        let lastUrl = url
        httpRequest.on('headersReceive', (headers: Object) => {
            const realHeaders = headers as Record<string, string | string[]>
            responseContentType =
                realHeaders['content-type'] as string ||
                realHeaders['Content-Type'] as string ||
                ''
            responseContentDisposition =
                realHeaders['content-disposition'] as string ||
                realHeaders['Content-Disposition'] as string ||
                ''
            const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie']
            if (setCookieHeader) {
                setCookieSync(lastUrl, setCookieHeader as string[])
            }
            const location = realHeaders['location'] || realHeaders['Location']
            if (location) {
                lastUrl = location as string
            }
            // TODO headersReceive存在bug，暂不支持回调给用户。注意重定向时会多次触发，但是只需要给用户回调最后一次
            // emitter.emit('headersReceive', header);
        })
        httpRequest.on('dataReceiveProgress', ({ receiveSize, totalSize }) => {
            emitter.emit('progress', {
                progress: Math.floor((receiveSize / totalSize) * 100),
                totalBytesWritten: receiveSize,
                totalBytesExpectedToWrite: totalSize,
            } as OnProgressDownloadResult)
        })
        const CACHE_PATH = getEnv().CACHE_PATH as string
        const downloadPath = CACHE_PATH + '/uni-download'
        if (!fs.accessSync(downloadPath)) {
            fs.mkdirSync(downloadPath, true)
        }
        let stream: fs.Stream
        let tempFilePath = ''

        let writePromise = Promise.resolve(0)
        async function queueWrite(data: ArrayBuffer): Promise<number> {
            writePromise = writePromise.then(async (total) => {
                const length = await stream.write(data)
                return total + length
            })
            return writePromise
        }

        httpRequest.on('dataReceive', (data) => {
            if (!stream) {
                tempFilePath = getDownloadFilePath({
                    dirName,
                    fileName,
                    contentType: responseContentType,
                    contentDisposition: responseContentDisposition,
                    url,
                    downloadPath
                } as GetDownloadFilePathOptions)
                try {
                  stream = fs.createStreamSync(tempFilePath, 'w+');
                } catch (error) {
                  exec.reject((error as BusinessError).message)
                }
            }
            queueWrite(data)
        })
        httpRequest.requestInStream(
            parseUrl(url),
            {
                header: header ? header : {} as ESObject,
                method: http.RequestMethod.GET,
                connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout
                readTimeout: timeout ? timeout : undefined,
                clientCert: getClientCertificate(url)
            } as http.HttpRequestOptions,
            (err, statusCode) => {
                // 此回调先于dataEnd回调执行
                let finishPromise: Promise<void> = Promise.resolve()
                if (err) {
                    /**
                     * TODO abort后此处收到如下错误，待确认是否直接将此错误码转为abort错误
                     * {"code":2300023,"message":"Failed writing received data to disk/application"}
                     */
                    exec.reject(err.message)
                } else {
                    finishPromise = writePromise.then(async () => {
                        await stream.flush()
                        await stream.close()
                        exec.resolve({
                            tempFilePath,
                            statusCode,
                        } as UniDownloadFileSuccess)
                    }).catch((err: Error) => {
                        exec.reject(err.message)
                    })
                }
                finishPromise.then(() => {
                    downloadTask.offHeadersReceived()
                    downloadTask.offProgressUpdate()
                    httpRequest.destroy() // 调用完毕后必须调用destroy方法
                    mp.off('beforeClose', destroy)
                })
            }
        )
        return new DownloadTask(downloadTask)
    },
    DownloadFileApiProtocol,
    DownloadFileApiOptions
) as DownloadFile
