package sqlg

import (
	"fmt"
	"time"

	"github.com/gofrs/uuid/v5"
	"github.com/huandu/xstrings"
	"github.com/sorintlab/errors"

	"agola.io/agola/internal/sqlg/sql"
)

type Object interface {
	GetID() string
}

type Initer interface {
	Init() error
}

type PreJSONSetupper interface {
	PreJSON() error
}

type ExportMeta struct {
	Kind string `json:"kind"`
}

type ObjectMeta struct {
	// ID is the unique ID of the object.
	ID string `json:"id"`

	// CreationTime represents the time when this object has been created.
	CreationTime time.Time `json:"creationTime"`

	// UpdateTime represents the time when this object has been created/updated.
	UpdateTime time.Time `json:"updateTime"`

	// Revision is the object revision, it's not saved in the object but
	// populated by the fetch from the database
	Revision uint64 `json:"-"`

	// TxID is the current transaction id, used internally and must not be saved in the object
	TxID string `json:"-"`
}

func NewObjectMeta(tx *sql.Tx) ObjectMeta {
	return ObjectMeta{
		ID:   uuid.Must(uuid.NewV4()).String(),
		TxID: tx.ID(),
	}
}

type Sequence struct {
	Name   string
	Table  string
	Column string
}

func (m *ObjectMeta) GetID() string {
	return m.ID
}

var ErrConcurrent = errors.New("concurrent update")

type MigrateFunc func(tx *sql.Tx) error

type ObjectField struct {
	Name     string
	ColName  string
	Type     string
	BaseType string
	SQLType  string
	Nullable bool
	Unique   bool
	Sequence bool
	JSON     bool
}

type ObjectInfo struct {
	Name   string
	Table  string
	Fields []ObjectField
	// TODO(sgotti) instead of a pure ddl, use data to generate it
	Constraints []string
	// TODO(sgotti) instead of a pure ddl, use data to generate it
	Indexes []string
}

func ObjectNames(objectInfos []ObjectInfo) []string {
	names := []string{}
	for _, objectInfo := range objectInfos {
		names = append(names, objectInfo.Name)
	}

	return names
}

func (oi ObjectInfo) PopulatePostgres() ObjectInfo {
	for i, field := range oi.Fields {
		if field.JSON {
			field.SQLType = "jsonb"
		}
		if field.Sequence {
			field.SQLType = "bigint generated by default as identity"
			field.Unique = true
		}

		if field.SQLType == "" {
			switch field.BaseType {
			case "string":
				field.SQLType = "varchar"
			case "bool":
				field.SQLType = "boolean"
			case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "byte", "rune":
				field.SQLType = "bigint"
			case "float32":
				field.SQLType = "real"
			case "float64":
				field.SQLType = "double precision"
			case "time.Time":
				field.SQLType = "timestamptz"
			case "time.Duration":
				field.SQLType = "bigint"
			case "[]byte":
				field.SQLType = "bytea"

			default:
				panic(fmt.Errorf("unknown field type: %q", field.BaseType))
			}
		}

		oi.Fields[i] = field
	}

	return oi
}

func (oi ObjectInfo) PopulateSqlite3() ObjectInfo {
	for i, field := range oi.Fields {
		if field.JSON {
			field.SQLType = "text"
		}

		if field.Sequence {
			field.SQLType = "integer"
			field.Unique = true
		}

		if field.SQLType == "" {
			switch field.BaseType {
			case "string":
				field.SQLType = "varchar"
			case "bool":
				field.SQLType = "integer"
			case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "byte", "rune":
				field.SQLType = "bigint"
			case "float32":
				field.SQLType = "real"
			case "float64":
				field.SQLType = "double precision"
			case "time.Time":
				// timestamp, datatime and date are declared column types supported by the go-sqlite3 driver
				field.SQLType = "timestamp"
			case "time.Duration":
				field.SQLType = "bigint"
			case "[]byte":
				field.SQLType = "blob"

			default:
				panic(fmt.Errorf("unknown field type: %q", field.BaseType))
			}
		}

		if field.SQLType == "serial" {
			field.Nullable = false
		}

		oi.Fields[i] = field
	}

	return oi
}

func PopulateObjectsInfo(inObjectsInfo []ObjectInfo, dbType sql.Type) []ObjectInfo {
	objectsInfo := make([]ObjectInfo, len(inObjectsInfo))

	for i, oi := range inObjectsInfo {
		for i, field := range oi.Fields {
			if field.ColName == "" {
				field.ColName = xstrings.ToSnakeCase(field.Name)
			}

			if field.BaseType == "" {
				field.BaseType = field.Type
			}

			oi.Fields[i] = field
		}

		switch dbType {
		case sql.Postgres:
			oi = oi.PopulatePostgres()
		case sql.Sqlite3:
			oi = oi.PopulateSqlite3()
		default:
		}

		objectsInfo[i] = oi
	}

	return objectsInfo
}
