const {EMPTY, from, interval, of, zip} = require('rxjs')
const {catchError, delay, exhaustMap, filter, map, concatMap, switchMap} = require('rxjs/operators')
const log = require('sepal/log').getLogger('apps')
const {fileToJson$, lastModifiedDate$} = require('./file')
const {exec$} = require('./terminal')
const {access, writeFile} = require('fs/promises')
const {basename, dirname, join} = require('path')
const {userInfo} = require('os')

const monitorApps = () =>
    interval(5000).pipe(
        exhaustMap(() => apps$().pipe(
            concatMap(app => updateApp$(app).pipe(
                delay(5000)
            ))
        ))
    ).subscribe({
        error: error => log.fatal('Monitor exited:', error),
        complete: () => log.fatal('Monitor unexpectedly completed')
    })

const apps$ = () =>
    fileToJson$('/var/sepal/app-manager/apps.json').pipe(
        switchMap(({apps}) => from(apps)),
        filter(({repository}) => repository),
        map(({endpoint = 'shiny', repository, branch}) => {
            const name = basename(repository)
            return {
                endpoint,
                name,
                path: endpoint === 'jupyter'
                    ? `/var/sepal/app-manager/apps/${name}`
                    : `/shiny/${name}`,
                repository,
                branch
            }
        }),
        catchError(error => {
            log.error('Failed to list apps:', error)
            return EMPTY
        })
    )

const updateApp$ = app => {
    log.info('Updating app', app.path)
    return exists$(app).pipe(
        switchMap(cloned => cloned ? of(true) : clone$(app)),
        switchMap(() => checkout$(app)),
        switchMap(() => pull$(app)),
        switchMap(() => useCustomKernel$(app)),
        switchMap(useCustomKernel => useCustomKernel ? updateKernel$(app) : of(false)),
        catchError(error => {
            log.error('Failed to update app:', error)
            return EMPTY
        })
    )
}

const useCustomKernel$ = ({endpoint, path}) =>
    endpoint === 'jupyter'
        ? exists$({path: join(path, 'requirements.txt')})
        : of(false)

const updateKernel$ = ({name, path}) => {
    const kernelPath = join('/usr/local/share/jupyter/kernels', `venv-${name}`)
    const venvPath = join(kernelPath, 'venv')
    return exists$({path: kernelPath}).pipe(
        switchMap(kernelExists => kernelExists ? of(true) : createKernel$({path: kernelPath})),
        switchMap(() => exists$(venvPath)),
        switchMap(venvExists => venvExists ? of(true) : createVenv$({path: venvPath})),
        switchMap(() => installRequirements$({venvPath, requirementsPath: join(path, 'requirements.txt')}))
    )
}

const createKernel$ = ({path}) => {
    const venvPath = join(path, 'venv')
    const kernel = {
        'argv': [join(venvPath, 'bin/python3'), '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
        'language': 'python'
    }

    return exec$(
        '/',
        'sudo',
        ['mkdir', '-p', path]
    ).pipe(
        switchMap(() => exec$(
            '/',
            'sudo',
            ['chown', `${userInfo().username}:`, path]
        )),
        switchMap(() => from(writeFile(join(path, 'kernel.json'), JSON.stringify(kernel))))
    )
}

const createVenv$ = ({path}) =>
    exec$(
        '/',
        'sudo',
        ['python3', '-m', 'venv', path]
    ).pipe(
        switchMap(() => exec$(
            '/',
            'sudo',
            [join(path, 'bin/pip3'), 'install', '--cache-dir', '/root/.cache/pip', 'ipykernel', 'wheel']
        )),
        switchMap(() => exec$(
            '/',
            'sudo',
            [join(path, 'bin/pip3'), 'install', '--cache-dir', '/root/.cache/pip', '--upgrade', 'pip']
        ))
    )

const installRequirements$ = ({venvPath, requirementsPath}) => {
    // If $venvPath/.installed exists, compare modified timestamp. If requirements are newer, install, otherwise, do nothing
    return zip(
        lastModifiedDate$(requirementsPath),
        lastModifiedDate$(join(venvPath, '.installed'))
    ).pipe(
        switchMap(([requirementsTime, installTime]) => {
            if (installTime < requirementsTime) {
                log.info(`${requirementsPath} modified since last installation. ${installTime} >= ${requirementsTime}`)
                return exec$(
                    '/',
                    'sudo',
                    ['install-requirements', venvPath, requirementsPath]
                )
            } else {
                log.info(`${requirementsPath} not modified since last installation. ${installTime} < ${requirementsTime}`)
                return EMPTY
            }
        })
    )
}

const exists$ = ({path}) =>
    from(access(path)).pipe(
        map(() => true),
        catchError(() => of(false))
    )

const checkout$ = ({path, branch}) => {
    return branch
        ? onBranch$({path, branch}).pipe(
            switchMap(onBranch =>
                onBranch
                    ? of(true)
                    : exec$(
                        path,
                        'sudo',
                        ['GIT_TERMINAL_PROMPT=0', 'git', 'checkout', '-t', `origin/${branch}`]
                    )),
            catchError(() => {
                log.warn(`Failed to checkout branch ${branch}: ${path}`)
                return of(false)
            })
        )
        : of(true)
}

const onBranch$ = ({path, branch}) =>
    exec$(
        path,
        'sudo',
        ['GIT_TERMINAL_PROMPT=0', 'git', 'rev-parse', '--abbrev-ref', 'HEAD']
    ).pipe(
        map(currentBranch => currentBranch.trim() === branch)
    )

const pull$ = ({path}) => {
    return exec$(
        path,
        'sudo',
        ['GIT_TERMINAL_PROMPT=0', 'git', 'pull', '--recurse-submodules']
    )
}

const clone$ = ({path, repository}) => {
    return exec$(
        '/',
        'sudo',
        ['mkdir', '-p', dirname(path)]
    ).pipe(
        switchMap(() => exec$(
            dirname(path),
            'sudo',
            ['GIT_TERMINAL_PROMPT=0', 'git', 'clone', '--recurse-submodules', repository]
        ))
    )
}

module.exports = {monitorApps}

@@ -984,16 +984,23 @@
 'shiny',
+ label,
  reposit
@@ -1133,16 +1133,39 @@
   name,%0A
+                label,%0A
         
@@ -2189,32 +2189,39 @@
 (%7Bname, path
+, label
 %7D) =%3E %7B%0A
     const ke
@@ -2200,32 +2200,32 @@
 h, label%7D) =%3E %7B%0A
-
     const kernel
@@ -2466,24 +2466,31 @@
 : kernelPath
+, label
 %7D)),%0A       
@@ -2751,32 +2751,39 @@
 Kernel$ = (%7Bpath
+, label
 %7D) =%3E %7B%0A    cons
@@ -2835,16 +2835,16 @@
 nel = %7B%0A
-
         
@@ -2939,16 +2939,57 @@
 ile%7D'%5D,%0A
+        'display_name': %60App: $%7Blabel%7D%60,%0A
         

Set kernel display_name.