package gorm

import (
	"fmt"
	"strings"

	"git.warky.dev/wdevs/relspecgo/pkg/models"
	"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
	"git.warky.dev/wdevs/relspecgo/pkg/writers"
)

// TypeMapper handles type conversions between SQL and Go types
type TypeMapper struct {
	sqlTypesAlias string
	typeStyle     string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
}

// NewTypeMapper creates a new TypeMapper.
// typeStyle should be writers.NullableTypeSqlTypes, writers.NullableTypeStdlib, or
// writers.NullableTypeBaselib; an empty string defaults to baselib.
func NewTypeMapper(typeStyle string) *TypeMapper {
	if typeStyle == "" {
		typeStyle = writers.NullableTypeBaselib
	}
	return &TypeMapper{
		sqlTypesAlias: "sql_types",
		typeStyle:     typeStyle,
	}
}

// SQLTypeToGoType converts a SQL type to its Go equivalent.
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
	// Array types are handled separately for both styles.
	if pgsql.IsArrayType(sqlType) {
		return tm.arrayGoType(tm.extractBaseType(sqlType))
	}

	baseType := tm.extractBaseType(sqlType)

	switch tm.typeStyle {
	case writers.NullableTypeStdlib:
		if notNull {
			return tm.rawGoType(baseType)
		}
		return tm.stdlibNullableGoType(baseType)
	case writers.NullableTypeBaselib:
		if notNull {
			return tm.rawGoType(baseType)
		}
		return tm.baselibNullableGoType(baseType)
	}

	// sqltypes
	if notNull {
		return tm.baseGoType(baseType)
	}
	return tm.nullableGoType(baseType)
}

// extractBaseType extracts the base type from a SQL type string
// Examples: varchar(100) → varchar, numeric(10,2) → numeric
func (tm *TypeMapper) extractBaseType(sqlType string) string {
	return pgsql.CanonicalizeBaseType(pgsql.ExtractBaseTypeLower(sqlType))
}

// baseGoType returns the base Go type for a SQL type (not null)
func (tm *TypeMapper) baseGoType(sqlType string) string {
	typeMap := map[string]string{
		// Integer types
		"integer":     "int32",
		"int":         "int32",
		"int4":        "int32",
		"smallint":    "int16",
		"int2":        "int16",
		"bigint":      "int64",
		"int8":        "int64",
		"serial":      "int32",
		"bigserial":   "int64",
		"smallserial": "int16",

		// String types
		"text":      "string",
		"varchar":   "string",
		"char":      "string",
		"character": "string",
		"citext":    "string",
		"bpchar":    "string",

		// Boolean
		"boolean": "bool",
		"bool":    "bool",

		// Float types
		"real":             "float32",
		"float4":           "float32",
		"double precision": "float64",
		"float8":           "float64",
		"numeric":          "float64",
		"decimal":          "float64",

		// Date/Time types
		"timestamp":                   "time.Time",
		"timestamp without time zone": "time.Time",
		"timestamp with time zone":    "time.Time",
		"timestamptz":                 "time.Time",
		"date":                        "time.Time",
		"time":                        "time.Time",
		"time without time zone":      "time.Time",
		"time with time zone":         "time.Time",
		"timetz":                      "time.Time",

		// Binary
		"bytea": "[]byte",

		// UUID
		"uuid": "string",

		// JSON
		"json":  "string",
		"jsonb": "string",

		// Network
		"inet":    "string",
		"cidr":    "string",
		"macaddr": "string",

		// Other
		"money": "float64",

		// pgvector — always uses SqlVector even when NOT NULL
		"vector": tm.sqlTypesAlias + ".SqlVector",
	}

	if goType, ok := typeMap[sqlType]; ok {
		return goType
	}

	// Default to string for unknown types
	return "string"
}

