package fetcher

// |@@| C

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	erro "gardening/src/lib/error"
	"gardening/src/lib/filter"
	"gardening/src/lib/sql_db"
	"strconv"
	"strings"
)

type Fetcher struct {
	SqlDb *sql_db.SqlDb
}

type RawFilter struct {
	Type      string
	Key       string
	Operation string
	Value     string
}

func (this *FetcherMod) FilterByKey(key string) filter.Filter {
	for _, allowedFilter := range this.AllowedFilters {
		if allowedFilter.GetKey() == key {
			return allowedFilter
		}
	}
	panic(erro.N("Cannot find a filter for the key").KV("key", key))
}

func (this *FetcherMod) ConvertRawFilters(rawFilters []RawFilter) {
	for _, rawFilter := range rawFilters {
		if rawFilter.Type != "fields" {
			continue
		}
		f := this.FilterByKey(rawFilter.Key)
		switch f.(type) {
		case *filter.IdFilter:
			this.idFilterCase(rawFilter)
		case *filter.IntegerFilter:
			this.integerFilterCase(rawFilter)
		case *filter.BooleanFilter:
			this.booleanFilterCase(rawFilter)
		case *filter.StringFilter:
			this.stringFilterCase(rawFilter)
		case *filter.RelationIdFilter:
			this.relationIdFilterCase(rawFilter)
		default:
			erro.P(erro.N("Unexpected filter").KVT("key", rawFilter.Key).
				KV("filterType", fmt.Sprintf("%T", f)).KV("operation", rawFilter.Operation))
		}
	}
}

