/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package dal

import (
	"database/sql"
	"fmt"
	"reflect"

	"github.com/apache/incubator-devlake/core/errors"
)

const (
	Varchar ColumnType = "varchar(255)"
	Text    ColumnType = "text"
	Int     ColumnType = "bigint"
	Time    ColumnType = "timestamp"
	Float   ColumnType = "float"
)

var columnTypes = map[string]ColumnType{
	Varchar.String(): Varchar,
	Text.String():    Text,
	Int.String():     Int,
	Time.String():    Time,
	Float.String():   Float,
}

type ColumnType string

func (c ColumnType) String() string {
	return string(c)
}

// ToColumnType converts a string to ColumnType
func ToColumnType(s string) (ColumnType, bool) {
	t, ok := columnTypes[s]
	return t, ok
}

type Tabler interface {
	TableName() string
}

// DefaultTabler is working for the Tabler interface witch only need TableName
type DefaultTabler struct {
	Name string
}

var _ Tabler = (*DefaultTabler)(nil)

func (d DefaultTabler) TableName() string {
	return d.Name
}

// Clause represents SQL Clause
type Clause struct {
	Type string
	Data interface{}
}

// ClauseColumn quote with name
type ClauseColumn struct {
	Table string
	Name  string
	Alias string
	Raw   bool
}

// ClauseTable quote with name
type ClauseTable struct {
	Name  string
	Alias string
	Raw   bool
}

// ColumnMeta column type interface
type ColumnMeta interface {
	Name() string
	DatabaseTypeName() string                 // varchar
	ColumnType() (columnType string, ok bool) // varchar(64)
	PrimaryKey() (isPrimaryKey bool, ok bool)
	AutoIncrement() (isAutoIncrement bool, ok bool)
	Length() (length int64, ok bool)
	DecimalSize() (precision int64, scale int64, ok bool)
	Nullable() (nullable bool, ok bool)
	Unique() (unique bool, ok bool)
	ScanType() reflect.Type
	Comment() (value string, ok bool)
	DefaultValue() (value string, ok bool)
}

// SessionConfig specify options for the session
type SessionConfig struct {
	PrepareStmt            bool
	SkipDefaultTransaction bool
}

// Dal aims to facilitate an isolation between DBS and our System by defining a set of operations should a DBS provide
type Dal interface {
	// AutoMigrate runs auto migration for given entity
	AutoMigrate(entity interface{}, clauses ...Clause) errors.Error
	// AddColumn add column for the table
	AddColumn(table, columnName string, columnType ColumnType) errors.Error
	// DropColumns drop column from the table
	DropColumns(table string, columnName ...string) errors.Error
	// Exec executes raw sql query
	Exec(query string, params ...interface{}) errors.Error
	// Cursor returns a database cursor, cursor is especially useful when handling big amount of rows of data
	Cursor(clauses ...Clause) (Rows, errors.Error)
	// Fetch loads row data from `cursor` into `dst`
	Fetch(cursor Rows, dst interface{}) errors.Error
	// All loads matched rows from database to `dst`, USE IT WITH CAUTIOUS!!
	All(dst interface{}, clauses ...Clause) errors.Error
	// First loads the first matched row from database to `dst`, error will be returned if no records were found
	First(dst interface{}, clauses ...Clause) errors.Error
	// Count matched rows from database
	Count(clauses ...Clause) (int64, errors.Error)
	// Pluck used to query single column
	Pluck(column string, dest interface{}, clauses ...Clause) errors.Error
	// Create insert record to database
	Create(entity interface{}, clauses ...Clause) errors.Error
	// CreateWithMap insert record to database, the record is organized as map
	CreateWithMap(entity interface{}, record map[string]interface{}) errors.Error
	// Update updates record
	Update(entity interface{}, clauses ...Clause) errors.Error
	// UpdateColumn allows you to update multiple records
	UpdateColumn(entityOrTable interface{}, columnName string, value interface{}, clauses ...Clause) errors.Error
	// UpdateColumns allows you to update multiple columns of multiple records
	UpdateColumns(entityOrTable interface{}, set []DalSet, clauses ...Clause) errors.Error
	// UpdateAllColumn updated all Columns of entity
	UpdateAllColumn(entity interface{}, clauses ...Clause) errors.Error
	// CreateOrUpdate tries to create the record, or fallback to update all if failed
	CreateOrUpdate(entity interface{}, clauses ...Clause) errors.Error
	// CreateIfNotExist tries to create the record if not exist
	CreateIfNotExist(entity interface{}, clauses ...Clause) errors.Error
	// Delete records from database
	Delete(entity interface{}, clauses ...Clause) errors.Error
	// AllTables returns all tables in database
	AllTables() ([]string, errors.Error)
	// DropTables drops all specified tables
	DropTables(dst ...interface{}) errors.Error
	// HasTable checks if table exists
	HasTable(table interface{}) bool
	// HasColumn checks if column exists
	HasColumn(table interface{}, columnName string) bool
	// RenameTable renames table name
	RenameTable(oldName, newName string) errors.Error
	// GetColumns returns table columns in database
	GetColumns(dst Tabler, filter func(columnMeta ColumnMeta) bool) (cms []ColumnMeta, err errors.Error)
	// GetPrimaryKeyFields get the PrimaryKey from `gorm` tag
	GetPrimaryKeyFields(t reflect.Type) []reflect.StructField
	// RenameColumn renames column name for specified table
	RenameColumn(table, oldColumnName, newColumnName string) errors.Error
	// ModifyColumnType modifies column type
	ModifyColumnType(table, columnName, columnType string) errors.Error
	// DropIndexes drops indexes by their name
	DropIndexes(table string, indexes ...string) errors.Error
	// DropIndex drops the index of specified column names
	DropIndex(table string, columnNames ...string) errors.Error
	// Dialect returns the dialect of current database
	Dialect() string
	// Session creates a new manual session for special scenarios
	Session(config SessionConfig) Dal
	// Begin create a new transaction
	Begin() Transaction
	// IsErrorNotFound returns true if error is record-not-found
	IsErrorNotFound(err error) bool
	// IsDuplicationError returns true if error is duplicate-error
	IsDuplicationError(err error) bool
	// RawCursor (Deprecated) executes raw sql query and returns a database cursor.
	RawCursor(query string, params ...interface{}) (*sql.Rows, errors.Error)
}

