package mo

import (
	"encoding/xml"
	"errors"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type Type int8

// https://docs.mongodb.com/manual/reference/bson-types/
const (
	TypeDouble     Type = 1   // flat64
	TypeString     Type = 2   // string
	TypeObject     Type = 3   // M
	TypeArray      Type = 4   // A
	TypeBinary     Type = 5   // Binary reference https://bsonspec.org/spec.html subtype
	TypeObjectId   Type = 7   // ObjectID
	TypeBoolean    Type = 8   // bool
	TypeDate       Type = 9   // DateTime
	TypeNull       Type = 10  // nil
	TypeRegex      Type = 11  // Regex
	TypeJavaScript Type = 13  // JavaScript
	TypeInt        Type = 16  // int32
	TypeTimestamp  Type = 17  // Timestamp DO NOT USE, for internal MongoDB only: https://docs.mongodb.com/manual/reference/bson-types/#timestamps
	TypeInt64      Type = 18  // int64
	TypeDecimal128 Type = 19  // Decimal128
	TypeMinKey     Type = -1  // MinKey
	TypeMaxKey     Type = 127 // MaxKey
)

var nameType = map[Type]string{
	1:   "double",
	2:   "string",
	3:   "object",
	4:   "array",
	5:   "binData",
	7:   "objectId",
	8:   "bool",
	9:   "date",
	10:  "null",
	11:  "regex",
	13:  "javascript",
	16:  "int",
	17:  "timestamp",
	18:  "long",
	19:  "decimal",
	-1:  "minKey",
	127: "maxKey",
}

var typeName = map[string]Type{
	"double":     1,
	"string":     2,
	"object":     3,
	"array":      4,
	"binData":    5,
	"objectId":   7,
	"bool":       8,
	"date":       9,
	"null":       10,
	"regex":      11,
	"javascript": 13,
	"int":        16,
	"timestamp":  17,
	"long":       18,
	"decimal":    19,
	"minKey":     -1,
	"maxKey":     127,
}

func (c *Type) UnmarshalXMLAttr(attr xml.Attr) error {
	if t, ok := typeName[attr.Value]; ok {
		*c = t
		return nil
	}
	return fmt.Errorf("unknown type: %s", attr.Value)
}

func (c *Type) String() string {
	return fmt.Sprintf("mo.Type(%s)", nameType[*c])
}

var (
	NilObjectID = ObjectID{}
)

type (
	ObjectID      = primitive.ObjectID
	Regex         = primitive.Regex
	JavaScript    = primitive.JavaScript
	Symbol        = primitive.Symbol
	Binary        = primitive.Binary
	CodeWithScope = primitive.CodeWithScope // Deprecated, reference https://bsonspec.org/spec.html Notes > Code
	Decimal128    = primitive.Decimal128
	Null          = primitive.Null
	DBPointer     = primitive.DBPointer
	DateTime      = primitive.DateTime
	Undefined     = primitive.Undefined
	Timestamp     = primitive.Timestamp
	D             = primitive.D
	E             = primitive.E
	M             = primitive.M
	A             = primitive.A
	MinKey        = primitive.MinKey
	MaxKey        = primitive.MaxKey

	Cursor                  = mongo.Cursor
	Pipeline                = mongo.Pipeline
	Client                  = mongo.Client
	Database                = mongo.Database
	Collection              = mongo.Collection
	IndexModel              = mongo.IndexModel
	Credential              = options.Credential
	CreateCollectionOptions = options.CreateCollectionOptions

	FindOptions      = options.FindOptions
	FindOneOptions   = options.FindOneOptions
	AggregateOptions = options.AggregateOptions
)

var (
	ErrNilObjectId = errors.New("objectId is zero")

	ErrNilDocument = mongo.ErrNilDocument
)

// type Lookup struct {
// 	Form         string `json:"from"`
// 	LocalField   string `json:"localField"`
// 	ForeignField string `json:"foreignField"`
// 	AS           string `json:"as"`
// }
//
// func (c Lookup) String() string {
// 	body, err := json.Marshal(c)
// 	if err != nil {
// 		return ""
// 	}
// 	return string(body)
// }

// Pipeline commands
const (
	PGroup   = "$group"
	PMatch   = "$match"
	PProject = "$project"
	PSort    = "$sort"
	PLimit   = "$limit"
	PSkip    = "$skip"
	PSet     = "$set"
	PLookup  = "$lookup"
)

// the Key commands
const (
	KOr  = "$or"
	KAnd = "$and"
	KNor = "$nor"
)

// the Value or value's key commands
const (
	VRegex    = "$regex"
	VPush     = "$push"     // for PGroup
	VEach     = "$each"     // for VPush
	VPosition = "$position" // for VPush
	VIn       = "$in"
	VNin      = "$nin"
	VEq       = "$eq"
	VNe       = "$ne"
	VGt       = "$gt"
	VGte      = "$gte"
	VLt       = "$lt"
	VLte      = "$lte"
	VNot      = "$not" // for Regex

	ASC  = int64(1)  // for PSort
	DESC = int64(-1) // for PSort
)

const (
	DefaultTimout = 10 * time.Second
)

const (
	DefaultDbName  = "test"
	DateTimeLayout = "2006-01-06 15:04:05"
)

const (
	SubtypeGeneric = 0x00
)

func NewObjectID() ObjectID {
	return primitive.NewObjectID()
}

func ObjectIDFromHex(s string) (ObjectID, error) {
	return primitive.ObjectIDFromHex(s)
}

func ObjectIdMustFromHex(s string) ObjectID {
	oid, err := ObjectIDFromHex(s)
	if err != nil {
		panic(err)
	}
	return oid
}

func IsValidObjectID(s string) bool {
	_, err := ObjectIDFromHex(s)
	return err == nil
}

func UnmarshalExtJSON(data []byte, canonical bool, val interface{}) error {
	return bson.UnmarshalExtJSON(data, canonical, val)
}

func NewDateTimeFromTime(t time.Time) DateTime {
	return primitive.NewDateTimeFromTime(t)
}

func NewDecimal128(h, l uint64) Decimal128 {
	return primitive.NewDecimal128(h, l)
}

func NewOptFind() *FindOptions {
	return options.Find()
}

func IsDuplicateKeyError(err error) bool {
	return mongo.IsDuplicateKeyError(err)
}