// nullableGoType returns the nullable Go type using sql_types package
func (tm *TypeMapper) nullableGoType(sqlType string) string {
	typeMap := map[string]string{
		// Integer types
		"integer":     tm.sqlTypesAlias + ".SqlInt32",
		"int":         tm.sqlTypesAlias + ".SqlInt32",
		"int4":        tm.sqlTypesAlias + ".SqlInt32",
		"smallint":    tm.sqlTypesAlias + ".SqlInt16",
		"int2":        tm.sqlTypesAlias + ".SqlInt16",
		"bigint":      tm.sqlTypesAlias + ".SqlInt64",
		"int8":        tm.sqlTypesAlias + ".SqlInt64",
		"serial":      tm.sqlTypesAlias + ".SqlInt32",
		"bigserial":   tm.sqlTypesAlias + ".SqlInt64",
		"smallserial": tm.sqlTypesAlias + ".SqlInt16",

		// String types
		"text":      tm.sqlTypesAlias + ".SqlString",
		"varchar":   tm.sqlTypesAlias + ".SqlString",
		"char":      tm.sqlTypesAlias + ".SqlString",
		"character": tm.sqlTypesAlias + ".SqlString",
		"citext":    tm.sqlTypesAlias + ".SqlString",
		"bpchar":    tm.sqlTypesAlias + ".SqlString",

		// Boolean
		"boolean": tm.sqlTypesAlias + ".SqlBool",
		"bool":    tm.sqlTypesAlias + ".SqlBool",

		// Float types
		"real":             tm.sqlTypesAlias + ".SqlFloat32",
		"float4":           tm.sqlTypesAlias + ".SqlFloat32",
		"double precision": tm.sqlTypesAlias + ".SqlFloat64",
		"float8":           tm.sqlTypesAlias + ".SqlFloat64",
		"numeric":          tm.sqlTypesAlias + ".SqlFloat64",
		"decimal":          tm.sqlTypesAlias + ".SqlFloat64",

		// Date/Time types
		"timestamp":                   tm.sqlTypesAlias + ".SqlTimeStamp",
		"timestamp without time zone": tm.sqlTypesAlias + ".SqlTimeStamp",
		"timestamp with time zone":    tm.sqlTypesAlias + ".SqlTimeStamp",
		"timestamptz":                 tm.sqlTypesAlias + ".SqlTimeStamp",
		"date":                        tm.sqlTypesAlias + ".SqlDate",
		"time":                        tm.sqlTypesAlias + ".SqlTime",
		"time without time zone":      tm.sqlTypesAlias + ".SqlTime",
		"time with time zone":         tm.sqlTypesAlias + ".SqlTime",
		"timetz":                      tm.sqlTypesAlias + ".SqlTime",

		// Binary
		"bytea": "[]byte", // No nullable version needed

		// UUID
		"uuid": tm.sqlTypesAlias + ".SqlUUID",

		// JSON
		"json":  tm.sqlTypesAlias + ".SqlString",
		"jsonb": tm.sqlTypesAlias + ".SqlString",

		// Network
		"inet":    tm.sqlTypesAlias + ".SqlString",
		"cidr":    tm.sqlTypesAlias + ".SqlString",
		"macaddr": tm.sqlTypesAlias + ".SqlString",

		// Other
		"money": tm.sqlTypesAlias + ".SqlFloat64",

		// pgvector
		"vector": tm.sqlTypesAlias + ".SqlVector",
	}

	if goType, ok := typeMap[sqlType]; ok {
		return goType
	}

	// Default to SqlString for unknown types
	return tm.sqlTypesAlias + ".SqlString"
}

