const AdmZip = require('adm-zip')
const fs = require('fs-extra')
const got = require('got')
const os = require('os')
const path = require('path')
const { pipeline } = require('stream/promises')

const ConfigManager = require('./configmanager')

function getProfileBaseDirectory(profileId){
    return path.join(ConfigManager.getDataDirectory(), 'profiles', profileId)
}

function getProfileDownloadDirectory(profileId){
    return path.join(getProfileBaseDirectory(profileId), 'downloads')
}

function getProfileCacheFile(profileId, fileName){
    return path.join(getProfileDownloadDirectory(profileId), fileName)
}

function getServerBundleDirectory(profile){
    return path.join(getProfileBaseDirectory(profile.id), profile.serverDirectoryName || 'server')
}

function isRemoteSource(source){
    return /^https?:\/\//i.test(source)
}

function resolveLocalSource(source){
    if(source == null){
        return null
    }

    if(source.startsWith('file://')){
        return decodeURIComponent(source.substring('file://'.length))
    }

    return path.resolve(source)
}

async function downloadSourceToFile(source, destination){
    await fs.ensureDir(path.dirname(destination))

    if(isRemoteSource(source)){
        await pipeline(
            got.stream(source),
            fs.createWriteStream(destination)
        )
        return destination
    }

    const localSource = resolveLocalSource(source)
    const localStat = await fs.stat(localSource)
    if(localStat.isDirectory()){
        throw new Error(`Directory source is not supported for file cache: ${localSource}`)
    }
    await fs.copy(localSource, destination, { overwrite: true })
    return destination
}

async function extractZipToDirectory(zipPath, destination){
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'launcher-assets-'))
    try {
        await fs.ensureDir(tempDir)
        new AdmZip(zipPath).extractAllTo(tempDir, true)

        const rootEntries = (await fs.readdir(tempDir)).filter((entry) => entry !== '__MACOSX')
        await fs.remove(destination)

        if(rootEntries.length === 1){
            const onlyEntry = path.join(tempDir, rootEntries[0])
            const entryStat = await fs.stat(onlyEntry)
            if(entryStat.isDirectory()){
                await fs.copy(onlyEntry, destination, { overwrite: true })
                return
            }
        }

        await fs.copy(tempDir, destination, { overwrite: true })
    } finally {
        await fs.remove(tempDir)
    }
}

async function ensureCachedFile(profileId, source, fileName){
    const cachePath = getProfileCacheFile(profileId, fileName)
    await downloadSourceToFile(source, cachePath)
    return cachePath
}

async function ensureServerWorldInstalled(profile, serverDirectory){
    if(!profile.worldArchiveUrl){
        return null
    }

    const targetWorldDirectory = path.join(serverDirectory, 'world')
    if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){
        const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip')
        await extractZipToDirectory(cachePath, targetWorldDirectory)
    } else {
        await fs.remove(targetWorldDirectory)
        await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true })
    }

    return targetWorldDirectory
}

exports.getProfileBaseDirectory = getProfileBaseDirectory
exports.getServerBundleDirectory = getServerBundleDirectory

exports.prefetchProfileAssets = async function(profile){
    if(profile.worldArchiveUrl){
        await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip')
    }
    if(profile.serverJarUrl){
        await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar')
    }

    const currentState = ConfigManager.getLibraryProfileAssetState(profile.id)
    ConfigManager.setLibraryProfileAssetState(profile.id, {
        ...currentState,
        prefetchedAt: new Date().toISOString()
    })
    ConfigManager.save()
}

exports.ensureWorldInstalled = async function(profile, serverId){
    if(!profile.worldArchiveUrl || !profile.worldDirectoryName){
        return null
    }

    const savesDirectory = path.join(ConfigManager.getInstanceDirectory(), serverId, 'saves')
    const targetWorldDirectory = path.join(savesDirectory, profile.worldDirectoryName)

    await fs.ensureDir(savesDirectory)
    if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){
        const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip')
        await extractZipToDirectory(cachePath, targetWorldDirectory)
    } else {
        await fs.remove(targetWorldDirectory)
        await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true })
    }

    ConfigManager.setLibraryQuickPlayWorld(profile.id, profile.worldDirectoryName)
    ConfigManager.setLibraryProfileAssetState(profile.id, {
        worldInstalledAt: new Date().toISOString(),
        worldInstalledServerId: serverId,
        worldDirectoryName: profile.worldDirectoryName
    })
    ConfigManager.save()

    return targetWorldDirectory
}

exports.ensureServerJarInstalled = async function(profile){
    if(!profile.serverJarUrl){
        return null
    }

    const targetDirectory = getServerBundleDirectory(profile)
    const targetJarPath = path.join(targetDirectory, 'server.jar')
    await fs.ensureDir(targetDirectory)

    const cachePath = await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar')
    await fs.copy(cachePath, targetJarPath, { overwrite: true })
    await ensureServerWorldInstalled(profile, targetDirectory)

    ConfigManager.setLibraryProfileAssetState(profile.id, {
        serverInstalledAt: new Date().toISOString(),
        serverDirectory: targetDirectory,
        serverJarPath: targetJarPath
    })
    ConfigManager.save()

    return {
        serverDirectory: targetDirectory,
        serverJarPath: targetJarPath
    }
}

exports.ensureServerBundleInstalled = exports.ensureServerJarInstalled

exports.prepareProfileForLaunch = async function(profile, serverId){
    return exports.ensureWorldInstalled(profile, serverId)
}

exports.ensureServerWorldInstalled = ensureServerWorldInstalled
