// Copyright 2019 Mikhail Klementev. All rights reserved.
// Use of this source code is governed by a AGPLv3 license
// (or later) that can be found in the LICENSE file.

package main

import (
	"database/sql"
	"fmt"
	"strconv"
	"time"

	_ "github.com/mattn/go-sqlite3"

	"code.dumpstack.io/tools/out-of-tree/config"
	"code.dumpstack.io/tools/out-of-tree/qemu"
)

// Change on ANY database update
const currentDatabaseVersion = 2

const versionField = "db_version"

type logEntry struct {
	ID        int
	Tag       string
	Timestamp time.Time

	qemu.System
	config.Artifact
	config.KernelInfo
	phasesResult
}

func createLogTable(db *sql.DB) (err error) {
	_, err = db.Exec(`
	CREATE TABLE IF NOT EXISTS log (
		id		INTEGER PRIMARY KEY,
		time		DATETIME DEFAULT CURRENT_TIMESTAMP,
		tag		TEXT,

		name		TEXT,
		type		TEXT,

		distro_type	TEXT,
		distro_release	TEXT,
		kernel_release	TEXT,

		build_output	TEXT,
		build_ok	BOOLEAN,

		run_output	TEXT,
		run_ok		BOOLEAN,

		test_output	TEXT,
		test_ok		BOOLEAN,

		qemu_stdout	TEXT,
		qemu_stderr	TEXT,

		kernel_panic	BOOLEAN,
		timeout_kill	BOOLEAN
	)`)
	return
}

func createMetadataTable(db *sql.DB) (err error) {
	_, err = db.Exec(`
	CREATE TABLE IF NOT EXISTS metadata (
		id	INTEGER PRIMARY KEY,
		key	TEXT UNIQUE,
		value	TEXT
	)`)
	return
}

func metaChkValue(db *sql.DB, key string) (exist bool, err error) {
	sql := "SELECT EXISTS(SELECT id FROM metadata WHERE key = $1)"
	stmt, err := db.Prepare(sql)
	if err != nil {
		return
	}
	defer stmt.Close()

	err = stmt.QueryRow(key).Scan(&exist)
	return
}

func metaGetValue(db *sql.DB, key string) (value string, err error) {
	stmt, err := db.Prepare("SELECT value FROM metadata " +
		"WHERE key = $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	err = stmt.QueryRow(key).Scan(&value)
	return
}

func metaSetValue(db *sql.DB, key, value string) (err error) {
	stmt, err := db.Prepare("INSERT OR REPLACE INTO metadata " +
		"(key, value) VALUES ($1, $2)")
	if err != nil {
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(key, value)
	return
}

func getVersion(db *sql.DB) (version int, err error) {
	s, err := metaGetValue(db, versionField)
	if err != nil {
		return
	}

	version, err = strconv.Atoi(s)
	return
}

func addToLog(db *sql.DB, q *qemu.System, ka config.Artifact,
	ki config.KernelInfo, res *phasesResult, tag string) (err error) {

	stmt, err := db.Prepare("INSERT INTO log (name, type, tag, " +
		"distro_type, distro_release, kernel_release, " +
		"build_output, build_ok, " +
		"run_output, run_ok, " +
		"test_output, test_ok, " +
		"qemu_stdout, qemu_stderr, " +
		"kernel_panic, timeout_kill) " +
		"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, " +
		"$10, $11, $12, $13, $14, $15, $16);")
	if err != nil {
		return
	}

	defer stmt.Close()

	_, err = stmt.Exec(
		ka.Name, ka.Type, tag,
		ki.DistroType, ki.DistroRelease, ki.KernelRelease,
		res.Build.Output, res.Build.Ok,
		res.Run.Output, res.Run.Ok,
		res.Test.Output, res.Test.Ok,
		q.Stdout, q.Stderr,
		q.KernelPanic, q.KilledByTimeout,
	)
	if err != nil {
		return
	}

	return
}

func getAllLogs(db *sql.DB, tag string, num int) (les []logEntry, err error) {
	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " +
		"distro_type, distro_release, kernel_release, " +
		"build_ok, run_ok, test_ok, kernel_panic, " +
		"timeout_kill FROM log ORDER BY datetime(time) DESC " +
		"LIMIT $1")
	if err != nil {
		return
	}
	defer stmt.Close()

	rows, err := stmt.Query(num)
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		le := logEntry{}
		err = rows.Scan(&le.ID, &le.Timestamp,
			&le.Name, &le.Type, &le.Tag,
			&le.DistroType, &le.DistroRelease, &le.KernelRelease,
			&le.Build.Ok, &le.Run.Ok, &le.Test.Ok,
			&le.KernelPanic, &le.KilledByTimeout,
		)
		if err != nil {
			return
		}

		if tag == "" || tag == le.Tag {
			les = append(les, le)
		}
	}

	return
}

