import {
    Awaitable,
    AwareOfContainerLifecycle, ErrorWithContext,
    Filesystem,
    infer,
    objectToKeyValue,
    UniversalPath,
    uuid4,
} from '@extollo/lib'
import {CommandPalette, CommandPaletteCommands, OwnershipSpec, shellCommand, ShellCommand} from './types'
import {ExecutionResult} from './ExecutionResult'
import {CommandError} from './errors'
import {SystemMetrics} from './SystemMetrics'

export class StandardCommandPalette implements CommandPalette {
    changeDirectory = shellCommand(`cd "%%PATH%%"`)
    cpuPercentage = shellCommand(`grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage}'`)
    fileDirectoryDelete = shellCommand(`rm -rf "%%RESOURCE%%"`)
    fileDirectoryOwnershipFetch = shellCommand(`stat -c "%U:%G" "%%PATH%%"`)
    fileDirectoryOwnershipSetFlat = shellCommand(`chown %%OWNERS%% "%%PATH%%"`)
    fileDirectoryOwnershipSetRecursive = shellCommand(`chown -R %%OWNERS%% "%%PATH%%"`)
    fileDirectoryPermissionFetch = shellCommand(`stat -c %a "%%PATH%%"`)
    fileDirectoryPermissionSetFlat = shellCommand(`chmod %%LEVEL%% "%%PATH%%"`)
    fileDirectoryPermissionSetRecursive = shellCommand(`chmod -R %%LEVEL%% "%%PATH%%"`)
    listMountPoints = shellCommand(`df -hl | grep '/' | awk '{print $6}`)
    mountPointPercentage = shellCommand(`df -hl | grep -w '%%MOUNTPOINT%%$' | awk '{print $5}'`)
    ramPercentage = shellCommand(`free | grep Mem | awk '{print $3/$2 * 100.0}'`)
    reboot = shellCommand(`reboot`)
    resolvePath = shellCommand(`readlink -f "%%PATH%%"`)
    tempFile = shellCommand(`mktemp`)
    tempPath = shellCommand(`mktemp -d`)
    echo = shellCommand(`echo "%%OUTPUT%%"`)

    public format(command: keyof CommandPaletteCommands, substitutions: Record<string, any>): ShellCommand {
        let formatted = `${this[command]}`

        objectToKeyValue(substitutions)
            .map(item =>
                formatted = formatted.replaceAll(`%%${item.key.toUpperCase()}%%`, `${item.value}`))

        return shellCommand(formatted)
    }
}

export abstract class Host implements AwareOfContainerLifecycle {
    awareOfContainerLifecycle: true = true

    public abstract getFilesystem(): Awaitable<Filesystem>

    public abstract execute(command: ShellCommand): Awaitable<ExecutionResult>

    close(): Awaitable<void> {}

    protected getCommandPalette(): CommandPalette {
        return new StandardCommandPalette()
    }

    onContainerDestroy() {
        this.close()
    }

    onContainerRelease() {
        this.close()
    }

    /** Runs the provided command and throws if it failed. */
    public async run(command: ShellCommand): Promise<ExecutionResult> {
        const result = await this.execute(command)
        if ( !result.wasSuccessful() ) {
            throw CommandError.make(command, result)
        }
        return result
    }

    /** Runs the provided command, throws if it failed, and returns the `infer(...)` result of the first line otherwise. */
    public async runLineResult(command: ShellCommand): Promise<ReturnType<typeof infer>> {
        const result = await this.execute(command)
        if ( !result.wasSuccessful() || result.standardOutput.length < 1 ) {
            throw CommandError.make(command, result)
        }
        return infer(result.standardOutput[0].trim())
    }

    /** Try to ping the host and verify that it is online & accessible. */
    public async isAlive(): Promise<boolean> {
        const output = uuid4()
        const cmd = this.getCommandPalette().format('echo', { output })
        try {
            const result = await this.execute(cmd)
            return result.wasSuccessful() && result.combinedOutput.length > 0 && result.combinedOutput[0] === output
        } catch (e) {
            return false
        }
    }

