/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/
package database

import (
	"errors"
	"math/rand"
	"strings"
	"time"
)

type DBType string

const (
	MariaDBType    DBType = "maria"
	MysqlDBType    DBType = "mysql"
	PostgresDBType DBType = "postgres"
)

var (
	ErrIndexOutOfBound         = errors.New("index out of bound")
	ErrNoTableRows             = errors.New("no row left to do Fetch()")
	ErrNoDBTransaction         = errors.New("no transaction has been started")
	ErrNoDBSession             = errors.New("no db session has been started")
	ErrNoDBConn                = errors.New("no connection in the pool to reconnect")
	ErrDuplicatedDBTransaction = errors.New("no duplicated transaction has supported")
	ErrDBConnLocked            = errors.New("database connection locked")
	ErrDBDown                  = errors.New("database connection is downed")
	ErrDBConfigNil             = errors.New("db config is nil")
	ErrDBConnNil               = errors.New("db connection is nil")
	ErrDBResultNil             = errors.New("database result nil")
	ErrNoAvailableDBConn       = errors.New("no connections available in pool")
)

type Database interface {
	Close() // close connections from pool
	IsConnClosed(index int) bool
	ConnReconnect(index int) error
	GetPoolSize() int
	GetConn() (DBConnection, error) // gets the connect from pool or makes the new connection if there is no conn in pool
	GetConnFromPool(index int) (DBConnection, error)
	GetDBConfig() *DBConfig
}

type DBConnection interface {
	Begin() error                                   //
	Commit() error                                  //
	Rollback() error                                //
	Execute(format string, arg ...any) (int, error) //
	Select(format string, arg ...any) (DBresult, error)
	StartSession() (DBConnection, error) // start database session
	EndSession() error                   // end database session. errors on DBDown or fails to reset the connection
	Reconnect(config *DBConfig) error    // recovery the db connection
	IsAlive() bool                       // checks whether db connection is alive or not
	IsClosed() bool                      // checks whether db connection is closed or not
	Close()
	DBErrCode() string    //
	DBErrMessage() string //
}

type ResultStatusProvider interface {
	DBResultStatus() int
}

func GetConnFromPool(db Database) (DBConnection, error) {
	idx := rand.Intn(db.GetPoolSize())
	loopCount := 0
	for {
		conn, err := db.GetConnFromPool(idx)
		if err == nil {
			return conn, nil
		}

		if errors.Is(err, ErrDBConnLocked) {
			if loopCount >= db.GetPoolSize() {
				// every connections have been tried and there are no valid connections
				return nil, ErrNoAvailableDBConn
			}

			// Move to the next index
			idx = (idx + 1) % db.GetPoolSize()
			loopCount++
			continue
		}

		return nil, err
	}
}

type DBresult interface {
	Fetch() (map[string]string, error)
	HasNextRow() bool
	Free()
}

type DBConfig struct {
	dbPort, maxConCount                                                                int
	tlsMode, tlsCertFile, tlsKeyFile, tlsCaFile, tlsCipher, tlsCipher13, mysqlDBSocket string
	dbHostname, dbUser, dbPasswd, dbName                                               string
	poolWaitInterval                                                                   time.Duration
}

func NewDBConfig() *DBConfig {
	return &DBConfig{
		poolWaitInterval: 1 * time.Second, // set default value
		maxConCount:      1,               // deafult max connection
	}
}

// DBconfig's setters
// Setters
func (cfg *DBConfig) SetDBPort(port int) {
	cfg.dbPort = port
}

func (cfg *DBConfig) SetMaxConCount(count int) {
	cfg.maxConCount = count
}

func (cfg *DBConfig) SetTLSMode(mode string) {
	cfg.tlsMode = mode
}

func (cfg *DBConfig) SetTLSCertFile(file string) {
	cfg.tlsCertFile = file
}

func (cfg *DBConfig) SetTLSKeyFile(file string) {
	cfg.tlsKeyFile = file
}

func (cfg *DBConfig) SetTLSCaFile(file string) {
	cfg.tlsCaFile = file
}

func (cfg *DBConfig) SetTLSCipher(cipher string) {
	cfg.tlsCipher = cipher
}

func (cfg *DBConfig) SetTLSCipher13(cipher string) {
	cfg.tlsCipher13 = cipher
}

func (cfg *DBConfig) SetDBHostname(hostname string) {
	cfg.dbHostname = hostname
}

func (cfg *DBConfig) SetDBUser(user string) {
	cfg.dbUser = user
}

func (cfg *DBConfig) SetDBPasswd(passwd string) {
	cfg.dbPasswd = passwd
}

func (cfg *DBConfig) SetDBName(name string) {
	cfg.dbName = name
}

func (cfg *DBConfig) SetPoolWaitInterval(interval time.Duration) {
	cfg.poolWaitInterval = interval
}

func (cfg *DBConfig) SetMysqlDBSocket(path string) {
	cfg.mysqlDBSocket = path
}

// DBconfig's getters
func (cfg *DBConfig) Hostname() string {
	return cfg.dbHostname
}

func (cfg *DBConfig) MysqlDBSocket() string {
	return cfg.mysqlDBSocket
}

func (cfg *DBConfig) User() string {
	return cfg.dbUser
}

func (cfg *DBConfig) Password() string {
	return cfg.dbPasswd
}

func (cfg *DBConfig) DBName() string {
	return cfg.dbName
}

func (cfg *DBConfig) Port() int {
	return cfg.dbPort
}

func (cfg *DBConfig) MaxConCount() int {
	return cfg.maxConCount
}

func (cfg *DBConfig) TLSMode() string {
	return cfg.tlsMode
}

func (cfg *DBConfig) TLSCertFile() string {
	return cfg.tlsCertFile
}

func (cfg *DBConfig) TLSKeyFile() string {
	return cfg.tlsKeyFile
}

func (cfg *DBConfig) TLSCaFile() string {
	return cfg.tlsCaFile
}

func (cfg *DBConfig) TLSCipher() string {
	return cfg.tlsCipher
}

func (cfg *DBConfig) TLSCipher13() string {
	return cfg.tlsCipher13
}

func (cfg *DBConfig) PoolWaitInterval() time.Duration {
	return cfg.poolWaitInterval
}

func ReplaceRune(s string, old rune, new string) string {
	var b strings.Builder

	for _, r := range s {
		if r == old {
			b.WriteString(new)
		} else {
			b.WriteRune(r)
		}
	}
	return b.String()
}