// arrayGoType returns the Go type for a PostgreSQL array column.
// The baseElemType is the canonical base type (e.g. "text", "integer").
func (tm *TypeMapper) arrayGoType(baseElemType string) string {
	if tm.typeStyle == writers.NullableTypeStdlib || tm.typeStyle == writers.NullableTypeBaselib {
		return tm.stdlibArrayGoType(baseElemType)
	}
	typeMap := map[string]string{
		"text": tm.sqlTypesAlias + ".SqlStringArray", "varchar": tm.sqlTypesAlias + ".SqlStringArray",
		"char": tm.sqlTypesAlias + ".SqlStringArray", "character": tm.sqlTypesAlias + ".SqlStringArray",
		"citext": tm.sqlTypesAlias + ".SqlStringArray", "bpchar": tm.sqlTypesAlias + ".SqlStringArray",
		"inet": tm.sqlTypesAlias + ".SqlStringArray", "cidr": tm.sqlTypesAlias + ".SqlStringArray",
		"macaddr": tm.sqlTypesAlias + ".SqlStringArray",
		"json":    tm.sqlTypesAlias + ".SqlStringArray", "jsonb": tm.sqlTypesAlias + ".SqlStringArray",
		"integer": tm.sqlTypesAlias + ".SqlInt32Array", "int": tm.sqlTypesAlias + ".SqlInt32Array",
		"int4": tm.sqlTypesAlias + ".SqlInt32Array", "serial": tm.sqlTypesAlias + ".SqlInt32Array",
		"smallint": tm.sqlTypesAlias + ".SqlInt16Array", "int2": tm.sqlTypesAlias + ".SqlInt16Array",
		"smallserial": tm.sqlTypesAlias + ".SqlInt16Array",
		"bigint":      tm.sqlTypesAlias + ".SqlInt64Array", "int8": tm.sqlTypesAlias + ".SqlInt64Array",
		"bigserial": tm.sqlTypesAlias + ".SqlInt64Array",
		"real":      tm.sqlTypesAlias + ".SqlFloat32Array", "float4": tm.sqlTypesAlias + ".SqlFloat32Array",
		"double precision": tm.sqlTypesAlias + ".SqlFloat64Array", "float8": tm.sqlTypesAlias + ".SqlFloat64Array",
		"numeric": tm.sqlTypesAlias + ".SqlFloat64Array", "decimal": tm.sqlTypesAlias + ".SqlFloat64Array",
		"money":   tm.sqlTypesAlias + ".SqlFloat64Array",
		"boolean": tm.sqlTypesAlias + ".SqlBoolArray", "bool": tm.sqlTypesAlias + ".SqlBoolArray",
		"uuid": tm.sqlTypesAlias + ".SqlUUIDArray",
	}
	if goType, ok := typeMap[baseElemType]; ok {
		return goType
	}
	return tm.sqlTypesAlias + ".SqlStringArray"
}

// rawGoType returns the plain Go type for a NOT NULL column in stdlib mode.
func (tm *TypeMapper) rawGoType(sqlType string) string {
	typeMap := map[string]string{
		"integer": "int32", "int": "int32", "int4": "int32", "serial": "int32",
		"smallint": "int16", "int2": "int16", "smallserial": "int16",
		"bigint": "int64", "int8": "int64", "bigserial": "int64",
		"boolean": "bool", "bool": "bool",
		"real": "float32", "float4": "float32",
		"double precision": "float64", "float8": "float64",
		"numeric": "float64", "decimal": "float64", "money": "float64",
		"text": "string", "varchar": "string", "char": "string",
		"character": "string", "citext": "string", "bpchar": "string",
		"inet": "string", "cidr": "string", "macaddr": "string",
		"uuid": "string", "json": "string", "jsonb": "string",
		"timestamp":                   "time.Time",
		"timestamp without time zone": "time.Time",
		"timestamp with time zone":    "time.Time",
		"timestamptz":                 "time.Time",
		"date":                        "time.Time",
		"time":                        "time.Time",
		"time without time zone":      "time.Time",
		"time with time zone":         "time.Time",
		"timetz":                      "time.Time",
		"bytea":                       "[]byte",
		"vector":                      "[]float32",
	}
	if goType, ok := typeMap[sqlType]; ok {
		return goType
	}
	return "string"
}

// stdlibNullableGoType returns the database/sql nullable type for a column.
func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string {
	typeMap := map[string]string{
		"integer": "sql.NullInt32", "int": "sql.NullInt32", "int4": "sql.NullInt32", "serial": "sql.NullInt32",
		"smallint": "sql.NullInt16", "int2": "sql.NullInt16", "smallserial": "sql.NullInt16",
		"bigint": "sql.NullInt64", "int8": "sql.NullInt64", "bigserial": "sql.NullInt64",
		"boolean": "sql.NullBool", "bool": "sql.NullBool",
		"real": "sql.NullFloat64", "float4": "sql.NullFloat64",
		"double precision": "sql.NullFloat64", "float8": "sql.NullFloat64",
		"numeric": "sql.NullFloat64", "decimal": "sql.NullFloat64", "money": "sql.NullFloat64",
		"text": "sql.NullString", "varchar": "sql.NullString", "char": "sql.NullString",
		"character": "sql.NullString", "citext": "sql.NullString", "bpchar": "sql.NullString",
		"inet": "sql.NullString", "cidr": "sql.NullString", "macaddr": "sql.NullString",
		"uuid": "sql.NullString", "json": "sql.NullString", "jsonb": "sql.NullString",
		"timestamp":                   "sql.NullTime",
		"timestamp without time zone": "sql.NullTime",
		"timestamp with time zone":    "sql.NullTime",
		"timestamptz":                 "sql.NullTime",
		"date":                        "sql.NullTime",
		"time":                        "sql.NullTime",
		"time without time zone":      "sql.NullTime",
		"time with time zone":         "sql.NullTime",
		"timetz":                      "sql.NullTime",
		"bytea":                       "[]byte",
		"vector":                      "[]float32",
	}
	if goType, ok := typeMap[sqlType]; ok {
		return goType
	}
	return "sql.NullString"
}