    public async waitForAlive(maxTries: number = 30): Promise<void> {
        let tries = maxTries
        while ( tries > 0 ) {
            if ( await this.isAlive() ) {
                return
            }

            tries -= 1
            await new Promise<void>(res => setTimeout(() => res(), 2000))
        }

        throw new ErrorWithContext('Host did not come online before maxTries exceeded', {
            host: this,
            maxTries,
        })
    }

    /** Get the path to a temporary directory on the host. */
    public async getTempDirectory(): Promise<UniversalPath> {
        const cmd = this.getCommandPalette().format('tempPath', {})
        const result = await this.runLineResult(cmd)
        return new UniversalPath(`${result}`, await this.getFilesystem())
    }

    /** Get the path to a temporary file on the host. */
    public async getTempFile(): Promise<UniversalPath> {
        const cmd = this.getCommandPalette().format('tempFile', {})
        const result = await this.runLineResult(cmd)
        return new UniversalPath(`${result}`, await this.getFilesystem())
    }

    /** Get a list of filesystem mount points on the host. */
    public async getMountPoints(): Promise<string[]> {
        const result = await this.run(this.getCommandPalette().format('listMountPoints', {}))
        return result.standardOutput
    }

    /** Load standard metrics about the host. */
    public async metrics(): Promise<SystemMetrics> {
        const metric = new SystemMetrics()

        const palette = this.getCommandPalette()
        const cpuCmd = palette.format('cpuPercentage', {})
        const cpuResult = await this.runLineResult(cpuCmd)
        if ( typeof cpuResult === 'number' ) {
            metric.cpuPercent = cpuResult
        }

        const ramCmd = palette.format('ramPercentage', {})
        const ramResult = await this.runLineResult(ramCmd)
        if ( typeof ramResult === 'number' ) {
            metric.ramPercent = ramResult
        }

        const mountPoints = await this.getMountPoints()
        await Promise.all(mountPoints.map(async mountPoint => {
            const cmd = palette.format('mountPointPercentage', { mountPoint })
            const result = await this.run(cmd)
            const utilization = Number(result.standardOutput[0].replace('%', '')) / 100
            metric.setMount(mountPoint, utilization)
        }))

        return metric
    }

    /** Get the user/group ownership of the given resource. */
    public async getOwnership(path: UniversalPath): Promise<OwnershipSpec> {
        const cmd = this.getCommandPalette().format('fileDirectoryOwnershipFetch', { path: path.toLocal })
        const result = `${await this.runLineResult(cmd)}`
        const parts = result.split(':')
        return {user: parts[0], group: parts[1]}
    }

    /** Set the user/group ownership of the given resource. */
    public setOwnership(path: UniversalPath, user: string, group: string, recursive: boolean = false): Awaitable<ExecutionResult> {
        const cmd = this.getCommandPalette().format(
            recursive ? 'fileDirectoryOwnershipSetRecursive' : 'fileDirectoryOwnershipSetFlat',
            {
                path: path.toLocal,
                owners: `${user}:${group}`,
            }
        )
        return this.run(cmd)
    }

    /** Retrieve the filesystem permissions for the given resource. */
    public async getPermissions(path: UniversalPath): Promise<string> {
        const cmd = this.getCommandPalette().format('fileDirectoryPermissionFetch', { path: path.toLocal })
        return `${await this.runLineResult(cmd)}`
    }

    /** Set the filesystem permissions for the given resource. */
    public setPermissions(path: UniversalPath, level: string, recursive: boolean = false): Awaitable<ExecutionResult> {
        const cmd = this.getCommandPalette().format(
            recursive ? 'fileDirectoryPermissionSetRecursive' : 'fileDirectoryPermissionSetFlat',
            {
                path: path.toLocal,
                level,
            }
        )
        return this.run(cmd)
    }

    /** Reboot the host. */
    public reboot(): Awaitable<ExecutionResult> {
        return this.run(this.getCommandPalette().format('reboot', {}))
    }
}
