import chalk from 'chalk'
import * as fs from 'fs-extra'
import { Maybe, Nothing, Result } from 'ghetto-monad'
import * as path from 'path'
import * as docker from './docker'
import { Binder } from './docker'
import { localPath, localWorldsPath } from './paths'
import { RestConfig, SMAServerConfig } from './SMAServerConfig'
import { World } from './worlds'

export type ServerType = 'bukkit' | 'nukkit'

class Server {
    static defaultServerType: ServerType = 'bukkit'
    static defaultPort = {
        bukkit: '25565',
        nukkit: '19132',
    }
    static defaultDockerTag = 'latest'
    static defaultMemory = 2048
    static restPort = 8086
    static restPassword = 'INSECURE'
    static defaultDockerImage = 'magikcraft/scriptcraft'

    private serverConfig!: Promise<Maybe<Result<SMAServerConfig>>>
    binder: docker.Binder
    filename!: string
    serverType!: ServerType

    constructor() {
        this.binder = new Binder()
    }

    async getServerTargetFromPackageJson() {
        const conf = await this.getServerConfig()
        if (conf.isNothing) {
            return new Nothing()
        } else {
            return new Result(conf.value.serverName)
        }
    }

    async getNodeModulesBinding() {
        const conf = await this.getServerConfig()
        if (conf.isNothing) {
            return new Nothing()
        } else {
            console.log(`Bind node_modules: ${conf.value.node_modules}`)
            return new Result(conf.value.node_modules)
        }
    }

    async getTestMode() {
        const conf = await this.getServerConfig()
        if (conf.isNothing) {
            return false
        } else {
            const testMode = conf.value.testMode === true
            return testMode
        }
    }

    async getName() {
        const conf = await this.getServerConfig()
        if (conf.isNothing) {
            return undefined
        } else {
            return conf.value.serverName
        }
    }

    // @TODO Bind node_modules directly
    createNodeModuleBindings() {
        const modules = fs.readdirSync('node_modules')
        if (!modules) {
            console.log(
                chalk.yellow(
                    'WARNING: node_modules directory not found, and it was specified in the bindings. Do you need to run '
                ) +
                    chalk.magenta('npm i') +
                    chalk.yellow('?')
            )
            console.log('Skipping node_modules binding')
            return ''
        }
        return this.binder.makeMount(
            localPath(`node_modules`),
            `scriptcraft-plugins/node_modules`
        )
        // const nsPackage = m => {
        //     const isNamespacedPackage = m && m.indexOf('@') === 0
        //     if (isNamespacedPackage) {
        //         const pkgs = fs.readdirSync(`node_modules/${m}`)
        //         return pkgs
        //             .map(p =>
        //                 this.binder.makeMount(
        //                     localPath(`node_modules/${m}/${p}`),
        //                     `scriptcraft-plugins/${m}/${p}`
        //                 )
        //             )
        //             .join(' ')
        //     }
        //     return this.binder.makeMount(
        //         localPath(`node_modules/${m}`),
        //         `scriptcraft-plugins/${m}`
        //     )
        // }
        // if (modules.length > 0) {
        //     return modules.map(nsPackage).join(' ')
        // }
        // return ''
    }

    async getBindings(name) {
        const worlds = await this.getWorldMounts()

        const bindings = (await this.getCustomBindings())
            .map(({ src, dst }) => this.binder.makeMount(localPath(src), dst))
            .join(' ')
        const mountNodeModules = await this.getNodeModulesBinding()
        const nodeModules =
            !mountNodeModules.isNothing && mountNodeModules.value
                ? this.createNodeModuleBindings()
                : ``
        console.log('Found bindings in config:')
        console.log(bindings)
        return `${worlds} ${bindings} ${nodeModules}`
    }

    private async getWorldMounts() {
        // Check for worlds in the local worlds folder
        const localMounts = this.getLocalWorldMounts()

        // Parse worldDefinitions and make mounts
        const smaMounts = await this.getSmaWorldMounts()

        // Make them unique - prefer local
        const allMounts = {} as {
            [index: string]: { src: string; dst: string }
        }
        localMounts.map(({ src, dst }) => {
            allMounts[dst] = { src, dst }
        })
        for (const smaMount of smaMounts) {
            // Do we need to scan these dirs?
            console.log(`Found: ${smaMount.src}`)
            const existingMount = allMounts[smaMount.dst]
            if (existingMount) {
                if (smaMount.src !== existingMount.src) {
                    console.log(
                        chalk.redBright(
                            `Duplicate worlds found at ${smaMount.src} and ${
                                existingMount.src
                            }`
                        )
                    )
                    console.log(
                        chalk.yellowBright(
                            `Using world from ${existingMount.src}`
                        )
                    )
                }
            } else {
                allMounts[smaMount.dst] = smaMount
            }
        }
        if (Object.keys(allMounts).length > 0) {
            console.log(`Loading the following worlds:`)
        }
        return Object.keys(allMounts)
            .map(m => {
                console.log(allMounts[m])
                const r = this.binder.makeMount(
                    allMounts[m].src,
                    allMounts[m].dst
                )
                return r
            })
            .join(' ')
    }