// baselibNullableGoType returns plain Go pointer types for nullable columns.
func (tm *TypeMapper) baselibNullableGoType(sqlType string) string {
	typeMap := map[string]string{
		"integer": "*int32", "int": "*int32", "int4": "*int32", "serial": "*int32",
		"smallint": "*int16", "int2": "*int16", "smallserial": "*int16",
		"bigint": "*int64", "int8": "*int64", "bigserial": "*int64",
		"boolean": "*bool", "bool": "*bool",
		"real": "*float32", "float4": "*float32",
		"double precision": "*float64", "float8": "*float64",
		"numeric": "*float64", "decimal": "*float64", "money": "*float64",
		"text": "*string", "varchar": "*string", "char": "*string",
		"character": "*string", "citext": "*string", "bpchar": "*string",
		"inet": "*string", "cidr": "*string", "macaddr": "*string",
		"uuid": "*string", "json": "*string", "jsonb": "*string",
		"timestamp":                   "*time.Time",
		"timestamp without time zone": "*time.Time",
		"timestamp with time zone":    "*time.Time",
		"timestamptz":                 "*time.Time",
		"date":                        "*time.Time",
		"time":                        "*time.Time",
		"time without time zone":      "*time.Time",
		"time with time zone":         "*time.Time",
		"timetz":                      "*time.Time",
		"bytea":                       "[]byte",
		"vector":                      "[]float32",
	}
	if goType, ok := typeMap[sqlType]; ok {
		return goType
	}
	return "*string"
}

// stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode.
func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string {
	typeMap := map[string]string{
		"text": "[]string", "varchar": "[]string", "char": "[]string",
		"character": "[]string", "citext": "[]string", "bpchar": "[]string",
		"inet": "[]string", "cidr": "[]string", "macaddr": "[]string",
		"uuid": "[]string", "json": "[]string", "jsonb": "[]string",
		"integer": "[]int32", "int": "[]int32", "int4": "[]int32", "serial": "[]int32",
		"smallint": "[]int16", "int2": "[]int16", "smallserial": "[]int16",
		"bigint": "[]int64", "int8": "[]int64", "bigserial": "[]int64",
		"real": "[]float32", "float4": "[]float32",
		"double precision": "[]float64", "float8": "[]float64",
		"numeric": "[]float64", "decimal": "[]float64", "money": "[]float64",
		"boolean": "[]bool", "bool": "[]bool",
	}
	if goType, ok := typeMap[baseElemType]; ok {
		return goType
	}
	return "[]string"
}

