const express = require('express')
const fs = require('fs-extra')
const multer = require('multer')
const path = require('path')

const HOST = process.env.LAUNCHER_ADMIN_HOST || '127.0.0.1'
const PORT = Number.parseInt(process.env.LAUNCHER_ADMIN_PORT || '8787', 10)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const RUNTIME_DATA_DIR = path.join(__dirname, 'data')
const UPLOADS_DIR = path.join(RUNTIME_DATA_DIR, 'uploads')
const DISTRIBUTIONS_DIR = path.join(RUNTIME_DATA_DIR, 'distributions')
const RUNTIME_CATALOG_PATH = path.join(RUNTIME_DATA_DIR, 'catalog.json')
const LAUNCHER_CATALOG_PATH = path.join(PROJECT_ROOT, 'app', 'assets', 'launcher', 'catalog.json')
const PUBLIC_DIR = path.join(__dirname, 'public')
const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json')
const EXAMPLE_DISTRIBUTION_PREFIX = 'https://example.com/launcher/'
const PUBLIC_BASE_URL = normalizeText(process.env.LAUNCHER_PUBLIC_BASE_URL).replace(/\/+$/, '')

function normalizeText(value){
    return typeof value === 'string' ? value.trim() : ''
}

function normalizeMultilineText(value){
    return typeof value === 'string'
        ? value.replace(/\r\n/g, '\n').trim()
        : ''
}

function normalizePort(value){
    const port = Number.parseInt(String(value ?? ''), 10)
    if(Number.isFinite(port) && port >= 1 && port <= 65535){
        return port
    }
    return 25565
}

function normalizePositiveInteger(value, fallback, minimum = 1){
    const parsed = Number.parseInt(String(value ?? ''), 10)
    if(Number.isFinite(parsed) && parsed >= minimum){
        return parsed
    }
    return fallback
}

function normalizeBoolean(value){
    return value === true
}

function getPublicBaseUrl(){
    if(PUBLIC_BASE_URL.length > 0){
        return PUBLIC_BASE_URL
    }
    return `http://${HOST}:${PORT}`
}

function resolveSafeProjectPath(relativePath){
    const resolvedPath = path.resolve(PROJECT_ROOT, relativePath)
    if(!resolvedPath.startsWith(PROJECT_ROOT + path.sep) && resolvedPath !== PROJECT_ROOT){
        throw new Error('허용되지 않은 경로입니다.')
    }
    return resolvedPath
}

function createDistributionFileName(profileId){
    const safeId = String(profileId ?? 'distribution')
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9._-]+/g, '-')
        .replace(/-+/g, '-')
        .replace(/^[-_.]+|[-_.]+$/g, '')

    return `${safeId.length > 0 ? safeId : 'distribution'}.distribution.json`
}

function deriveFeatureFlags(rawProfile){
    const legacyKind = normalizeText(rawProfile?.kind)

    let modsEnabled = normalizeBoolean(rawProfile?.modsEnabled)
    let pluginsEnabled = normalizeBoolean(rawProfile?.pluginsEnabled)
    let serverEnabled = normalizeBoolean(rawProfile?.serverEnabled)

    if(legacyKind === 'modpack'){
        modsEnabled = true
    } else if(legacyKind === 'server-pack'){
        pluginsEnabled = true
        serverEnabled = true
    }

    if(pluginsEnabled){
        serverEnabled = true
    }

    return {
        modsEnabled,
        pluginsEnabled,
        serverEnabled
    }
}

function normalizeServerJarSource(rawProfile){
    const directValue = normalizeText(rawProfile?.serverJarUrl)
    if(directValue.length > 0){
        return directValue
    }

    const legacyBundle = normalizeText(rawProfile?.serverBundleUrl)
    if(legacyBundle.toLowerCase().endsWith('.jar')){
        return legacyBundle
    }

    return ''
}

function sanitizeProfile(rawProfile, index){
    const {
        modsEnabled,
        pluginsEnabled,
        serverEnabled
    } = deriveFeatureFlags(rawProfile)

    const sanitized = {
        id: normalizeText(rawProfile?.id) || `profile-${index + 1}`,
        name: normalizeText(rawProfile?.name) || `새 프로필 ${index + 1}`,
        description: normalizeText(rawProfile?.description),
        details: normalizeMultilineText(rawProfile?.details),
        distributionUrl: normalizeText(rawProfile?.distributionUrl),
        modsEnabled,
        pluginsEnabled,
        serverEnabled,
        worldArchiveUrl: normalizeText(rawProfile?.worldArchiveUrl),
        worldDirectoryName: normalizeText(rawProfile?.worldDirectoryName)
    }

    if(serverEnabled){
        sanitized.serverJarUrl = normalizeServerJarSource(rawProfile)
        sanitized.serverDirectoryName = normalizeText(rawProfile?.serverDirectoryName) || `${sanitized.id}-server`
        sanitized.serverPort = normalizePort(rawProfile?.serverPort)
        sanitized.serverMemoryMb = normalizePositiveInteger(rawProfile?.serverMemoryMb, 4096, 512)
        sanitized.serverMaxPlayers = normalizePositiveInteger(rawProfile?.serverMaxPlayers, 20, 1)
        sanitized.serverWhitelistEnabled = normalizeBoolean(rawProfile?.serverWhitelistEnabled)
    }

    if(normalizeText(rawProfile?.artwork).length > 0){
        sanitized.artwork = normalizeText(rawProfile.artwork)
    }

    return sanitized
}

