/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor
 * license agreements; and to You under the Apache License, Version 2.0.
 */
/*
 * written by Scopevisio AG 2021
 */
import { Context } from "../context"
import loggger from "../log"

export abstract class AbstractJob {
    abstract name: string
    minSecondsBetweenRuns = 120
    logOutput = ''

    async run() {
        let dbLock
        try {
            dbLock = await Context.instance.pool.connect()

            await dbLock.query('BEGIN')
            const lock = await dbLock.query(`SELECT * FROM job WHERE name = $1 FOR UPDATE SKIP LOCKED`, [this.name])
            if (lock.rowCount != 1) {
                loggger.info(`Skipping, already running`, { label: this.name })
                return
            }

            const job: DbJob = lock.rows[0]
            if (job.latestrunts) {
                const duration = new Date().getTime() - job.latestrunts.getTime()
                if (duration <= (this.minSecondsBetweenRuns * 1000)) {
                    loggger.info(`Skipping, latest run at=${job.latestrunts}, min seconds between runs must be=${this.minSecondsBetweenRuns}`, { label: this.name })
                    return
                }
            }

            try {
                loggger.info(`Starting job run`, { label: this.name })
                const jobRunId = await this.startJobRun()
                let success
                try {
                    success = await this.doRun()
                } catch (err) {
                    this.log('error', 'Unexpected error', err)
                    success = false
                }
                const duration = await this.endJobRun(jobRunId, success ? JobRunStatus.COMPLETED : JobRunStatus.FAILED)
                if (success) {
                    loggger.info(`Finished job run. Took ${JSON.stringify(duration)}`, { label: this.name })
                } else {
                    loggger.error(`Finished job run with errors. Took ${JSON.stringify(duration)}`, { label: this.name })
                }
            } finally {
                await dbLock?.query('UPDATE job SET latestrunts = now() WHERE name = $1', [this.name])
            }

        } catch (err) { // dbLock
            loggger.error(`Job run failed`, { label: this.name, stack: err.stack })
        } finally {
            this.logOutput = ``
            try {
                await dbLock?.query('COMMIT')
            } catch (err) {
                loggger.error(`Failed to commit transaction`, { label: this.name, stack: err.stack })
            }
            dbLock?.release()
        }
    }

    abstract doRun(): Promise<boolean>

    log(level: 'info' | 'warn' | 'error', message: string, err?: Error) {
        loggger.log(level, message, { label: this.name, stack: err?.stack })
        this.logOutput += `[${level}] ${message}\n`
        if (err?.stack) {
            this.logOutput += `${err.stack}\n`
        }
    }

    private async startJobRun(): Promise<number> {
        let db
        try {
            db = await Context.instance.pool.connect()
            const r = await db.query(`INSERT INTO jobrun(
        job, startts, status
      ) VALUES(
        $1, now(), $2
      ) returning pk`, [this.name, JobRunStatus.RUNNING])

            if (r.rowCount !== 1)
                throw Error(`Failed to insert job run`)
            return r.rows[0].pk
        } finally {
            db?.release()
        }
    }

    private async endJobRun(pk: number, status: JobRunStatus) {
        let db
        try {
            db = await Context.instance.pool.connect()
            const r = await db.query(`UPDATE jobrun SET
        endts = now(),
        status = $1,
        log = $2
      WHERE
        pk = $3
      RETURNING age(endts, startts)
      `, [status, this.logOutput, pk])

            if (r.rowCount !== 1)
                throw Error(`Failed to update job run`)
            return r.rows[0].age
        } finally {
            db?.release()
        }
    }
}

export interface DbJob {
    name: string
    schedule: string
    createdts: Date
    latestrunts: Date
}

export enum JobRunStatus {
    RUNNING = 'RUNNING',
    COMPLETED = 'COMPLETED',
    FAILED = 'FAILED',
}

export interface DbJobRun {
    pk: number
    name: string
    schedule: string
    createdts: Date
    latestrunts: Date
    job: string
    startts: Date
    endts: Date
    status: JobRunStatus
    log: string
}