// BuildGormTag generates a complete GORM tag string for a column
func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) string {
	var parts []string

	// Always include column name (lowercase as per user requirement)
	// Sanitize to remove backticks which would break struct tag syntax
	safeName := writers.SanitizeStructTagValue(column.Name)
	parts = append(parts, fmt.Sprintf("column:%s", safeName))

	// Add type if specified
	if column.Type != "" {
		// Include length, precision, scale if present
		// Sanitize type to remove backticks
		typeStr := writers.SanitizeStructTagValue(column.Type)
		hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(typeStr)
		if !hasExplicitTypeModifier && column.Length > 0 {
			typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
		} else if !hasExplicitTypeModifier && column.Precision > 0 {
			if column.Scale > 0 {
				typeStr = fmt.Sprintf("%s(%d,%d)", typeStr, column.Precision, column.Scale)
			} else {
				typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Precision)
			}
		}
		parts = append(parts, fmt.Sprintf("type:%s", typeStr))
	}

	// Primary key
	if column.IsPrimaryKey {
		parts = append(parts, "primaryKey")
	}

	// Auto increment
	if column.AutoIncrement {
		parts = append(parts, "autoIncrement")
	}

	// Not null (skip if primary key, as it's implied)
	if column.NotNull && !column.IsPrimaryKey {
		parts = append(parts, "not null")
	}

	// Default value
	if column.Default != nil {
		// Sanitize default value to remove backticks, then quote based on column type
		safeDefault := writers.QuoteDefaultValue(writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)), column.Type)
		parts = append(parts, fmt.Sprintf("default:%s", safeDefault))
	}

	// Check for unique constraint
	if table != nil {
		for _, constraint := range sortConstraints(table.Constraints) {
			if constraint.Type == models.UniqueConstraint {
				for _, col := range constraint.Columns {
					if col == column.Name {
						if constraint.Name != "" {
							parts = append(parts, fmt.Sprintf("uniqueIndex:%s", constraint.Name))
						} else {
							parts = append(parts, "unique")
						}
						break
					}
				}
			}
		}

		// Check for index
		for _, index := range sortIndexes(table.Indexes) {
			for _, col := range index.Columns {
				if col == column.Name {
					if index.Unique {
						if index.Name != "" {
							parts = append(parts, fmt.Sprintf("uniqueIndex:%s", index.Name))
						} else {
							parts = append(parts, "unique")
						}
					} else {
						if index.Name != "" {
							parts = append(parts, fmt.Sprintf("index:%s", index.Name))
						} else {
							parts = append(parts, "index")
						}
					}
					break
				}
			}
		}
	}

	return strings.Join(parts, ";")
}

// BuildRelationshipTag generates GORM tag for relationship fields
func (tm *TypeMapper) BuildRelationshipTag(constraint *models.Constraint, isParent bool) string {
	var parts []string

	if !isParent {
		// Child side (has foreign key)
		if len(constraint.Columns) > 0 && len(constraint.ReferencedColumns) > 0 {
			// foreignKey points to the field name in this struct
			fkFieldName := SnakeCaseToPascalCase(constraint.Columns[0])
			parts = append(parts, fmt.Sprintf("foreignKey:%s", fkFieldName))

			// references points to the field name in the other struct
			refFieldName := SnakeCaseToPascalCase(constraint.ReferencedColumns[0])
			parts = append(parts, fmt.Sprintf("references:%s", refFieldName))
		}
	} else {
		// Parent side (being referenced)
		if len(constraint.Columns) > 0 {
			fkFieldName := SnakeCaseToPascalCase(constraint.Columns[0])
			parts = append(parts, fmt.Sprintf("foreignKey:%s", fkFieldName))
		}
	}

	// Add constraint actions
	if constraint.OnDelete != "" {
		parts = append(parts, fmt.Sprintf("constraint:OnDelete:%s", strings.ToUpper(constraint.OnDelete)))
	}
	if constraint.OnUpdate != "" {
		if len(parts) > 0 && strings.Contains(parts[len(parts)-1], "constraint:") {
			// Append to existing constraint
			parts[len(parts)-1] += fmt.Sprintf(",OnUpdate:%s", strings.ToUpper(constraint.OnUpdate))
		} else {
			parts = append(parts, fmt.Sprintf("constraint:OnUpdate:%s", strings.ToUpper(constraint.OnUpdate)))
		}
	}

	return strings.Join(parts, ";")
}

// NeedsTimeImport checks if the Go type requires time package import
func (tm *TypeMapper) NeedsTimeImport(goType string) bool {
	return strings.Contains(goType, "time.Time")
}

// NeedsFmtImport checks if we need fmt import (for GetIDStr method)
func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool {
	return generateGetIDStr
}

// GetSQLTypesImport returns the import path for the sqltypes package.
func (tm *TypeMapper) GetSQLTypesImport() string {
	return "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
}

// GetNullableTypeImportLine returns the full Go import line for the nullable type
// package (ready to pass to AddImport). Returns empty string when no import is needed.
func (tm *TypeMapper) GetNullableTypeImportLine() string {
	switch tm.typeStyle {
	case writers.NullableTypeStdlib:
		return "\"database/sql\""
	case writers.NullableTypeBaselib:
		return ""
	}
	return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport())
}