func (this *FetcherMod) ExactIntegerValueFilter(column string, value int) {
	filter := &ExactIntegerValueFilter{
		Column: column,
		Value:  value,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) ExactStringValueFilter(column string, value string) {
	filter := &ExactStringValueFilter{
		Column: column,
		Value:  value,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) UnequalStringValueFilter(column string, value string) {
	filter := &UnequalStringValueFilter{
		Column: column,
		Value:  value,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) OperationStringValueFilter(column string, value string, operation string) {
	filter := &OperationStringValueFilter{
		Column:    column,
		Value:     value,
		Operation: operation,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) ContainsStringValueFilter(column string, value string) {
	filter := &ContainsStringValueFilter{
		Column: column,
		Value:  value,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) ExactBooleanValueFilter(column string, value bool) {
	filter := &ExactBooleanValueFilter{
		Column: column,
		Value:  value,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) AbsentValueFilter(column string) {
	filter := &AbsentValueFilter{
		Column: column,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) ExactIdValueFilter(column string, value string) {
	this.ExactStringValueFilter(column, value)
}

func (this *FetcherMod) AbsentIdValueFilter(column string) {
	this.AbsentValueFilter(column)
}

func (this *FetcherMod) InStringValuesFilter(column string, values []string) {
	filter := &InStringValuesFilter{
		Column: column,
		Values: values,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) NotInStringValuesFilter(column string, values []string) {
	filter := &NotInStringValuesFilter{
		Column: column,
		Values: values,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) InIntegerValuesFilter(column string, values []int) {
	filter := &InIntegerValuesFilter{
		Column: column,
		Values: values,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) IdFilter(column string, id string) {
	filter := &ExactStringValueFilter{
		Column: column,
		Value:  id,
	}
	this.Filters = append(this.Filters, filter)
}

func (this *FetcherMod) DescOrder(column string) {
	order := &FetcherOrderBy{
		Order:  "DESC",
		Column: column,
	}
	this.OrderBys = append(this.OrderBys, order)
}

func (this *FetcherMod) AscOrder(column string) {
	order := &FetcherOrderBy{
		Order:  "ASC",
		Column: column,
	}
	this.OrderBys = append(this.OrderBys, order)
}

func (this *FetcherMod) Debug() string {
	b, _ := json.Marshal(this.Filters)
	return string(b)
}

func (this *FetcherMod) integerFilterCase(rawFilter RawFilter) {
	if rawFilter.Operation != "eq" {
		return
	}
	v, err := strconv.Atoi(rawFilter.Value)
	if err != nil {
		panic("Invalid conversion")
	}
	this.ExactIntegerValueFilter(rawFilter.Key, v)
}

func (this *FetcherMod) booleanFilterCase(rawFilter RawFilter) {
	if rawFilter.Operation != "eq" {
		return
	}
	vBool := false
	if rawFilter.Value == "1" || rawFilter.Value == "true" {
		vBool = true
	}
	this.ExactBooleanValueFilter(rawFilter.Key, vBool)
}

func (this *FetcherMod) stringFilterCase(rawFilter RawFilter) {
	if rawFilter.Operation == "eq" {
		this.ExactStringValueFilter(rawFilter.Key, rawFilter.Value)
		return
	}
	if rawFilter.Operation == "contains" {
		this.ContainsStringValueFilter(rawFilter.Key, rawFilter.Value)
		return
	}
}

func (this *FetcherMod) relationIdFilterCase(rawFilter RawFilter) {
	if rawFilter.Operation == "eq" {
		this.ExactIdValueFilter(rawFilter.Key, rawFilter.Value)
	}
}

func (this *FetcherMod) idFilterCase(rawFilter RawFilter) {
	if rawFilter.Operation == "eq" {
		this.ExactIdValueFilter(rawFilter.Key, rawFilter.Value)
	}
}

type FetcherPagination struct {
	Total      int
	Page       int
	PerPage    int
	LastPage   int
	Modulation *FetcherMod
}

type FetcherMod struct {
	Paginated      bool
	One            bool
	Count          bool
	Page           int
	PerPage        int
	DefaultOrder   *FetcherOrderBy
	Relations      []*FetcherRelation
	Filters        []interface{}
	AllowedFilters filter.Filters
	OrderBys       []*FetcherOrderBy
}

type FetcherOrderBy struct {
	Order  string
	Column string
}

type FetcherRelation struct {
	Relation  string
	Columns   []string
	AllColumn bool
}

type ExactStringValueFilter struct {
	Column string
	Value  string
}

type UnequalStringValueFilter struct {
	Column string
	Value  string
}

type ContainsStringValueFilter struct {
	Column string
	Value  string
}

type ExactIntegerValueFilter struct {
	Column string
	Value  int
}

type AbsentValueFilter struct {
	Column string
}

type PresentValueFilter struct {
	Column string
}

type OperationStringValueFilter struct {
	Column    string
	Value     string
	Operation string
}

type InStringValuesFilter struct {
	Column string
	Values []string
}

type NotInStringValuesFilter struct {
	Column string
	Values []string
}

type InIntegerValuesFilter struct {
	Column string
	Values []int
}

type ExactBooleanValueFilter struct {
	Column string
	Value  bool
}

func (this *Fetcher) Filters(mod *FetcherMod, prefix string) (string, []interface{}) {
	var parts []string
	var args []interface{}
	if prefix != "" {
		prefix = prefix + "."
	}
	for _, mod := range mod.Filters {
		switch v := mod.(type) {
		case *ExactStringValueFilter:
			s := prefix + "\"" + v.Column + "\" = ?"
			args = append(args, v.Value)
			parts = append(parts, s)
		case *UnequalStringValueFilter:
			s := prefix + "\"" + v.Column + "\" != ?"
			args = append(args, v.Value)
			parts = append(parts, s)
		case *ContainsStringValueFilter:
			s := prefix + "\"" + v.Column + "\" LIKE ?"
			val := "%" + v.Value + "%"
			args = append(args, val)
			parts = append(parts, s)
		case *OperationStringValueFilter:
			s := prefix + "\"" + v.Column + "\" " + v.Operation + " ?"
			val := v.Value
			args = append(args, val)
			parts = append(parts, s)
		case *ExactIntegerValueFilter:
			s := prefix + "\"" + v.Column + "\" = ?"
			args = append(args, v.Value)
			parts = append(parts, s)
		case *ExactBooleanValueFilter:
			s := prefix + "\"" + v.Column + "\" = ?"
			arg := "0"
			if v.Value == true {
				arg = "1"
			}
			args = append(args, arg)
			parts = append(parts, s)
		case *AbsentValueFilter:
			s := prefix + "\"" + v.Column + "\" IS NULL"
			parts = append(parts, s)
		case *PresentValueFilter:
			s := prefix + "\"" + v.Column + "\" IS NOT NULL"
			parts = append(parts, s)
		case *InStringValuesFilter:
			s := prefix + "\"" + v.Column + "\" IN (" + this.repeater(len(v.Values)) + ")"
			for _, value := range v.Values {
				args = append(args, value)
			}
			parts = append(parts, s)
		case *NotInStringValuesFilter:
			s := prefix + "\"" + v.Column + "\" NOT IN (" + this.repeater(len(v.Values)) + ")"
			for _, value := range v.Values {
				args = append(args, value)
			}
			parts = append(parts, s)
		case *InIntegerValuesFilter:
			s := prefix + "\"" + v.Column + "\" IN (" + this.repeater(len(v.Values)) + ")"
			for _, value := range v.Values {
				args = append(args, value)
			}
			parts = append(parts, s)
		default:
			panic("Unknown filter")
		}
	}
	return strings.Join(parts, " AND "), args
}

func (this *Fetcher) CalculatePagination(pagination *FetcherPagination, mod *FetcherMod) {
	pagination.PerPage = mod.PerPage
	pagination.Page = mod.Page
	pagination.LastPage = ((pagination.Total - 1) / (mod.PerPage)) + 1
}

func (this *Fetcher) Paginate(mod *FetcherMod) string {
	if mod.PerPage < 1 {
		mod.PerPage = 25
	}
	if mod.PerPage > 100 {
		mod.PerPage = 100
	}
	if mod.Page < 1 {
		mod.Page = 1
	}
	from := (mod.Page - 1) * mod.PerPage
	return "LIMIT " + strconv.Itoa(mod.PerPage) + " OFFSET " + strconv.Itoa(from)
}

func (this *Fetcher) HasRelation(mod *FetcherMod, name string) bool {
	for _, relation := range mod.Relations {
		if relation.Relation == name {
			return true
		}
	}
	return false
}

func (this *Fetcher) Fields(fields []string, prefix string) string {
	if prefix != "" {
		prefix = prefix + "."
	}
	return prefix + "\"" + strings.Join(fields, "\", "+prefix+"\"") + "\""
}

func (this *Fetcher) BuildSelect(mod *FetcherMod, table string, columns []string) (string, []interface{}) {
	var parts []string
	parts = append(parts, "SELECT")
	if mod.Count {
		parts = append(parts, "COUNT(*)")
	} else {
		parts = append(parts, this.Fields(columns, "o"))
	}
	parts = append(parts, "FROM")
	parts = append(parts, table+" AS o")
	//parts = this.joins(parts, mod)
	filters, args := this.Filters(mod, "o")
	if filters != "" {
		parts = append(parts, "WHERE")
		parts = append(parts, filters)
	}
	if len(mod.OrderBys) > 0 {
		for _, orderBy := range mod.OrderBys {
			orderByS := "ORDER BY " + "o.\"" + orderBy.Column + "\"" + " " + orderBy.Order
			parts = append(parts, orderByS)
		}
	}
	if len(mod.OrderBys) == 0 && mod.DefaultOrder != nil {
		orderByS := "ORDER BY " + "o.\"" + mod.DefaultOrder.Column + "\"" + " " + mod.DefaultOrder.Order
		parts = append(parts, orderByS)
	}
	if mod.Paginated && !mod.Count {
		parts = append(parts, this.Paginate(mod))
	}
	if mod.One {
		parts = append(parts, "LIMIT 1")
	}
	return strings.Join(parts, " "), args
}

func (this *Fetcher) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
	return this.SqlDb.Query(ctx, query, args...)
}

func (this *Fetcher) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row {
	return this.SqlDb.QueryRow(ctx, query, args...)
}

func (this *Fetcher) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
	return this.SqlDb.Exec(ctx, query, args...)
}

func (this *Fetcher) IsNoRow(err error) bool {
	return this.SqlDb.IsNoRow(err)
}

func (this *Fetcher) MustHave(has bool, mod *FetcherMod) {
	if has == false {
		panic(erro.N("Must have").KV("mod", mod.Debug()))
	}
}

func (this *Fetcher) repeater(length int) string {
	return strings.TrimPrefix(strings.Repeat(",?", length), ",")
}