function sanitizeCatalog(payload){
    const profiles = Array.isArray(payload?.profiles) ? payload.profiles : []

    return {
        version: 1,
        profiles: profiles.map((profile, index) => sanitizeProfile(profile, index))
    }
}

function toProjectRelativePath(targetPath){
    return path.relative(PROJECT_ROOT, targetPath).split(path.sep).join('/')
}

async function ensureRuntimeCatalog(){
    await fs.ensureDir(RUNTIME_DATA_DIR)
    await fs.ensureDir(UPLOADS_DIR)
    await fs.ensureDir(DISTRIBUTIONS_DIR)

    if(!(await fs.pathExists(RUNTIME_CATALOG_PATH))){
        if(await fs.pathExists(LAUNCHER_CATALOG_PATH)){
            await fs.copy(LAUNCHER_CATALOG_PATH, RUNTIME_CATALOG_PATH, { overwrite: true })
        } else {
            await fs.writeJson(RUNTIME_CATALOG_PATH, { version: 1, profiles: [] }, { spaces: 2 })
        }
    }

    await migrateExampleCatalog()
}

async function ensureLocalSampleDistribution(profileId, profileName){
    const fileName = createDistributionFileName(profileId)
    const targetPath = path.join(DISTRIBUTIONS_DIR, fileName)

    if(!(await fs.pathExists(targetPath))){
        const template = JSON.parse(await fs.readFile(SAMPLE_DISTRIBUTION_PATH, 'utf8'))
        template.servers = [
            {
                id: profileId,
                name: profileName || profileId,
                description: '관리자 사이트 로컬 샘플 distribution입니다.',
                version: '1.20.1',
                minecraftVersion: '1.20.1',
                address: '127.0.0.1:25565',
                mainServer: true,
                autoconnect: false,
                modules: []
            }
        ]
        await fs.writeFile(targetPath, JSON.stringify(template, null, 2) + '\n', 'utf8')
    }

    return toProjectRelativePath(targetPath)
}

async function migrateExampleCatalog(){
    if(!(await fs.pathExists(RUNTIME_CATALOG_PATH))){
        return
    }

    const runtimeCatalog = sanitizeCatalog(await fs.readJson(RUNTIME_CATALOG_PATH))
    let changed = false

    for(const profile of runtimeCatalog.profiles){
        if(normalizeText(profile.distributionUrl).startsWith(EXAMPLE_DISTRIBUTION_PREFIX)){
            profile.distributionUrl = await ensureLocalSampleDistribution(profile.id, profile.name)
            changed = true
        }
    }

    if(changed){
        await fs.writeJson(RUNTIME_CATALOG_PATH, runtimeCatalog, { spaces: 2 })
        await fs.writeJson(LAUNCHER_CATALOG_PATH, runtimeCatalog, { spaces: 2 })
    }
}

async function readCatalog(){
    await ensureRuntimeCatalog()
    return sanitizeCatalog(await fs.readJson(RUNTIME_CATALOG_PATH))
}

async function writeCatalog(catalog){
    const sanitizedCatalog = sanitizeCatalog(catalog)
    await fs.ensureDir(path.dirname(LAUNCHER_CATALOG_PATH))
    await fs.writeJson(RUNTIME_CATALOG_PATH, sanitizedCatalog, { spaces: 2 })
    await fs.writeJson(LAUNCHER_CATALOG_PATH, sanitizedCatalog, { spaces: 2 })
    return sanitizedCatalog
}

function createUploadStorage(){
    return multer.diskStorage({
        destination(_req, _file, callback){
            fs.ensureDir(UPLOADS_DIR)
                .then(() => callback(null, UPLOADS_DIR))
                .catch((error) => callback(error))
        },
        filename(_req, file, callback){
            const extension = path.extname(file.originalname)
            const baseName = path.basename(file.originalname, extension)
                .replace(/[^a-zA-Z0-9._-]+/g, '-')
                .replace(/-+/g, '-')
                .replace(/^[-_.]+|[-_.]+$/g, '')
            const timestamp = Date.now()
            const safeBaseName = baseName.length > 0 ? baseName : 'file'
            callback(null, `${timestamp}-${safeBaseName}${extension.toLowerCase()}`)
        }
    })
}

