const childProcess = require('child_process')
const natUpnp = require('nat-upnp')

const ConfigManager = require('./configmanager')

const RULE_PREFIX = 'MRS Launcher Auto Port'
const UPnP_DESCRIPTION_PREFIX = 'MRS Launcher'
const UPnP_TTL_SECONDS = 3600

const states = new Map()

function buildState(profileId, port){
    return {
        profileId,
        port,
        statusCode: 'checking',
        summary: '포트 확인 중',
        message: '포트 개방 가능 여부를 확인하고 있습니다.',
        tone: 'info',
        firewallCreated: false,
        ruleName: null,
        upnpCreated: false,
        checkedAt: Date.now()
    }
}

function toPublicState(state){
    return {
        port: state.port,
        statusCode: state.statusCode,
        summary: state.summary,
        message: state.message,
        tone: state.tone,
        checkedAt: state.checkedAt
    }
}

function createClient(){
    return natUpnp.createClient()
}

function clientGetMappings(client){
    return new Promise((resolve, reject) => {
        client.getMappings((error, results) => {
            if(error){
                reject(error)
                return
            }
            resolve(Array.isArray(results) ? results : [])
        })
    })
}

function clientPortMapping(client, options){
    return new Promise((resolve, reject) => {
        client.portMapping(options, (error) => {
            if(error){
                reject(error)
                return
            }
            resolve()
        })
    })
}

function clientPortUnmapping(client, options){
    return new Promise((resolve, reject) => {
        client.portUnmapping(options, (error) => {
            if(error){
                reject(error)
                return
            }
            resolve()
        })
    })
}

function extractPort(mapping){
    if(mapping == null){
        return null
    }

    if(typeof mapping.public === 'number'){
        return mapping.public
    }
    if(typeof mapping.publicPort === 'number'){
        return mapping.publicPort
    }
    if(typeof mapping.port === 'number'){
        return mapping.port
    }
    if(mapping.public != null && typeof mapping.public === 'object' && typeof mapping.public.port === 'number'){
        return mapping.public.port
    }

    return null
}

function extractProtocol(mapping){
    if(mapping == null){
        return ''
    }

    const protocol = mapping.protocol ?? mapping.type ?? mapping.public?.protocol ?? ''
    return String(protocol).toUpperCase()
}

async function findExistingTcpMapping(port){
    const client = createClient()
    const mappings = await clientGetMappings(client)
    return mappings.find((mapping) => extractPort(mapping) === port && (extractProtocol(mapping) === '' || extractProtocol(mapping) === 'TCP')) ?? null
}

function getFirewallRuleName(profileId, port){
    return `${RULE_PREFIX} ${profileId} ${port}`
}

function ensureWindowsFirewallRule(profileId, port){
    if(process.platform !== 'win32'){
        return {
            created: false,
            ruleName: null
        }
    }

    const ruleName = getFirewallRuleName(profileId, port)
    const result = childProcess.spawnSync('netsh', [
        'advfirewall',
        'firewall',
        'add',
        'rule',
        `name=${ruleName}`,
        'dir=in',
        'action=allow',
        'protocol=TCP',
        `localport=${port}`
    ], {
        encoding: 'utf8'
    })

    return {
        created: result.status === 0,
        ruleName
    }
}

function deleteWindowsFirewallRule(ruleName){
    if(process.platform !== 'win32' || !ruleName){
        return
    }

    childProcess.spawnSync('netsh', [
        'advfirewall',
        'firewall',
        'delete',
        'rule',
        `name=${ruleName}`
    ], {
        encoding: 'utf8'
    })
}

function buildPortFailureMessage(error){
    const baseMessage = '자동 포트 개방 실패. 직접 포트포워딩 해주세요.'
    if(error == null){
        return baseMessage
    }

    const errorMessage = typeof error === 'string'
        ? error
        : (error.message ?? String(error))

    return `${baseMessage} ${errorMessage}`
}

