package bun

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 for Bun
type TypeMapper struct {
	sqlTypesAlias string
	typeStyle     string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
	arrayNullable string // writers.NullableArraysSlice | writers.NullableArraysPointerSlice
}

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

// SQLTypeToGoType converts a SQL type to its Go equivalent.
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
	// Array columns always use a native Go slice, regardless of typeStyle.
	if pgsql.IsArrayType(sqlType) {
		goType := tm.arrayGoType(tm.extractBaseType(sqlType))
		if !notNull && tm.arrayNullable == writers.NullableArraysPointerSlice {
			goType = "*" + goType
		}
		return goType
	}

	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: use base Go types only for simple NOT NULL fields.
	if notNull && tm.isSimpleType(baseType) {
		return tm.baseGoType(baseType)
	}
	return tm.bunGoType(baseType)
}

// extractBaseType extracts the base type from a SQL type string
func (tm *TypeMapper) extractBaseType(sqlType string) string {
	return pgsql.CanonicalizeBaseType(pgsql.ExtractBaseTypeLower(sqlType))
}

// isSimpleType checks if a type should use base Go type when NOT NULL
func (tm *TypeMapper) isSimpleType(sqlType string) bool {
	simpleTypes := map[string]bool{
		"bigint":  true,
		"integer": true,
		"int8":    true,
		"int4":    true,
		"boolean": true,
		"bool":    true,
	}
	return simpleTypes[sqlType]
}

// isSerialType checks if a SQL type is a serial type (auto-incrementing)
func (tm *TypeMapper) isSerialType(sqlType string) bool {
	baseType := tm.extractBaseType(sqlType)
	serialTypes := map[string]bool{
		"serial":      true,
		"bigserial":   true,
		"smallserial": true,
	}
	return serialTypes[baseType]
}

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

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

	// Default to sqltypes type
	return tm.bunGoType(sqlType)
}

// bunGoType returns the Bun/sqltypes common type
func (tm *TypeMapper) bunGoType(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",

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

		// JSON
		"json":  tm.sqlTypesAlias + ".SqlJSON",
		"jsonb": tm.sqlTypesAlias + ".SqlJSONB",

		// 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").
//
// Array columns always use a plain native Go slice, even in sqltypes mode:
// bun's pgdialect scans native slices directly, and the SqlXxxArray wrapper
// types are not usable as array columns (their Scan/Append are bypassed
// whenever the "array" bun tag is set, which is required for arrays).
func (tm *TypeMapper) arrayGoType(baseElemType string) string {
	return tm.stdlibArrayGoType(baseElemType)
}

// 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"
}

// BuildBunTag generates a complete Bun tag string for a column
// Bun format: bun:"column_name,type:type_name,pk,default:value"
func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) string {
	var parts []string

	// Column name comes first (no prefix)
	// Sanitize to remove backticks which would break struct tag syntax
	safeName := writers.SanitizeStructTagValue(column.Name)
	parts = append(parts, safeName)

	// Add type if specified
	if column.Type != "" {
		// Sanitize type to remove backticks
		typeStr := writers.SanitizeStructTagValue(column.Type)
		isArray := pgsql.IsArrayType(typeStr)
		hasExplicitTypeModifier := pgsql.HasExplicitTypeModifier(typeStr)
		if !hasExplicitTypeModifier && !isArray && column.Length > 0 {
			typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
		} else if !hasExplicitTypeModifier && !isArray && 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))
		if isArray {
			parts = append(parts, "array")
		}
	}

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

	// Auto increment (for serial types or explicit auto_increment)
	if column.AutoIncrement || tm.isSerialType(column.Type) {
		parts = append(parts, "autoincrement")
	}

	// 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))
	}

	// Nullable (Bun uses nullzero for nullable fields)
	// and notnull tag for explicitly non-nullable fields
	if !column.NotNull && !column.IsPrimaryKey {
		parts = append(parts, "nullzero")
	} else if column.NotNull && !column.IsPrimaryKey {
		parts = append(parts, "notnull")
	}

	// Check for indexes (unique indexes should be added to tag)
	if table != nil {
		for _, index := range sortIndexes(table.Indexes) {
			if !index.Unique {
				continue
			}
			// Check if this column is in the index
			for _, col := range index.Columns {
				if col == column.Name {
					// Add unique tag with index name for composite indexes
					// or simple unique for single-column indexes
					if len(index.Columns) > 1 {
						// Composite index - use index name
						parts = append(parts, fmt.Sprintf("unique:%s", index.Name))
					} else {
						// Single column - use index name if it's not auto-generated
						// Auto-generated names typically follow pattern: idx_tablename_columnname
						expectedAutoName := fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
						if index.Name == expectedAutoName {
							parts = append(parts, "unique")
						} else {
							parts = append(parts, fmt.Sprintf("unique:%s", index.Name))
						}
					}
					break
				}
			}
		}
	}

	// Join with commas and add trailing comma (Bun convention)
	return strings.Join(parts, ",") + ","
}

// BuildRelationshipTag generates Bun tag for relationship fields
// Bun format: bun:"rel:has-one,join:local_column=foreign_column"
func (tm *TypeMapper) BuildRelationshipTag(constraint *models.Constraint, relType string) string {
	var parts []string

	// Add relationship type
	parts = append(parts, fmt.Sprintf("rel:%s", relType))

	// Add join clause
	if len(constraint.Columns) > 0 && len(constraint.ReferencedColumns) > 0 {
		localCol := constraint.Columns[0]
		foreignCol := constraint.ReferencedColumns[0]

		// For has-many relationships, swap the columns
		// has-one: join:fk_in_this_table=pk_in_other_table
		// has-many: join:pk_in_this_table=fk_in_other_table
		if relType == "has-many" {
			parts = append(parts, fmt.Sprintf("join:%s=%s", foreignCol, localCol))
		} else {
			parts = append(parts, fmt.Sprintf("join:%s=%s", localCol, foreignCol))
		}
	}

	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())
}

// GetBunImport returns the import path for Bun
func (tm *TypeMapper) GetBunImport() string {
	return "github.com/uptrace/bun"
}
