package gorm

import (
	"fmt"
	"strings"

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

// TypeMapper handles type conversions between SQL and Go types
type TypeMapper struct {
	// Package alias for sql_types import
	sqlTypesAlias string
}

// NewTypeMapper creates a new TypeMapper with default settings
func NewTypeMapper() *TypeMapper {
	return &TypeMapper{
		sqlTypesAlias: "sql_types",
	}
}

// SQLTypeToGoType converts a SQL type to its Go equivalent
// Handles nullable types using ResolveSpec sql_types package
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
	// Normalize SQL type (lowercase, remove length/precision)
	baseType := tm.extractBaseType(sqlType)

	// If not null, use base Go types
	if notNull {
		return tm.baseGoType(baseType)
	}

	// For nullable fields, use sql_types
	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 {
	sqlType = strings.ToLower(strings.TrimSpace(sqlType))

	// Remove everything after '('
	if idx := strings.Index(sqlType, "("); idx > 0 {
		sqlType = sqlType[:idx]
	}

	return 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",
	}

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

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

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

// 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)
		if column.Length > 0 {
			typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Length)
		} else if 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 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 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 sql_types
func (tm *TypeMapper) GetSQLTypesImport() string {
	return "github.com/bitechdev/ResolveSpec/pkg/spectypes"
}