exports.ensurePortAvailability = async function(profile){
    if(profile?.serverEnabled !== true){
        return {
            port: null,
            statusCode: 'not-needed',
            summary: '포트 개방 필요 없음',
            message: '이 프로필은 서버를 사용하지 않습니다.',
            tone: 'info',
            checkedAt: Date.now()
        }
    }

    const manualAddress = ConfigManager.getLibraryServerAddressOverride(profile.id)
    if(manualAddress != null && manualAddress.length > 0){
        await exports.releaseProfilePort(profile.id)
        return {
            port: profile.serverPort ?? 25565,
            statusCode: 'manual-address',
            summary: '접속 주소 사용 중',
            message: `직접 입력한 접속 주소가 설정되어 있어 자동 포트 개방을 건너뜁니다. (${manualAddress})`,
            tone: 'info',
            checkedAt: Date.now()
        }
    }

    const port = profile.serverPort ?? 25565
    const currentState = states.get(profile.id)
    if(currentState != null && currentState.port === port && currentState.statusCode !== 'failed'){
        currentState.checkedAt = Date.now()
        return toPublicState(currentState)
    }

    const state = buildState(profile.id, port)
    states.set(profile.id, state)

    const firewallState = ensureWindowsFirewallRule(profile.id, port)
    state.firewallCreated = firewallState.created
    state.ruleName = firewallState.ruleName

    try {
        const existingMapping = await findExistingTcpMapping(port)
        if(existingMapping != null){
            state.statusCode = 'already-open'
            state.summary = '이미 포트가 열려있습니다.'
            state.message = `TCP ${port} 포트에 기존 포트포워딩이 있어 그대로 사용합니다.`
            state.tone = 'success'
            state.checkedAt = Date.now()
            return toPublicState(state)
        }

        const client = createClient()
        await clientPortMapping(client, {
            public: port,
            private: port,
            protocol: 'TCP',
            ttl: UPnP_TTL_SECONDS,
            description: `${UPnP_DESCRIPTION_PREFIX} ${profile.id}`
        })

        state.upnpCreated = true
        state.statusCode = 'opened'
        state.summary = '자동 포트 개방 성공'
        state.message = `UPnP로 TCP ${port} 포트를 열었습니다.${state.firewallCreated ? ' 윈도우 방화벽 허용도 같이 적용했습니다.' : ''}`
        state.tone = 'success'
    } catch (error) {
        const message = typeof error?.message === 'string' ? error.message : String(error)
        if(/ConflictInMappingEntry|Already/i.test(message)){
            state.statusCode = 'already-open'
            state.summary = '이미 포트가 열려있습니다.'
            state.message = `TCP ${port} 포트에 기존 포트포워딩이 있어 그대로 사용합니다.`
            state.tone = 'success'
        } else {
            state.statusCode = 'failed'
            state.summary = '자동 포트 개방 실패'
            state.message = buildPortFailureMessage(error)
            state.tone = 'error'
        }
    }

    state.checkedAt = Date.now()
    return toPublicState(state)
}

exports.getPortAvailabilityState = function(profileId){
    const state = states.get(profileId)
    return state != null ? toPublicState(state) : null
}

exports.releaseProfilePort = async function(profileId){
    const state = states.get(profileId)
    if(state == null){
        return
    }

    if(state.upnpCreated){
        try {
            const client = createClient()
            await clientPortUnmapping(client, {
                public: state.port,
                protocol: 'TCP'
            })
        } catch (error) {
            void error
        }
    }

    if(state.firewallCreated && state.ruleName){
        deleteWindowsFirewallRule(state.ruleName)
    }

    states.delete(profileId)
}

exports.cleanupAll = async function(){
    await Promise.all(Array.from(states.keys()).map((profileId) => exports.releaseProfilePort(profileId)))
}

process.once('exit', () => {
    for(const state of states.values()){
        if(state.firewallCreated && state.ruleName){
            deleteWindowsFirewallRule(state.ruleName)
        }
    }
})
