package pgsql

import (
	"sort"
	"strconv"
	"strings"
)

// TypeSpec describes PostgreSQL type capabilities used by parsers/writers.
type TypeSpec struct {
	SupportsLength    bool
	SupportsPrecision bool

	// SupportsTypeModifier marks types whose "(...)" modifier is opaque and must be
	// preserved verbatim (e.g. vector(1536), geometry(Point,4326)) instead of being
	// decomposed into Length/Precision/Scale.
	SupportsTypeModifier bool

	// Extension is the PostgreSQL extension providing the type; empty for built-ins.
	Extension string
}

var postgresBaseTypes = map[string]TypeSpec{
	// Numeric types
	"smallint":         {},
	"integer":          {},
	"bigint":           {},
	"decimal":          {SupportsPrecision: true},
	"numeric":          {SupportsPrecision: true},
	"real":             {},
	"double precision": {},
	"smallserial":      {},
	"serial":           {},
	"bigserial":        {},
	"money":            {},

	// Character types
	"char":              {SupportsLength: true},
	"character":         {SupportsLength: true},
	"varchar":           {SupportsLength: true},
	"character varying": {SupportsLength: true},
	"text":              {},
	"name":              {},

	// Binary
	"bytea": {},

	// Date/time
	"timestamp":                   {SupportsPrecision: true},
	"timestamp without time zone": {SupportsPrecision: true},
	"timestamp with time zone":    {SupportsPrecision: true},
	"time":                        {SupportsPrecision: true},
	"time without time zone":      {SupportsPrecision: true},
	"time with time zone":         {SupportsPrecision: true},
	"date":                        {},
	"interval":                    {SupportsPrecision: true},

	// Boolean
	"boolean": {},

	// Geometric
	"point":   {},
	"line":    {},
	"lseg":    {},
	"box":     {},
	"path":    {},
	"polygon": {},
	"circle":  {},

	// Network
	"cidr":     {},
	"inet":     {},
	"macaddr":  {},
	"macaddr8": {},

	// Bit string
	"bit":         {SupportsLength: true},
	"bit varying": {SupportsLength: true},
	"varbit":      {SupportsLength: true},

	// Text search
	"tsvector": {},
	"tsquery":  {},

	// UUID/XML/JSON
	"uuid":  {},
	"xml":   {},
	"json":  {},
	"jsonb": {},

	// Range
	"int4range":      {},
	"int8range":      {},
	"numrange":       {},
	"tsrange":        {},
	"tstzrange":      {},
	"daterange":      {},
	"int4multirange": {},
	"int8multirange": {},
	"nummultirange":  {},
	"tsmultirange":   {},
	"tstzmultirange": {},
	"datemultirange": {},

	// Object identifier
	"oid":      {},
	"regclass": {},
	"regproc":  {},
	"regtype":  {},

	// Pseudo-ish/common built-ins seen in schemas
	"record": {},
	"void":   {},

	// Common extensions
	"citext":    {Extension: "citext"},
	"hstore":    {Extension: "hstore"},
	"ltree":     {Extension: "ltree"},
	"lquery":    {Extension: "ltree"},
	"ltxtquery": {Extension: "ltree"},

	// pgvector: modifier form is opaque (vector(dim), sparsevec(dim))
	"vector":    {SupportsTypeModifier: true, Extension: "vector"},
	"halfvec":   {SupportsTypeModifier: true, Extension: "vector"},
	"sparsevec": {SupportsTypeModifier: true, Extension: "vector"},

	// PostGIS: geometry/geography carry an opaque modifier (geometry(PointZ,4326))
	"geometry":      {SupportsTypeModifier: true, Extension: "postgis"},
	"geography":     {SupportsTypeModifier: true, Extension: "postgis"},
	"box2d":         {Extension: "postgis"},
	"box3d":         {Extension: "postgis"},
	"geometry_dump": {Extension: "postgis"},
	"geomval":       {Extension: "postgis"},
	"spheroid":      {Extension: "postgis"},
	"valid_detail":  {Extension: "postgis"},
	"raster":        {SupportsTypeModifier: true, Extension: "postgis_raster"},
	"topogeometry":  {Extension: "postgis_topology"},
}

var postgresTypeAliases = map[string]string{
	// Integer aliases
	"int2": "smallint",
	"int4": "integer",
	"int8": "bigint",
	"int":  "integer",

	// Serial aliases
	"serial2": "smallserial",
	"serial4": "serial",
	"serial8": "bigserial",

	// Character aliases
	"bpchar": "char",

	// Float aliases
	"float4": "real",
	"float8": "double precision",
	"float":  "double precision",

	// Time aliases
	"timestamptz": "timestamp with time zone",
	"timetz":      "time with time zone",

	// Bit alias
	"varbit": "bit varying",

	// Boolean alias
	"bool": "boolean",
}

