package pkg

import (
	"context"
	"database/sql/driver"
	"errors"
	"fmt"
	"github.com/jackc/pgtype"
	"html/template"
	"io/ioutil"
	"net/http"
	"net/mail"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var tagsRegex = regexp.MustCompile("[^a-z0-9-]+")

type Attribute struct {
	Key, Val string
}

type InputField struct {
	Name       string
	Label      string
	Type       string
	Val        string
	Is         string
	Required   bool
	Attributes []template.HTMLAttr
	Errors     []error
}

func (field *InputField) Scan(value interface{}) error {
	if value == nil {
		field.Val = ""
		return nil
	}
	switch v := value.(type) {
	case time.Time:
		if field.Type == "date" {
			field.Val = v.Format("2006-01-02")
		} else if field.Type == "time" {
			field.Val = v.Format("15:04")
		} else {
			field.Val = v.Format(time.RFC3339)
		}
	default:
		field.Val = fmt.Sprintf("%v", v)
	}
	return nil
}

func (field *InputField) Value() (driver.Value, error) {
	return field.Val, nil
}

func (field *InputField) FillValue(r *http.Request) {
	field.Val = strings.TrimSpace(r.FormValue(field.Name))
}

func (field *InputField) Integer() int {
	value, err := strconv.Atoi(field.Val)
	if err != nil {
		panic(err)
	}
	return value
}

func (field *InputField) IntegerOrNil() interface{} {
	if field.Val != "" {
		if i := field.Integer(); i > 0 {
			return i
		}
	}
	return nil
}

func (field *InputField) Float64() float64 {
	value, err := strconv.ParseFloat(field.Val, 64)
	if err != nil {
		panic(err)
	}
	return value
}

func (field *InputField) String() string {
	return field.Val
}

type SelectOption struct {
	Value string
	Label string
	Group string
}

type SelectField struct {
	Name       string
	Label      string
	Selected   []string
	Options    []*SelectOption
	Attributes []template.HTMLAttr
	Required   bool
	Multiple   bool
	EmptyLabel string
	Errors     []error
}

func (field *SelectField) Scan(value interface{}) error {
	if value == nil {
		field.Selected = append(field.Selected, "")
		return nil
	}
	if str, ok := value.(string); ok {
		if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
			for _, element := range array.Elements {
				field.Selected = append(field.Selected, element)
			}
			return nil
		}
	}
	field.Selected = append(field.Selected, fmt.Sprintf("%v", value))
	return nil
}

func (field *SelectField) Value() (driver.Value, error) {
	return field.String(), nil
}

func (field *SelectField) String() string {
	if field.Selected == nil {
		return ""
	}
	return field.Selected[0]
}

func (field *SelectField) FillValue(r *http.Request) {
	field.Selected = r.Form[field.Name]
}

func (field *SelectField) HasValidOptions() bool {
	for _, selected := range field.Selected {
		if !field.isValidOption(selected) {
			return false
		}
	}
	return true
}

func (field *SelectField) IsSelected(v string) bool {
	for _, selected := range field.Selected {
		if selected == v {
			return true
		}
	}
	return false
}

func (field *SelectField) FindOption(value string) *SelectOption {
	for _, option := range field.Options {
		if option.Value == value {
			return option
		}
	}
	return nil
}

func (field *SelectField) isValidOption(selected string) bool {
	return field.FindOption(selected) != nil
}

func (field *SelectField) Clear() {
	field.Selected = []string{}
}

func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
	rows, err := conn.Query(ctx, sql, args...)
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	var options []*SelectOption
	for rows.Next() {
		option := &SelectOption{}
		err = rows.Scan(&option.Value, &option.Label)
		if err != nil {
			panic(err)
		}
		options = append(options, option)
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}

	return options
}

func MustGetGroupedOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
	rows, err := conn.Query(ctx, sql, args...)
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	var options []*SelectOption
	for rows.Next() {
		option := &SelectOption{}
		err = rows.Scan(&option.Value, &option.Label, &option.Group)
		if err != nil {
			panic(err)
		}
		options = append(options, option)
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}

	return options
}

func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
	return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
}

type RadioOption struct {
	Value string
	Label string
}

type RadioField struct {
	Name       string
	Label      string
	Selected   string
	Options    []*RadioOption
	Attributes []template.HTMLAttr
	Required   bool
	Errors     []error
}

func (field *RadioField) Scan(value interface{}) error {
	if value == nil {
		field.Selected = ""
		return nil
	}
	field.Selected = fmt.Sprintf("%v", value)
	return nil
}

func (field *RadioField) Value() (driver.Value, error) {
	return field.Selected, nil
}

func (field *RadioField) String() string {
	return field.Selected
}

func (field *RadioField) FillValue(r *http.Request) {
	field.Selected = strings.TrimSpace(r.FormValue(field.Name))
}