func getAllArtifactLogs(db *sql.DB, tag string, num int, ka config.Artifact) (
	les []logEntry, err error) {

	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " +
		"distro_type, distro_release, kernel_release, " +
		"build_ok, run_ok, test_ok, kernel_panic, " +
		"timeout_kill FROM log WHERE name=$1 AND type=$2 " +
		"ORDER BY datetime(time) DESC LIMIT $3")
	if err != nil {
		return
	}
	defer stmt.Close()

	rows, err := stmt.Query(ka.Name, ka.Type, num)
	if err != nil {
		return
	}
	defer rows.Close()

	for rows.Next() {
		le := logEntry{}
		err = rows.Scan(&le.ID, &le.Timestamp,
			&le.Name, &le.Type, &le.Tag,
			&le.DistroType, &le.DistroRelease, &le.KernelRelease,
			&le.Build.Ok, &le.Run.Ok, &le.Test.Ok,
			&le.KernelPanic, &le.KilledByTimeout,
		)
		if err != nil {
			return
		}

		if tag == "" || tag == le.Tag {
			les = append(les, le)
		}
	}

	return
}

func getLogByID(db *sql.DB, id int) (le logEntry, err error) {
	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " +
		"distro_type, distro_release, kernel_release, " +
		"build_ok, run_ok, test_ok, " +
		"build_output, run_output, test_output, " +
		"qemu_stdout, qemu_stderr, " +
		"kernel_panic, timeout_kill " +
		"FROM log WHERE id=$1")
	if err != nil {
		return
	}
	defer stmt.Close()

	err = stmt.QueryRow(id).Scan(&le.ID, &le.Timestamp,
		&le.Name, &le.Type, &le.Tag,
		&le.DistroType, &le.DistroRelease, &le.KernelRelease,
		&le.Build.Ok, &le.Run.Ok, &le.Test.Ok,
		&le.Build.Output, &le.Run.Output, &le.Test.Output,
		&le.Stdout, &le.Stderr,
		&le.KernelPanic, &le.KilledByTimeout,
	)
	return
}

func getLastLog(db *sql.DB) (le logEntry, err error) {
	err = db.QueryRow("SELECT MAX(id), time, name, type, tag, "+
		"distro_type, distro_release, kernel_release, "+
		"build_ok, run_ok, test_ok, "+
		"build_output, run_output, test_output, "+
		"qemu_stdout, qemu_stderr, "+
		"kernel_panic, timeout_kill "+
		"FROM log").Scan(&le.ID, &le.Timestamp,
		&le.Name, &le.Type, &le.Tag,
		&le.DistroType, &le.DistroRelease, &le.KernelRelease,
		&le.Build.Ok, &le.Run.Ok, &le.Test.Ok,
		&le.Build.Output, &le.Run.Output, &le.Test.Output,
		&le.Stdout, &le.Stderr,
		&le.KernelPanic, &le.KilledByTimeout,
	)
	return
}

func createSchema(db *sql.DB) (err error) {
	err = createMetadataTable(db)
	if err != nil {
		return
	}

	err = createLogTable(db)
	if err != nil {
		return
	}

	return
}

func openDatabase(path string) (db *sql.DB, err error) {
	db, err = sql.Open("sqlite3", path)
	if err != nil {
		return
	}

	db.SetMaxOpenConns(1)

	exists, _ := metaChkValue(db, versionField)
	if !exists {
		err = createSchema(db)
		if err != nil {
			return
		}

		err = metaSetValue(db, versionField,
			strconv.Itoa(currentDatabaseVersion))
		return
	}

	version, err := getVersion(db)
	if err != nil {
		return
	}

	if version == 1 {
		_, err = db.Exec(`ALTER TABLE log ADD tag TEXT`)
		if err != nil {
			return
		}

		err = metaSetValue(db, versionField, "2")
		if err != nil {
			return
		}

		version = 2
	}

	if version != currentDatabaseVersion {
		err = fmt.Errorf("Database is not supported (%d instead of %d)",
			version, currentDatabaseVersion)
		return
	}

	return
}