package qx

import (
	"database/sql"
	"fmt"
	"reflect"
	"strings"
)

// Порядок определения имени: тег "c", тег "json", имя поля
func getColumnName(f reflect.StructField) string {
	c := f.Tag.Get("c")
	if c != "" {
		return c
	}

	json := f.Tag.Get("json")
	if json != "" && json != "-" {
		pair := strings.Split(json, ",")
		// Возможен тег json:",omitempty", поэтому нужно проверить на пустоту
		if pair[0] != "" {
			return pair[0]
		}
	}
	return f.Name
}

// ПАРСЕРЫ

func parseTag(tag string) (m map[string]string, err error) {
	m = make(map[string]string)
	for _, opt := range strings.Split(tag, ";") {
		opt = strings.TrimSpace(opt)
		if opt == "" {
			err = fmt.Errorf("empty option in 'qx' tag: %q", tag)
			return
		}
		kv := strings.Split(opt, "=")
		if len(kv) > 2 {
			err = fmt.Errorf("wrong option %q in 'qx' tag", opt)
			return
		}
		name := strings.TrimSpace(kv[0])
		var value string
		if len(kv) == 2 {
			value = strings.TrimSpace(kv[1])
		}
		m[name] = value
	}
	return
}

type List struct {
	Idx        int
	Field      string // для api.Err при проверке ключей
	StructInfo *structInfo
}

type Embed struct {
	Idx        int
	StructInfo *structInfo
}

// Временная структура - результат парсинга структур. Затем передаем Query билдеру.
type structInfo struct {
	StructName        string
	Table             string
	PrimaryKey        []string
	PrimaryKeyIndexes []int
	Seq               string
	Columns           []string
	//UpdateColumns     []string
	//UpdateIndexes     []int
	Indexes         []int
	Embed           []Embed        // структуры для JOIN в select запросах
	Lists           []List         // массивы для Insert
	ColumnMapping   map[string]int // columnName => field idx для проверки ключей
	ForeignKeys     []ForeignKey
	Unique          []Unique
	CompositeUnique []CompositeUnique
}