func (field *RadioField) IsSelected(v string) bool {
	return field.Selected == v
}

func (field *RadioField) FindOption(value string) *RadioOption {
	for _, option := range field.Options {
		if option.Value == value {
			return option
		}
	}
	return nil
}

func (field *RadioField) isValidOption(selected string) bool {
	return field.FindOption(selected) != nil
}

type FileField struct {
	Name             string
	Label            string
	MaxSize          int64
	OriginalFileName string
	ContentType      string
	Content          []byte
	Errors           []error
}

func (field *FileField) FillValue(r *http.Request) error {
	file, header, err := r.FormFile(field.Name)
	if err != nil {
		if err == http.ErrMissingFile {
			return nil
		}
		return err
	}
	defer file.Close()
	field.Content, err = ioutil.ReadAll(file)
	if err != nil {
		return err
	}
	field.OriginalFileName = header.Filename
	field.ContentType = header.Header.Get("Content-Type")
	if len(field.Content) == 0 {
		field.ContentType = http.DetectContentType(field.Content)
	}
	return nil
}

type TagsField struct {
	Name       string
	Label      string
	Tags       []string
	Attributes []template.HTMLAttr
	Required   bool
	Errors     []error
}

func (field *TagsField) Value() (driver.Value, error) {
	if field.Tags == nil {
		return []string{}, nil
	}
	return field.Tags, nil
}

func (field *TagsField) Scan(value interface{}) error {
	if value == nil {
		return nil
	}
	if str, ok := value.(string); ok {
		if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
			for _, element := range array.Elements {
				field.Tags = append(field.Tags, element)
			}
			return nil
		}
	}
	field.Tags = append(field.Tags, fmt.Sprintf("%v", value))
	return nil
}

func (field *TagsField) FillValue(r *http.Request) {
	field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",")
	if len(field.Tags) == 1 && len(field.Tags[0]) == 0 {
		field.Tags = []string{}
	}
}

func (field *TagsField) String() string {
	return strings.Join(field.Tags, ",")
}

type ToggleField struct {
	Name         string
	Label        string
	Selected     string
	FirstOption  *ToggleOption
	SecondOption *ToggleOption
	Attributes   []template.HTMLAttr
	Errors       []error
}

type ToggleOption struct {
	Value       string
	Label       string
	Description string
}

func (field *ToggleField) FillValue(r *http.Request) {
	field.Selected = strings.TrimSpace(r.FormValue(field.Name))
	if field.Selected != field.FirstOption.Value && field.Selected != field.SecondOption.Value {
		field.Selected = field.FirstOption.Value
	}
}

type FormValidator struct {
	Valid bool
}

func newFormValidator() *FormValidator {
	return &FormValidator{true}
}

func (v *FormValidator) AllOK() bool {
	return v.Valid
}

func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
	return v.checkInput(field, field.Val != "", message)
}

func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message string) bool {
	return v.checkInput(field, len(field.Val) >= min, message)
}

func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
	_, err := mail.ParseAddress(field.Val)
	return v.checkInput(field, err == nil, message)
}

func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
	// TODO: actual VATIN validation
	return v.checkInput(field, true, message)
}

func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
	// TODO: actual phone validation
	return v.checkInput(field, true, message)
}

func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
	return v.checkInput(confirm, password.Val == confirm.Val, message)
}

func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
	return v.checkSelect(field, field.HasValidOptions(), message)
}

func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
	repeated := false
	groups := map[string]bool{}
	for _, selected := range field.Selected {
		option := field.FindOption(selected)
		if exists := groups[option.Group]; exists {
			repeated = true
			break
		}
		groups[option.Group] = true
	}
	return v.checkSelect(field, !repeated, message)
}

func (v *FormValidator) CheckValidURL(field *InputField, message string) bool {
	_, err := url.ParseRequestURI(field.Val)
	return v.checkInput(field, err == nil, message)
}

func (v *FormValidator) CheckValidDate(field *InputField, message string) bool {
	_, err := time.Parse("2006-01-02", field.Val)
	return v.checkInput(field, err == nil, message)
}

func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
	pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
	match, err := regexp.MatchString(pattern, field.Val)
	if err != nil {
		panic(err)
	}
	return v.checkInput(field, match, message)
}

func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, message string) bool {
	value, err := strconv.Atoi(field.Val)
	return v.checkInput(field, err == nil && value >= min && value <= max, message)
}

func (v *FormValidator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool {
	value, err := strconv.ParseFloat(field.Val, 64)
	return v.checkInput(field, err == nil && value >= min && value <= max, message)
}

func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
	if !ok {
		field.Errors = append(field.Errors, errors.New(message))
		v.Valid = false
	}
	return ok
}

func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) bool {
	if !ok {
		field.Errors = append(field.Errors, errors.New(message))
		v.Valid = false
	}
	return ok
}