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

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

const runtimes = new Map()

function getRuntime(profileId){
    if(!runtimes.has(profileId)){
        runtimes.set(profileId, {
            serverProcess: null,
            tunnelProcess: null,
            logs: [],
            status: 'stopped',
            publishedAddress: ConfigManager.getPublishedLibraryServerAddress(profileId)
        })
    }

    return runtimes.get(profileId)
}

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

function interpolateCommand(template, variables){
    return Object.entries(variables).reduce((command, [key, value]) => {
        return command.replaceAll(`\${${key}}`, String(value))
    }, template)
}

function extractPublishedAddress(line, profile){
    if(profile.tunnelAddressRegex){
        const customRegex = new RegExp(profile.tunnelAddressRegex)
        const customMatch = line.match(customRegex)
        if(customMatch){
            return customMatch[1] ?? customMatch[0]
        }
    }

    const genericMatch = line.match(/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:\d+|[a-zA-Z0-9.-]+:\d{2,5})/)
    return genericMatch ? genericMatch[1] : null
}

async function resolveServerLaunchCommand(profile, serverDirectory){
    if(profile.serverLaunchCommand){
        return profile.serverLaunchCommand
    }

    const startScript = process.platform === 'win32' ? 'start.bat' : 'start.sh'
    const startScriptPath = path.join(serverDirectory, startScript)
    if(await fs.pathExists(startScriptPath)){
        return process.platform === 'win32' ? startScript : `./${startScript}`
    }

    const jarPath = path.join(serverDirectory, 'server.jar')
    if(await fs.pathExists(jarPath)){
        return 'java -jar server.jar nogui'
    }

    throw new Error('서버 시작 명령을 결정할 수 없습니다. serverLaunchCommand 또는 server.jar/start 스크립트를 준비하세요.')
}

function resolveWorkingDirectory(profile, serverDirectory){
    if(profile.serverWorkingDirectory){
        return path.join(serverDirectory, profile.serverWorkingDirectory)
    }
    return serverDirectory
}

async function startTunnelProcess(profile, runtime, serverDirectory){
    if(!profile.tunnelCommand){
        return null
    }

    const command = interpolateCommand(profile.tunnelCommand, {
        port: profile.serverPort ?? 25565,
        serverDir: serverDirectory
    })

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

    runtime.tunnelProcess = tunnelProcess

    tunnelProcess.stdout?.on('data', (chunk) => {
        const text = chunk.toString()
        text.split(/\r?\n/).filter(Boolean).forEach((line) => {
            appendLog(runtime, `[tunnel] ${line}`)
            const address = extractPublishedAddress(line, profile)
            if(address){
                runtime.publishedAddress = address
                ConfigManager.setPublishedLibraryServerAddress(profile.id, address)
                ConfigManager.save()
            }
        })
    })

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

    tunnelProcess.on('close', () => {
        runtime.tunnelProcess = null
    })

    return tunnelProcess
}

exports.startHostedProfile = async function(profile){
    const runtime = getRuntime(profile.id)
    if(runtime.serverProcess != null){
        return runtime
    }

    const serverDirectory = await ProfileAssetManager.ensureServerBundleInstalled(profile)
    const workingDirectory = resolveWorkingDirectory(profile, serverDirectory)
    const command = await resolveServerLaunchCommand(profile, workingDirectory)

    runtime.status = 'starting'
    runtime.publishedAddress = null
    ConfigManager.setPublishedLibraryServerAddress(profile.id, null)
    ConfigManager.save()
    appendLog(runtime, `[launcher] starting server: ${command}`)

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

    runtime.serverProcess = serverProcess

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

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

    serverProcess.on('close', () => {
        runtime.serverProcess = null
        runtime.status = 'stopped'
        runtime.publishedAddress = null
        ConfigManager.setPublishedLibraryServerAddress(profile.id, null)
        ConfigManager.save()
    })

    if(profile.tunnelCommand){
        await startTunnelProcess(profile, runtime, workingDirectory)
    }

    return runtime
}

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

    if(runtime.tunnelProcess != null){
        runtime.tunnelProcess.kill()
        runtime.tunnelProcess = null
    }

    if(runtime.serverProcess != null){
        runtime.serverProcess.kill()
        runtime.serverProcess = null
    }

    runtime.status = 'stopped'
    runtime.publishedAddress = null
    ConfigManager.setPublishedLibraryServerAddress(profileId, null)
    ConfigManager.save()
}

exports.getHostedProfileState = function(profileId){
    const runtime = getRuntime(profileId)
    return {
        status: runtime.status,
        running: runtime.serverProcess != null,
        tunneling: runtime.tunnelProcess != null,
        publishedAddress: runtime.publishedAddress ?? ConfigManager.getPublishedLibraryServerAddress(profileId),
        logs: [...runtime.logs]
    }
}

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