/*
В embed типе скорее всеего не будет колонки с parentID. Поэтому имя колонки по которой
идет связь, нужно указать в родительском типе в поле с массивом embed-значений.
*/
// Парсит структуру, частично валидирует. На основе полученной информации строим INSERT
// SELECT, UPDATE и DELETE запросы.
func parseStruct(t reflect.Type) (x *structInfo, err error) {
	x = new(structInfo)
	x.StructName = t.Name()
	x.ColumnMapping = make(map[string]int)

	if t.NumField() == 0 {
		err = fmt.Errorf("qx: empty struct not allowed")
		return
	}

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i) // reflect.StructField

		col := getColumnName(f)

		fieldType := f.Type.Kind()

		qxTag := f.Tag.Get("qx")

		if qxTag == "-" {
			continue
		}

		var (
			m map[string]string
		)

		if qxTag != "" {
			m, err = parseTag(qxTag)
			if err != nil {
				return nil, err
			}

			//fmt.Printf("%s: %# v\n\n", x.StructName, pretty.Formatter(m))

			for opt, value := range m {
				switch opt {
				case "table":
					if i != 0 {
						err = fmt.Errorf("qx: 'table' option in 'qx' tag allowed in 1st field only")
						return
					}
					if value == "" {
						err = fmt.Errorf("qx: empty 'table' option in 'qx' tag")
						return
					}
					x.Table = value

					//fmt.Printf("table (%s): %s\n", x.StructName, x.Table)

				case "pk":
					if i != 0 {
						err = fmt.Errorf("qx: 'pk' option in 'qx' tag allowed in 1st field only")
						return
					}
					if value == "" {
						// one column pk
						x.PrimaryKey = []string{col}
					} else {
						// composite pk
						columnList := strings.Split(value, ",")
						if len(columnList) == 1 {
							err = fmt.Errorf("qx: composite 'pk' option in 'qx' tag must have 2 more columns: %s", value)
							return
						}
						for idx, column := range columnList {
							columnList[idx] = strings.TrimSpace(column)
						}
						x.PrimaryKey = columnList
					}
				case "seq":
					// Seq для MySQL не важен, а для Oracle важен чтобы получить LastInsertId
					if i != 0 {
						err = fmt.Errorf("qx: 'seq' option in 'qx' tag allowed in 1st field only")
						return
					}
					if value == "" {
						err = fmt.Errorf("qx: empty 'seq' option in 'qx' tag")
						return
					}
					x.Seq = value

				case "fk":
					if value == "" {
						err = fmt.Errorf("qx: empty 'fk' option in 'qx' tag")
						return
					}
					// добавляем  foreign key
					x.ForeignKeys = append(x.ForeignKeys, ForeignKey{
						Table:    value,
						Column:   col,
						FieldIdx: i,
					})
				case "unique":
					if value == "" {
						// simple unique
						x.Unique = append(x.Unique, Unique{
							Column:   col,
							FieldIdx: i,
						})
					} else {
						// composite unique
						columnList := strings.Split(value, ",")
						if len(columnList) == 1 {
							err = fmt.Errorf("qx: composite 'unique' option in 'qx' tag must have 2 more columns: %s", value)
							return
						}
						for idx, column := range columnList {
							columnList[idx] = strings.TrimSpace(column)
						}
						x.CompositeUnique = append(x.CompositeUnique, CompositeUnique{
							Columns: columnList,
							Field:   col,
						})
					}

				//case "scalar":
				// pass

				default:
					err = fmt.Errorf("unknown option %q in 'qx' tag", opt)
					return
				}

			}
		}

		switch fieldType {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
			reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
			reflect.String, reflect.Bool, reflect.Float32, reflect.Float64:

			x.Columns = append(x.Columns, col)
			x.Indexes = append(x.Indexes, i)
			x.ColumnMapping[col] = i

		case reflect.Slice:
			// type of slice's element
			elemType := f.Type.Elem()

			//fmt.Printf("elemType: %v\n", elemType)

			if elemType.Kind() == reflect.Ptr {
				elemType = elemType.Elem()
			}

			if elemType.Kind() == reflect.Struct {
				var nested *structInfo

				nested, err = parseStruct(elemType)
				if err != nil {
					return
				}
				x.Lists = append(x.Lists, List{
					Idx:        i,
					StructInfo: nested,
					Field:      getColumnName(f), // имя коллекции для Leg{Field, Idx}
				})
			} else {
				if elemType.Kind() != reflect.Uint8 {
					err = fmt.Errorf("qx: embed list of type %s.%s must be []struct or []*struct, not '%v'",
						x.StructName, f.Name, elemType)
					return
				}

				// []byte
				col := getColumnName(f)

				// fix - add parse tag

				x.Columns = append(x.Columns, col)
				x.Indexes = append(x.Indexes, i)
				x.ColumnMapping[col] = i
			}

		case reflect.Struct:
			// FIX - временный хак из-за того, что метод получает на вход reflect.Type,
			// поэтому определить тип структуры нужно ее создать
			elem := reflect.New(f.Type).Elem().Interface()

			switch elem.(type) {
			case sql.NullInt64, sql.NullInt32, sql.NullInt16,
				sql.NullFloat64, sql.NullString, sql.NullBool, sql.NullTime, sql.NullByte:
				// NULL загрушки
				x.Columns = append(x.Columns, col)
				x.Indexes = append(x.Indexes, i)
				x.ColumnMapping[col] = i

			default:
				// embed
				var nested *structInfo

				nested, err = parseStruct(f.Type)
				if err != nil {
					return
				}
				// /nested.Field = getColumnName(f)
				x.Embed = append(x.Embed, Embed{
					Idx:        i,
					StructInfo: nested,
				})
			}

		default:
			err = fmt.Errorf("qx: unsupported field type %s", fieldType)
			return
		}
	}

	//fmt.Printf("struct: %# v\n", pretty.Formatter(x))

	// Валидация
	if x.Seq != "" {
		if len(x.PrimaryKey) != 1 {
			err = fmt.Errorf("qx: 'seq' option in 'qx' tag requires one column primary key")
			return
		}
	}
	// Проверяем compositeUnique колонки
	// Проверянть unique колонки нет смысла, ибо они все валидны
	for uniqueIdx, u := range x.CompositeUnique {
		for _, column := range u.Columns {
			idx, ok := x.ColumnMapping[column]
			if !ok {
				// FIX
				//err = fmt.Errorf("Unique constraint refers to unknown column %q in table %q",
				//column, info.Table)
				err = fmt.Errorf("qx: unknown column %q in composite unique %q",
					column, u.Columns)
				return
			}
			u.Indexes = append(u.Indexes, idx)
		}
		x.CompositeUnique[uniqueIdx] = u
	}
	// Проверяем колонки композитного primary key.
	// Одноколоночный primary key валиден по умолчанию, ибо не задается текстом,
	// а помечает существующую колонку.
	if len(x.PrimaryKey) > 1 {
		for _, column := range x.PrimaryKey {
			_, ok := x.ColumnMapping[column]
			if !ok {
				err = fmt.Errorf("qx: unknown column %q in primary key %q",
					column, x.PrimaryKey)
				return
			}
		}
	}
	// Для DELETE/UPDATE/ONE запросов
	for _, column := range x.PrimaryKey {
		idx, ok := x.ColumnMapping[column]
		if !ok {
			err = fmt.Errorf("qx: unknown column %q in primary key", column)
			return
		}
		x.PrimaryKeyIndexes = append(x.PrimaryKeyIndexes, idx)
	}

	return
}

/////////////////////

type ForeignKey struct {
	Table    string
	Column   string
	FieldIdx int
	//Field  string // для api.Err
}

type Unique struct {
	Column   string
	FieldIdx int
	//Field  string // для api.Err
	//Code   int    // для api.Err
}

type CompositeUnique struct {
	Columns []string
	Field   string // для api.Err
	Indexes []int
	//Code    int    // для api.Err
}

type Constraint struct {
	Table       string
	ForeignKeys []ForeignKey
	//Unique          []Unique
	CompositeUnique []CompositeUnique
}
