const childProcess = require('child_process')
const fs = require('fs-extra')
const path = require('path')

const ConfigManager = require('./configmanager')
const PortManager = require('./portmanager')
const ProfileAssetManager = require('./profileassetmanager')

const runtimes = new Map()
const READY_PATTERNS = [
    /Done \([0-9.]+s\)!/i,
    /For help, type "help"/i
]

function getRuntime(profileId){
    if(!runtimes.has(profileId)){
        runtimes.set(profileId, {
            serverProcess: null,
            logs: [],
            status: 'stopped',
            ready: false,
            readyPromise: null,
            readyResolver: null,
            readyRejecter: null
        })
    }

    return runtimes.get(profileId)
}

function appendLog(runtime, line){
    runtime.logs.push(line)
    if(runtime.logs.length > 200){
        runtime.logs.shift()
    }
}

function createReadyPromise(runtime){
    runtime.ready = false
    runtime.readyPromise = new Promise((resolve, reject) => {
        runtime.readyResolver = resolve
        runtime.readyRejecter = reject
    })
    return runtime.readyPromise
}

function resolveReady(runtime){
    if(runtime.ready === true){
        return
    }

    runtime.ready = true
    runtime.status = 'running'
    if(runtime.readyResolver){
        runtime.readyResolver(runtime)
    }
    runtime.readyResolver = null
    runtime.readyRejecter = null
}

function rejectReady(runtime, error){
    runtime.ready = false
    if(runtime.readyRejecter){
        runtime.readyRejecter(error)
    }
    runtime.readyResolver = null
    runtime.readyRejecter = null
}

function resolveServerJavaExecutable(){
    const selectedServerId = ConfigManager.getSelectedServer()
    if(selectedServerId != null){
        const configured = ConfigManager.getJavaExecutable(selectedServerId)
        if(configured){
            return configured
        }
    }

    return 'java'
}

async function writeServerFiles(profile, serverDirectory){
    await fs.writeFile(path.join(serverDirectory, 'eula.txt'), 'eula=true\n', 'utf8')

    const properties = [
        ['enable-query', 'false'],
        ['max-players', String(profile.serverMaxPlayers ?? 20)],
        ['online-mode', 'true'],
        ['server-port', String(profile.serverPort ?? 25565)],
        ['white-list', profile.serverWhitelistEnabled ? 'true' : 'false'],
        ['level-name', 'world']
    ]

    await fs.writeFile(
        path.join(serverDirectory, 'server.properties'),
        properties.map(([key, value]) => `${key}=${value}`).join('\n') + '\n',
        'utf8'
    )
}

function buildLaunchCommand(profile, javaExecutable){
    const memoryMb = Number.isFinite(Number(profile.serverMemoryMb))
        ? Math.max(512, Number(profile.serverMemoryMb))
        : 4096

    return `"${javaExecutable}" -Xms${memoryMb}M -Xmx${memoryMb}M -jar server.jar nogui`
}

exports.startHostedProfile = async function(profile){
    if(profile.serverEnabled !== true){
        throw new Error('서버 사용이 꺼진 프로필입니다.')
    }

    const runtime = getRuntime(profile.id)
    if(runtime.serverProcess != null){
        if(runtime.readyPromise != null){
            await runtime.readyPromise.catch(() => {})
        }
        return runtime
    }

    const installedServer = await ProfileAssetManager.ensureServerJarInstalled(profile)
    if(installedServer?.serverJarPath == null){
        throw new Error('로컬 서버를 시작하려면 버킷 JAR 업로드가 필요합니다.')
    }

    await ProfileAssetManager.ensureServerWorldInstalled(profile, installedServer.serverDirectory)
    await writeServerFiles(profile, installedServer.serverDirectory)
    await PortManager.ensurePortAvailability(profile)

    const command = buildLaunchCommand(profile, resolveServerJavaExecutable())
    appendLog(runtime, `[launcher] starting server: ${command}`)

    runtime.status = 'starting'
    createReadyPromise(runtime)

    const serverProcess = childProcess.spawn(command, {
        cwd: installedServer.serverDirectory,
        shell: true,
        detached: false
    })

    runtime.serverProcess = serverProcess

    const readyFallbackTimer = setTimeout(() => {
        if(runtime.serverProcess != null){
            resolveReady(runtime)
        }
    }, 25000)

    serverProcess.stdout?.on('data', (chunk) => {
        chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => {
            appendLog(runtime, `[server] ${line}`)
            if(READY_PATTERNS.some((pattern) => pattern.test(line))){
                resolveReady(runtime)
            }
        })
    })

    serverProcess.stderr?.on('data', (chunk) => {
        chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => {
            appendLog(runtime, `[server:err] ${line}`)
        })
    })

    serverProcess.on('close', () => {
        clearTimeout(readyFallbackTimer)
        runtime.serverProcess = null
        runtime.status = 'stopped'
        runtime.ready = false
        rejectReady(runtime, new Error('서버 프로세스가 종료되었습니다.'))
        PortManager.releaseProfilePort(profile.id).catch(() => {})
    })

    await runtime.readyPromise.catch(() => {})
    return runtime
}

exports.stopHostedProfile = async function(profileId){
    const runtime = getRuntime(profileId)

    if(runtime.serverProcess != null){
        runtime.status = 'stopping'
        try {
            runtime.serverProcess.stdin?.write('stop\n')
        } catch (error) {
            void error
        }

        setTimeout(() => {
            if(runtime.serverProcess != null){
                runtime.serverProcess.kill()
            }
        }, 5000)
    }

    runtime.status = 'stopped'
    runtime.ready = false
    await PortManager.releaseProfilePort(profileId).catch(() => {})
}

exports.getHostedProfileState = function(profileId){
    const runtime = getRuntime(profileId)
    return {
        status: runtime.status,
        running: runtime.serverProcess != null,
        ready: runtime.ready,
        logs: [...runtime.logs]
    }
}

exports.hasRunningProfiles = function(){
    for(const runtime of runtimes.values()){
        if(runtime.serverProcess != null){
            return true
        }
    }
    return false
}