var postgresEquivalentBaseTypes = map[string]string{
	"character varying":           "varchar",
	"character":                   "char",
	"timestamp without time zone": "timestamp",
	"timestamp with time zone":    "timestamptz",
	"time without time zone":      "time",
	"time with time zone":         "timetz",
}

var postgresEquivalentBaseTypeVariants = map[string][]string{
	"varchar":     {"varchar", "character varying"},
	"char":        {"char", "character"},
	"timestamp":   {"timestamp", "timestamp without time zone"},
	"timestamptz": {"timestamptz", "timestamp with time zone"},
	"time":        {"time", "time without time zone"},
	"timetz":      {"timetz", "time with time zone"},
}

// GetPostgresBaseTypes returns a sorted-ish stable list of registered base type names.
func GetPostgresBaseTypes() []string {
	result := make([]string, 0, len(postgresBaseTypes))
	for t := range postgresBaseTypes {
		result = append(result, t)
	}
	sort.Strings(result)
	return result
}

// GetPostgresTypes returns the registered PostgreSQL types.
// When includeArrays is true, each base type also includes an array variant ("type[]").
func GetPostgresTypes(includeArrays bool) []string {
	base := GetPostgresBaseTypes()
	if !includeArrays {
		return base
	}

	result := make([]string, 0, len(base)*2)
	result = append(result, base...)
	for _, t := range base {
		result = append(result, t+"[]")
	}
	return result
}

// ExtractBaseType returns the type without outer array suffixes and modifiers.
// Examples:
// - varchar(255) -> varchar
// - text[] -> text
// - numeric(10,2)[] -> numeric
func ExtractBaseType(sqlType string) string {
	t := normalizeTypeToken(sqlType)
	t = strings.TrimSpace(stripArraySuffixes(t))
	if idx := strings.Index(t, "("); idx > 0 {
		t = strings.TrimSpace(t[:idx])
	}
	return t
}

// ExtractBaseTypeLower is ExtractBaseType with lowercase normalization.
func ExtractBaseTypeLower(sqlType string) string {
	return strings.ToLower(ExtractBaseType(sqlType))
}

// IsArrayType reports whether the SQL type has one or more [] suffixes.
func IsArrayType(sqlType string) bool {
	t := normalizeTypeToken(sqlType)
	return strings.HasSuffix(t, "[]")
}

// ElementType returns the underlying element type for array types.
// For non-array types, it returns the input unchanged.
func ElementType(sqlType string) string {
	t := normalizeTypeToken(sqlType)
	return stripArraySuffixes(t)
}

// CanonicalizeBaseType resolves aliases to canonical PostgreSQL type names.
func CanonicalizeBaseType(baseType string) string {
	base := strings.ToLower(normalizeTypeToken(baseType))
	if canonical, ok := postgresTypeAliases[base]; ok {
		return canonical
	}
	return base
}

// EquivalentBaseType resolves broader SQL-equivalent spellings to a common comparable form.
func EquivalentBaseType(baseType string) string {
	base := CanonicalizeBaseType(baseType)
	if equivalent, ok := postgresEquivalentBaseTypes[base]; ok {
		return equivalent
	}
	return base
}

// NormalizeEquivalentSQLType returns a normalized SQL type string suitable for equality checks.
// Equivalent spellings such as "character varying(255)" and "varchar(255)" normalize identically.
func NormalizeEquivalentSQLType(sqlType string) string {
	t := normalizeTypeToken(sqlType)
	if t == "" {
		return ""
	}

	arrayDepth := 0
	for strings.HasSuffix(t, "[]") {
		arrayDepth++
		t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
	}

	modifier := ""
	if idx := strings.Index(t, "("); idx >= 0 {
		modifier = strings.TrimSpace(t[idx:])
		t = strings.TrimSpace(t[:idx])
	}

	base := EquivalentBaseType(t)
	normalized := base + modifier
	for i := 0; i < arrayDepth; i++ {
		normalized += "[]"
	}
	return normalized
}

