package schema

import (
	"bytes"
	"database/sql"
	"encoding/json"
	"reflect"
	"time"

	"github.com/uptrace/bun/dialect"
	"github.com/uptrace/bun/dialect/sqltype"
	"github.com/uptrace/bun/internal"
)

var (
	bunNullTimeType = reflect.TypeFor[NullTime]()
	nullTimeType    = reflect.TypeFor[sql.NullTime]()
	nullBoolType    = reflect.TypeFor[sql.NullBool]()
	nullFloatType   = reflect.TypeFor[sql.NullFloat64]()
	nullIntType     = reflect.TypeFor[sql.NullInt64]()
	nullStringType  = reflect.TypeFor[sql.NullString]()
)

var sqlTypes = []string{
	reflect.Bool:       sqltype.Boolean,
	reflect.Int:        sqltype.BigInt,
	reflect.Int8:       sqltype.SmallInt,
	reflect.Int16:      sqltype.SmallInt,
	reflect.Int32:      sqltype.Integer,
	reflect.Int64:      sqltype.BigInt,
	reflect.Uint:       sqltype.BigInt,
	reflect.Uint8:      sqltype.SmallInt,
	reflect.Uint16:     sqltype.SmallInt,
	reflect.Uint32:     sqltype.Integer,
	reflect.Uint64:     sqltype.BigInt,
	reflect.Uintptr:    sqltype.BigInt,
	reflect.Float32:    sqltype.Real,
	reflect.Float64:    sqltype.DoublePrecision,
	reflect.Complex64:  "",
	reflect.Complex128: "",
	reflect.Array:      "",
	reflect.Interface:  "",
	reflect.Map:        sqltype.VarChar,
	reflect.Ptr:        "",
	reflect.Slice:      sqltype.VarChar,
	reflect.String:     sqltype.VarChar,
	reflect.Struct:     sqltype.VarChar,
}

func DiscoverSQLType(typ reflect.Type) string {
	switch typ {
	case timeType, nullTimeType, bunNullTimeType:
		return sqltype.Timestamp
	case nullBoolType:
		return sqltype.Boolean
	case nullFloatType:
		return sqltype.DoublePrecision
	case nullIntType:
		return sqltype.BigInt
	case nullStringType:
		return sqltype.VarChar
	case jsonRawMessageType:
		return sqltype.JSON
	}

	switch typ.Kind() {
	case reflect.Slice:
		if typ.Elem().Kind() == reflect.Uint8 {
			return sqltype.Blob
		}
	}

	return sqlTypes[typ.Kind()]
}

//------------------------------------------------------------------------------

var jsonNull = []byte("null")

// NullTime is a time.Time wrapper that marshals zero time as JSON null and SQL NULL.
type NullTime struct {
	time.Time
}

var (
	_ json.Marshaler   = (*NullTime)(nil)
	_ json.Unmarshaler = (*NullTime)(nil)
	_ sql.Scanner      = (*NullTime)(nil)
	_ QueryAppender    = (*NullTime)(nil)
)

func (tm NullTime) MarshalJSON() ([]byte, error) {
	if tm.IsZero() {
		return jsonNull, nil
	}
	return tm.Time.MarshalJSON()
}

func (tm *NullTime) UnmarshalJSON(b []byte) error {
	if bytes.Equal(b, jsonNull) {
		tm.Time = time.Time{}
		return nil
	}
	return tm.Time.UnmarshalJSON(b)
}

func (tm NullTime) AppendQuery(gen QueryGen, b []byte) ([]byte, error) {
	if tm.IsZero() {
		return dialect.AppendNull(b), nil
	}
	return gen.Dialect().AppendTime(b, tm.Time), nil
}

func (tm *NullTime) Scan(src any) error {
	if src == nil {
		tm.Time = time.Time{}
		return nil
	}

	switch src := src.(type) {
	case time.Time:
		tm.Time = src
		return nil
	case string:
		newtm, err := internal.ParseTime(src)
		if err != nil {
			return err
		}
		tm.Time = newtm
		return nil
	case []byte:
		newtm, err := internal.ParseTime(internal.String(src))
		if err != nil {
			return err
		}
		tm.Time = newtm
		return nil
	default:
		return scanError(bunNullTimeType, src)
	}
}