async function start(){
    await ensureRuntimeCatalog()

    const app = express()
    const upload = multer({
        storage: createUploadStorage(),
        limits: {
            fileSize: 1024 * 1024 * 1024
        }
    })

    app.use((_req, res, next) => {
        res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
        res.setHeader('Pragma', 'no-cache')
        res.setHeader('Expires', '0')
        next()
    })

    app.use(express.json({ limit: '5mb' }))
    app.use('/uploads', express.static(UPLOADS_DIR))
    app.use('/admin/data/uploads', express.static(UPLOADS_DIR))
    app.use('/admin/data/distributions', express.static(DISTRIBUTIONS_DIR))

    app.get('/api/meta', async (_req, res) => {
        res.json({
            host: HOST,
            port: PORT,
            runtimeCatalogPath: toProjectRelativePath(RUNTIME_CATALOG_PATH),
            launcherCatalogPath: toProjectRelativePath(LAUNCHER_CATALOG_PATH),
            localCatalogUrl: `${getPublicBaseUrl()}/catalog.json`,
            distributionsPath: toProjectRelativePath(DISTRIBUTIONS_DIR)
        })
    })

    app.get('/api/distribution/template', async (_req, res, next) => {
        try {
            const content = await fs.readFile(SAMPLE_DISTRIBUTION_PATH, 'utf8')
            res.json({
                ok: true,
                content
            })
        } catch (error) {
            next(error)
        }
    })

    app.get('/api/distribution/content', async (req, res, next) => {
        try {
            const requestedPath = normalizeText(req.query.path)
            if(requestedPath.length === 0){
                res.status(400).json({
                    ok: false,
                    message: '불러올 distribution 경로가 없습니다.'
                })
                return
            }

            if(/^https?:\/\//i.test(requestedPath)){
                const response = await fetch(requestedPath, {
                    signal: AbortSignal.timeout(10000)
                })
                if(!response.ok){
                    throw new Error(`원격 distribution을 불러오지 못했습니다. (${response.status})`)
                }

                const content = await response.text()
                JSON.parse(content)
                res.json({
                    ok: true,
                    content
                })
                return
            }

            const resolvedPath = resolveSafeProjectPath(requestedPath)
            const content = await fs.readFile(resolvedPath, 'utf8')
            res.json({
                ok: true,
                content
            })
        } catch (error) {
            next(error)
        }
    })

    app.get('/api/catalog', async (_req, res, next) => {
        try {
            res.json(await readCatalog())
        } catch (error) {
            next(error)
        }
    })

    app.put('/api/catalog', async (req, res, next) => {
        try {
            const savedCatalog = await writeCatalog(req.body)
            res.json({
                ok: true,
                catalog: savedCatalog
            })
        } catch (error) {
            next(error)
        }
    })

    app.post('/api/upload', upload.single('file'), async (req, res, next) => {
        try {
            if(req.file == null){
                res.status(400).json({
                    ok: false,
                    message: '업로드할 파일이 없습니다.'
                })
                return
            }

            const relativePath = toProjectRelativePath(req.file.path)

            res.json({
                ok: true,
                file: {
                    name: req.file.originalname,
                    storedName: req.file.filename,
                    size: req.file.size,
                    path: relativePath,
                    localUrl: `${getPublicBaseUrl()}/uploads/${encodeURIComponent(req.file.filename)}`
                }
            })
        } catch (error) {
            next(error)
        }
    })

    app.post('/api/distribution/save', async (req, res, next) => {
        try {
            const profileId = normalizeText(req.body?.profileId)
            const rawContent = typeof req.body?.content === 'string' ? req.body.content : ''

            if(profileId.length === 0){
                res.status(400).json({
                    ok: false,
                    message: '프로필 ID가 필요합니다.'
                })
                return
            }

            const parsed = JSON.parse(rawContent)
            const fileName = createDistributionFileName(profileId)
            const targetPath = path.join(DISTRIBUTIONS_DIR, fileName)
            const relativePath = toProjectRelativePath(targetPath)

            await fs.writeFile(targetPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8')

            res.json({
                ok: true,
                file: {
                    path: relativePath,
                    localUrl: `${getPublicBaseUrl()}/${relativePath}`
                }
            })
        } catch (error) {
            next(error)
        }
    })

    app.get('/catalog.json', async (_req, res, next) => {
        try {
            res.sendFile(RUNTIME_CATALOG_PATH)
        } catch (error) {
            next(error)
        }
    })

    app.use(express.static(PUBLIC_DIR))

    app.get(/.*/, (_req, res) => {
        res.sendFile(path.join(PUBLIC_DIR, 'index.html'))
    })

    app.use((error, _req, res, _next) => {
        console.error(error)
        res.status(500).json({
            ok: false,
            message: error instanceof Error ? error.message : '관리자 서버 처리 중 오류가 발생했습니다.'
        })
    })

    app.listen(PORT, HOST, () => {
        console.log(`[launcher-admin] running on http://${HOST}:${PORT}`)
        console.log(`[launcher-admin] runtime catalog: ${toProjectRelativePath(RUNTIME_CATALOG_PATH)}`)
        console.log(`[launcher-admin] launcher catalog mirror: ${toProjectRelativePath(LAUNCHER_CATALOG_PATH)}`)
    })
}

start().catch((error) => {
    console.error(error)
    process.exit(1)
})