type LockTable struct {
	Table     interface{}
	Exclusive bool
}

func (l *LockTable) TableName() string {
	switch table := l.Table.(type) {
	case Tabler:
		return table.TableName()
	case string:
		return table
	default:
		panic("either Table or Tabler must be specified")
	}
}

type LockTables []*LockTable

type Transaction interface {
	Dal
	Rollback() errors.Error
	Commit() errors.Error
	// table: exclusive
	LockTables(lockTables LockTables) errors.Error
	UnlockTables() errors.Error
	// End(err *errors.Error)
}

type Rows interface {
	// Next prepares the next result row for reading with the Scan method. It
	// returns true on success, or false if there is no next result row or an error
	// happened while preparing it. Err should be consulted to distinguish between
	// the two cases.
	//
	// Every call to Scan, even the first one, must be preceded by a call to Next.
	Next() bool

	// Close closes the Rows, preventing further enumeration. If Next is called
	// and returns false and there are no further result sets,
	// the Rows are closed automatically and it will suffice to check the
	// result of Err. Close is idempotent and does not affect the result of Err.
	Close() error

	// Scan copies the columns in the current row into the values pointed at by dest.
	// The number of values in dest must be the same as the number of columns in Rows.
	Scan(dest ...any) error

	// Columns returns the column names.
	// Columns returns an error if the rows are closed.
	Columns() ([]string, error)

	// ColumnTypes returns column information such as column type, length,
	// and nullable. Some information may not be available from some drivers.
	ColumnTypes() ([]*sql.ColumnType, error)

	// Err returns the error, if any, that was encountered during iteration.
	// Err may be called after an explicit or implicit Close method.
	Err() error
}

// GetColumnNames returns table Column Names in database
func GetColumnNames(d Dal, dst Tabler, filter func(columnMeta ColumnMeta) bool) (names []string, err errors.Error) {
	columns, err := d.GetColumns(dst, filter)
	if err != nil {
		return
	}
	for _, pkColumn := range columns {
		names = append(names, pkColumn.Name())
	}
	return
}

// GetPrimarykeyColumns get returns PrimaryKey table Meta in database
func GetPrimarykeyColumns(d Dal, dst Tabler) ([]ColumnMeta, errors.Error) {
	return d.GetColumns(dst, func(columnMeta ColumnMeta) bool {
		isPrimaryKey, ok := columnMeta.PrimaryKey()
		return isPrimaryKey && ok
	})
}

// GetPrimarykeyColumnNames get returns PrimaryKey Column Names in database
func GetPrimarykeyColumnNames(d Dal, dst Tabler) (names []string, err errors.Error) {
	pkColumns, err := GetPrimarykeyColumns(d, dst)
	if err != nil {
		return
	}
	for _, pkColumn := range pkColumns {
		// in case the column name is a reserved identifier
		names = append(names, fmt.Sprintf("%s.%s", dst.TableName(), pkColumn.Name()))
	}
	return
}

type DalClause struct {
	Expr   string
	Params []interface{}
}

type DalSet struct {
	ColumnName string
	Value      interface{}
}

const JoinClause string = "Join"

// Join creates a new JoinClause
func Join(clause string, params ...interface{}) Clause {
	return Clause{Type: JoinClause, Data: DalClause{clause, params}}
}

const WhereClause string = "Where"

// Where creates a new WhereClause
func Where(clause string, params ...interface{}) Clause {
	return Clause{Type: WhereClause, Data: DalClause{clause, params}}
}

const LimitClause string = "Limit"

// Limit creates a new LimitClause
func Limit(limit int) Clause {
	return Clause{Type: LimitClause, Data: limit}
}

const OffsetClause string = "Offset"

// Offset creates a new OffsetClause
func Offset(offset int) Clause {
	return Clause{Type: OffsetClause, Data: offset}
}

const FromClause string = "From"

// From creates a new TableClause
func From(table interface{}, params ...interface{}) Clause {
	if len(params) == 0 {
		return Clause{Type: FromClause, Data: table}
	} else {
		return Clause{Type: FromClause, Data: DalClause{table.(string), params}}
	}
}

const SelectClause string = "Select"

// Select creates a new TableClause
func Select(clause string, params ...interface{}) Clause {
	return Clause{Type: SelectClause, Data: DalClause{clause, params}}
}

const OrderbyClause string = "OrderBy"

// Orderby creates a new Orderby clause
func Orderby(expr string) Clause {
	return Clause{Type: OrderbyClause, Data: expr}
}

const GroupbyClause string = "GroupBy"

// Groupby creates a new Groupby clause
func Groupby(expr string) Clause {
	return Clause{Type: GroupbyClause, Data: expr}
}

const HavingClause string = "Having"

// Having creates a new Having clause
func Having(clause string, params ...interface{}) Clause {
	return Clause{Type: HavingClause, Data: DalClause{clause, params}}
}

const LockClause string = "Lock"

// Having creates a new Having clause
func Lock(write bool, nowait bool) Clause {
	return Clause{Type: LockClause, Data: []bool{write, nowait}}
}

func Expr(expr string, params ...interface{}) DalClause {
	return DalClause{Expr: expr, Params: params}
}