// EquivalentSQLTypeVariants returns equivalent PostgreSQL spellings for a SQL type.
// Examples:
// - varchar(255) -> ["varchar(255)", "character varying(255)"]
// - timestamptz -> ["timestamptz", "timestamp with time zone"]
func EquivalentSQLTypeVariants(sqlType string) []string {
	t := normalizeTypeToken(sqlType)
	if t == "" {
		return nil
	}

	arrayDepth := 0
	for strings.HasSuffix(t, "[]") {
		arrayDepth++
		t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
	}

	modifier := ""
	if idx := strings.Index(t, "("); idx >= 0 {
		modifier = strings.TrimSpace(t[idx:])
		t = strings.TrimSpace(t[:idx])
	}

	base := EquivalentBaseType(t)
	bases := postgresEquivalentBaseTypeVariants[base]
	if len(bases) == 0 {
		bases = []string{base}
	}

	seen := make(map[string]bool, len(bases))
	result := make([]string, 0, len(bases))
	for _, variantBase := range bases {
		variant := variantBase + modifier
		for i := 0; i < arrayDepth; i++ {
			variant += "[]"
		}
		if !seen[variant] {
			seen[variant] = true
			result = append(result, variant)
		}
	}
	return result
}

// IsKnownPostgresType reports whether a type (including array forms) exists in the registry.
func IsKnownPostgresType(sqlType string) bool {
	base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
	_, ok := postgresBaseTypes[base]
	return ok
}

// SupportsLength reports if this SQL type accepts a single length/dimension modifier.
func SupportsLength(sqlType string) bool {
	base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
	spec, ok := postgresBaseTypes[base]
	return ok && spec.SupportsLength
}

// SupportsPrecision reports if this SQL type accepts precision (and possibly scale).
func SupportsPrecision(sqlType string) bool {
	base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
	spec, ok := postgresBaseTypes[base]
	return ok && spec.SupportsPrecision
}

// HasExplicitTypeModifier reports if the type already includes "(...)".
func HasExplicitTypeModifier(sqlType string) bool {
	return strings.Contains(sqlType, "(")
}

func stripArraySuffixes(t string) string {
	for strings.HasSuffix(t, "[]") {
		t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
	}
	return t
}

func normalizeTypeToken(t string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(t)), " ")
}

// SupportsTypeModifier reports if this SQL type carries an opaque "(...)" modifier
// that must be preserved verbatim (e.g. vector(1536), geometry(Point,4326)).
func SupportsTypeModifier(sqlType string) bool {
	base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
	spec, ok := postgresBaseTypes[base]
	return ok && spec.SupportsTypeModifier
}

// TypeExtension returns the PostgreSQL extension providing the given type
// ("postgis", "vector", "citext", …). Built-in types return "".
func TypeExtension(sqlType string) string {
	base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
	return postgresBaseTypes[base].Extension
}

// IsSpatialType reports whether the type comes from PostGIS (geometry, geography,
// raster, topogeometry, …).
func IsSpatialType(sqlType string) bool {
	return strings.HasPrefix(TypeExtension(sqlType), "postgis")
}

// IsVectorType reports whether the type comes from pgvector (vector, halfvec, sparsevec).
func IsVectorType(sqlType string) bool {
	return TypeExtension(sqlType) == "vector"
}

// TypeModifier returns the raw "(...)" modifier of a SQL type without the parentheses,
// or "" when the type has none. Array suffixes are ignored.
// Example: geometry(PointZ,4326)[] -> "PointZ,4326".
func TypeModifier(sqlType string) string {
	t := stripArraySuffixes(normalizeTypeToken(sqlType))
	start := strings.Index(t, "(")
	end := strings.LastIndex(t, ")")
	if start < 0 || end < start {
		return ""
	}
	return strings.TrimSpace(t[start+1 : end])
}

// SpatialSRID returns the SRID declared in a PostGIS type modifier, or 0 when absent.
// Example: geometry(Point,4326) -> 4326.
func SpatialSRID(sqlType string) int {
	if !IsSpatialType(sqlType) {
		return 0
	}
	parts := strings.Split(TypeModifier(sqlType), ",")
	if len(parts) < 2 {
		return 0
	}
	srid, err := strconv.Atoi(strings.TrimSpace(parts[len(parts)-1]))
	if err != nil {
		return 0
	}
	return srid
}

// SpatialGeometryType returns the geometry subtype declared in a PostGIS type modifier
// ("Point", "MultiPolygonZ", …), or "" when absent.
func SpatialGeometryType(sqlType string) string {
	if !IsSpatialType(sqlType) {
		return ""
	}
	modifier := TypeModifier(sqlType)
	if modifier == "" {
		return ""
	}
	return strings.TrimSpace(strings.Split(modifier, ",")[0])
}
