package pgsql

import "strings"

var GoToStdTypes = map[string]string{
	"bool":            "boolean",
	"int64":           "bigint",
	"int":             "integer",
	"int8":            "smallint",
	"int16":           "smallint",
	"int32":           "integer",
	"uint":            "integer",
	"uint8":           "smallint",
	"uint16":          "smallint",
	"uint32":          "integer",
	"uint64":          "bigint",
	"uintptr":         "bigint",
	"znullint64":      "bigint",
	"znullint32":      "integer",
	"znullbyte":       "smallint",
	"float64":         "double",
	"float32":         "double",
	"complex64":       "double",
	"complex128":      "double",
	"customfloat64":   "double",
	"string":          "text",
	"Pointer":         "bigint",
	"[]byte":          "blob",
	"customdate":      "date",
	"customtime":      "time",
	"customtimestamp": "timestamp",
	"sqlfloat64":      "double",
	"sqlfloat16":      "double",
	"sqluuid":         "uuid",
	"sqljsonb":        "jsonb",
	"sqljson":         "json",
	"sqlint64":        "bigint",
	"sqlint32":        "integer",
	"sqlint16":        "smallint",
	"sqlbool":         "boolean",
	"sqlstring":       "text",
	"nullablejsonb":   "jsonb",
	"nullablejson":    "json",
	"nullableuuid":    "uuid",
	"sqldate":         "date",
	"sqltime":         "time",
	"sqltimestamp":    "timestamp",
	"time.Time":       "timestamp",
}

var GoToPGSQLTypes = map[string]string{
	"bool":            "boolean",
	"int64":           "bigint",
	"int":             "integer",
	"int8":            "smallint",
	"int16":           "smallint",
	"int32":           "integer",
	"uint":            "integer",
	"uint8":           "smallint",
	"uint16":          "smallint",
	"uint32":          "integer",
	"uint64":          "bigint",
	"uintptr":         "bigint",
	"znullint64":      "bigint",
	"znullint32":      "integer",
	"znullbyte":       "integer",
	"float64":         "double precision",
	"float32":         "real",
	"complex64":       "double precision",
	"complex128":      "double precision",
	"customfloat64":   "double precision",
	"string":          "text",
	"Pointer":         "bigint",
	"[]byte":          "bytea",
	"customdate":      "date",
	"customtime":      "time",
	"customtimestamp": "timestamp",
	"sqlfloat64":      "double precision",
	"sqlfloat16":      "double precision",
	"sqluuid":         "uuid",
	"sqljsonb":        "jsonb",
	"sqljson":         "json",
	"sqlint64":        "bigint",
	"sqlint32":        "integer",
	"sqlint16":        "smallint",
	"sqlbool":         "boolean",
	"sqlstring":       "text",
	"nullablejsonb":   "jsonb",
	"nullablejson":    "json",
	"nullableuuid":    "uuid",
	"sqldate":         "date",
	"sqltime":         "time",
	"sqltimestamp":    "timestamp",
	"time.Time":       "timestamp",
	"citext":          "citext",
}

func ValidSQLType(sqltype string) bool {
	for _, sql := range GoToPGSQLTypes {
		if strings.EqualFold(sql, sqltype) {
			return true
		}
	}
	for _, sql := range GoToStdTypes {
		if strings.EqualFold(sql, sqltype) {
			return true
		}
	}
	return false
}

func GetSQLType(anytype string) string {
	for gotype, sql := range GoToPGSQLTypes {
		if strings.EqualFold(gotype, anytype) || strings.EqualFold(sql, anytype) {
			return sql
		}
	}
	for gotype, sql := range GoToStdTypes {
		if strings.EqualFold(gotype, anytype) || strings.EqualFold(sql, anytype) {
			return sql
		}
	}
	return "text"
}

func ConvertSQLType(anytype string) string {
	for gotype, sql := range GoToPGSQLTypes {
		if strings.EqualFold(gotype, anytype) || strings.EqualFold(sql, anytype) {
			return sql
		}
	}
	for gotype, sql := range GoToStdTypes {
		if strings.EqualFold(gotype, anytype) || strings.EqualFold(sql, anytype) {
			return sql
		}
	}
	return anytype
}

// PGTypeCanonical maps PostgreSQL type aliases and synonyms to their canonical base name.
// Input should be a base type (no dimension parameters, lowercase).
var PGTypeCanonical = map[string]string{
	// integer aliases
	"int":  "integer",
	"int4": "integer",
	"int2": "smallint",
	"int8": "bigint",
	// float aliases
	"float4": "real",
	"float8": "double precision",
	// bool alias
	"bool": "boolean",
	// char aliases
	"character":         "char",
	"character varying": "varchar",
	"bpchar":            "char",
	// timestamp aliases
	"timestamp without time zone": "timestamp",
	"timestamp with time zone":    "timestamptz",
	// time aliases
	"time without time zone": "time",
	"time with time zone":    "timetz",
	// decimal alias
	"decimal": "numeric",
}

// knownPGBaseTypes is the set of canonical PostgreSQL base types (no aliases).
var knownPGBaseTypes = map[string]struct{}{
	"integer": {}, "bigint": {}, "smallint": {},
	"serial": {}, "bigserial": {}, "smallserial": {},
	"numeric": {}, "real": {}, "double precision": {}, "money": {},
	"varchar": {}, "char": {}, "text": {}, "citext": {},
	"boolean": {},
	"date":    {}, "time": {}, "timetz": {}, "timestamp": {}, "timestamptz": {}, "interval": {},
	"uuid": {}, "json": {}, "jsonb": {}, "bytea": {},
	"inet": {}, "cidr": {}, "macaddr": {}, "xml": {},
}

// NormalizePGType maps a PostgreSQL base type (no dimension parameters) to its
// canonical form. Unknown types are returned as-is (lowercased).
func NormalizePGType(baseType string) string {
	lower := strings.ToLower(strings.TrimSpace(baseType))
	if canonical, ok := PGTypeCanonical[lower]; ok {
		return canonical
	}
	return lower
}

// IsKnownPGBaseType reports whether the given name (after NormalizePGType) is a
// recognized built-in PostgreSQL type. Custom types (e.g. vector, postgis) return false.
func IsKnownPGBaseType(baseType string) bool {
	_, ok := knownPGBaseTypes[strings.ToLower(strings.TrimSpace(baseType))]
	return ok
}

// serialUnderlyingType maps each serial pseudo-type to the integer type
// PostgreSQL actually stores the column as. serial/bigserial/smallserial are
// not real types: they are sugar for an integer column plus a sequence
// default, and pg_catalog (and information_schema) always reports the
// underlying integer type back for such columns.
var serialUnderlyingType = map[string]string{
	"serial":      "integer",
	"bigserial":   "bigint",
	"smallserial": "smallint",
}

// SerialUnderlyingType returns the underlying integer type for a serial
// pseudo-type (e.g. "bigserial" -> "bigint"). If baseType (after
// NormalizePGType) is not a serial type, it is returned unchanged.
func SerialUnderlyingType(baseType string) string {
	normalized := NormalizePGType(baseType)
	if underlying, ok := serialUnderlyingType[normalized]; ok {
		return underlying
	}
	return normalized
}

func IsGoType(pTypeName string) bool {
	for k := range GoToStdTypes {
		if strings.EqualFold(pTypeName, k) {
			return true
		}
	}
	return false
}

func GetStdTypeFromGo(pTypeName string) string {
	for k, s := range GoToStdTypes {
		if strings.EqualFold(pTypeName, k) {
			return s
		}
	}
	return pTypeName
}