    private getLocalWorldMounts() {
        const mountData = (name, path) => ({
            src: `${path}/${name}`,
            dst: `worlds/${name}`,
        })
        const localPath = localWorldsPath()
        console.log('Scanning local directory:', localPath)
        if (fs.existsSync(localPath)) {
            const dirs = fs.readdirSync(localPath)
            return dirs.map(name => {
                console.log('Found:', path.join(localPath, name))
                const m = mountData(name, localPath)
                return m
            })
        }
        return []
    }

    private async getSmaWorldMounts() {
        const mountData = (name, path) => ({
            src: `${path}/${name}`,
            dst: `worlds/${name}`,
        })
        console.log(`Checking world definitions in ${this.filename}`)
        const worldDefs = await this.getWorldDefinitions()
        if (worldDefs.isNothing) {
            console.log('None found.')
            return []
        }
        const worlds = worldDefs.value.map(d => new World(d))
        let smaMounts = [] as { src: string; dst: string }[]
        for (const world of worlds) {
            const path = await world.getPath()
            if (!path.isNothing && !path.isError) {
                if (fs.existsSync(path.value)) {
                    const dirs = fs.readdirSync(path.value)
                    dirs.map(name => {
                        smaMounts = [...smaMounts, mountData(name, path.value)]
                    })
                    return smaMounts
                }
            }
        }
        if (smaMounts.length != worlds.length) {
            console.log(
                chalk.red(
                    'WARNING: Some worlds specified in the Worlds definition are not available.'
                )
            )
        }
        return smaMounts
    }

    async getDockerTag() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.dockerTag) {
            return Server.defaultDockerTag
        } else {
            return conf.value.dockerTag
        }
    }

    async getServerType() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.serverType) {
            return Server.defaultServerType
        } else {
            return conf.value.serverType
        }
    }

    async getDockerImage() {
        const serverType = await this.getServerType()
        if (serverType === 'bukkit') {
            return docker.images.bukkit
        }
        if (serverType === 'nukkit') {
            return docker.images.nukkit
        }
        return docker.images.bukkit
    }

    async getContainerPort() {
        const serverType = await this.getServerType()
        return Server.defaultPort[serverType]
    }

    async getExposedPort() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.port) {
            return this.getContainerPort()
        } else {
            return conf.value.port
        }
    }

    async getMemoryConfig() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.memory) {
            return Server.defaultMemory
        } else {
            return conf.value.memory
        }
    }

    async getRestConfig(): Promise<RestConfig> {
        const conf = await this.getServerConfig()
        const defaultConfig = {
            port: Server.restPort,
            password: Server.restPassword,
        }

        if (conf.isNothing || !conf.value.restEndpoint) {
            return defaultConfig
        } else {
            return { ...defaultConfig, ...conf.value.restEndpoint }
        }
    }

    async getEnvironment() {
        const memory = await this.getMemoryConfig()
        const restConfig = await this.getRestConfig()
        const env = [] as any
        env.push(`-e SERVERMEM=${memory}`)
        env.push(`-e MINECRAFT_REST_CONSOLE_PORT=${restConfig.port}`)
        env.push(`-e MINECRAFT_REST_CONSOLE_API_KEY=${restConfig.password}`)
        return env.join(' ')
    }

    private async getCustomBindings() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.bind) {
            return []
        } else {
            return conf.value.bind
        }
    }

    private checkForConfigFile(filename: string) {
        const cwd = process.cwd()
        const pkgPath = path.join(cwd, filename)
        if (!fs.existsSync(pkgPath)) {
            return undefined
        } else {
            return pkgPath
        }
    }

    private async getServerConfig() {
        if (this.serverConfig) {
            return this.serverConfig
        }
        const pkgPath = this.filename
            ? this.checkForConfigFile(this.filename)
            : this.checkForConfigFile('smac.json') ||
              this.checkForConfigFile('package.json')
        if (!pkgPath) {
            this.serverConfig = Promise.resolve(new Nothing())
            return new Nothing()
        }
        const md = await import(pkgPath)
        if (!md.smaServerConfig) {
            this.serverConfig = Promise.resolve(new Nothing())
            return new Nothing()
        }
        console.log(`Loading settings from ${pkgPath}`)
        this.serverConfig = Promise.resolve(
            new Result<SMAServerConfig>(md.smaServerConfig)
        )

        return new Result<SMAServerConfig>(md.smaServerConfig)
    }

    private async getWorldDefinitions() {
        const conf = await this.getServerConfig()
        if (conf.isNothing || !conf.value.worlds) {
            return new Nothing()
        } else {
            return new Result(conf.value.worlds)
        }
    }
}

export const server = new Server()
