package web

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
)

var maxMemoryForMultipartParsing int64 = 10 * 1024 * 1024

type State struct {
	Header        http.Header
	Session       *SessionObject
	w             http.ResponseWriter
	r             *http.Request
	body          []byte
	isBodyRead    bool
	reply         *Reply
	wrappers      []Wrapper
	handler       HandleFunc
	wrapperIndex  int
	uploadTempDir string
	isFormParsed  bool
	values        url.Values
	files         map[string][]*multipart.FileHeader
}

func (s *State) Request() *http.Request {
	return s.r
}

func (s *State) ResponseWriter() http.ResponseWriter {
	return s.w
}

func (s *State) ReadBody() (_ []byte, err error) {
	if !s.isBodyRead {
		s.body, err = io.ReadAll(s.r.Body)
		if err != nil {
			err = fmt.Errorf("read request body: %s", err)
			return
		}
		s.r.Body.Close()
		s.isBodyRead = true
	}
	return s.body, nil
}

func (s *State) Cookie(name string) (*http.Cookie, error) {
	return s.r.Cookie(name)
}

func (s *State) Cookies() []*http.Cookie {
	return s.r.Cookies()
}

func (s *State) Next() (err error) {
	defer recoverErr(&err)
	if s.wrapperIndex < len(s.wrappers) {
		s.wrapperIndex++
		return s.wrappers[s.wrapperIndex-1].Wrap(s, s.reply)
	} else {
		return s.handler(s, s.reply)
	}
}

func (s *State) JSON(dst any) (err error) {
	contentType, _, err := mime.ParseMediaType(s.Header.Get("Content-Type"))
	if err != nil {
		return fmt.Errorf("mime.ParseMediaType: %s", err)
	}
	if contentType != "text/json" {
		return fmt.Errorf("wrong Content-Type: %s", contentType)
	}
	buf, err := s.ReadBody()
	if err != nil {
		return
	}
	err = json.Unmarshal(buf, dst)
	if err != nil {
		return fmt.Errorf("json.Unmarshal: %s", err)
	}
	return
}

func (s *State) parseForm() (err error) {
	if s.r.Method == "POST" {
		var contentType string
		contentType, _, err = mime.ParseMediaType(s.Header.Get("Content-Type"))
		if err != nil {
			return fmt.Errorf("mime.ParseMediaType: %s", err)
		}
		switch contentType {
		case "application/x-www-form-urlencoded":
			err = s.r.ParseForm()
			if err != nil {
				return fmt.Errorf("ParseForm: %s", err)
			}
			s.values = s.r.PostForm
		case "multipart/form-data":
			err = s.r.ParseMultipartForm(maxMemoryForMultipartParsing)
			if err != nil {
				return fmt.Errorf("ParseMultipartForm: %s", err)
			}
			s.values = s.r.MultipartForm.Value
			s.files = s.r.MultipartForm.File
		default:
			return fmt.Errorf("unsupported Content-Type: %s", contentType)
		}
	} else {
		err = s.r.ParseForm()
		if err != nil {
			return fmt.Errorf("ParseForm: %s", err)
		}
		s.values = s.r.Form
	}
	s.isFormParsed = true
	return
}

func (s *State) In(dst any) (err error) {
	v := reflect.ValueOf(dst)
	if v.Kind() != reflect.Ptr {
		return errors.New("argument must be pointer to struct")
	}
	v = v.Elem()
	if v.Kind() != reflect.Struct {
		return errors.New("argument must be pointer to struct")
	}
	if !s.isFormParsed {
		err = s.parseForm()
		if err != nil {
			return
		}
	}
	t := v.Type()
	for i := 0; i < t.NumField(); i++ {
		fv := v.Field(i)
		ft := t.Field(i)
		inputName := ft.Tag.Get("in")
		if inputName == "" {
			inputName = ft.Name
		}
		err = s.parseField(&fv, inputName)
		if err != nil {
			return
		}
	}
	return
}

func (s *State) Val(name string, dst any) (err error) {
	v := reflect.ValueOf(dst)
	if v.Kind() != reflect.Ptr {
		return errors.New("argument must be pointer to non-struct type")
	}
	v = v.Elem()
	if v.Kind() != reflect.Struct {
		return errors.New("argument must be pointer to non-struct type")
	}
	if !s.isFormParsed {
		if err = s.parseForm(); err != nil {
			return
		}
	}
	return s.parseField(&v, name)
}

func (s *State) parseField(field *reflect.Value, inputName string) (err error) {
	if field.Kind() == reflect.Slice {
		list, ok := s.values[inputName]
		if ok {
			switch field.Interface().(type) {
			case []string:
				field.Set(reflect.ValueOf(list))
			case []int:
				arr := make([]int, len(list))
				for i, str := range list {
					var (
						num int
						str = strings.TrimSpace(str)
					)
					num, err = strconv.Atoi(str)
					if err != nil {
						return fmt.Errorf("strconv.Atoi: %s; src=%s", err, str)
					}
					arr[i] = int(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []int64:
				arr := make([]int64, len(list))
				for i, str := range list {
					var (
						num int64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseInt(str, 10, 64)
					if err != nil {
						return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
					}
					arr[i] = num
				}
				field.Set(reflect.ValueOf(arr))
			case []int32:
				arr := make([]int32, len(list))
				for i, str := range list {
					var (
						num int64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseInt(str, 10, 32)
					if err != nil {
						return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
					}
					arr[i] = int32(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []int16:
				arr := make([]int16, len(list))
				for i, str := range list {
					var (
						num int64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseInt(str, 10, 16)
					if err != nil {
						return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
					}
					arr[i] = int16(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []int8:
				arr := make([]int8, len(list))
				for i, str := range list {
					var (
						num int64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseInt(str, 10, 8)
					if err != nil {
						return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
					}
					arr[i] = int8(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []uint:
				arr := make([]uint, len(list))
				for i, str := range list {
					var (
						num uint64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseUint(str, 10, 64)
					if err != nil {
						return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
					}
					arr[i] = uint(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []uint64:
				arr := make([]uint64, len(list))
				for i, str := range list {
					var (
						num uint64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseUint(str, 10, 64)
					if err != nil {
						return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
					}
					arr[i] = num
				}
				field.Set(reflect.ValueOf(arr))
			case []uint32:
				arr := make([]uint32, len(list))
				for i, str := range list {
					var (
						num uint64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseUint(str, 10, 32)
					if err != nil {
						return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
					}
					arr[i] = uint32(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []uint16:
				arr := make([]uint16, len(list))
				for i, str := range list {
					var (
						num uint64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseUint(str, 10, 16)
					if err != nil {
						return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
					}
					arr[i] = uint16(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []uint8:
				arr := make([]uint8, len(list))
				for i, str := range list {
					var (
						num uint64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseUint(str, 10, 8)
					if err != nil {
						return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
					}
					arr[i] = uint8(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []float64:
				arr := make([]float64, len(list))
				for i, str := range list {
					var (
						num float64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseFloat(str, 64)
					if err != nil {
						return fmt.Errorf("strconv.ParseFloat: %s; src=%s", err, str)
					}
					arr[i] = num
				}
				field.Set(reflect.ValueOf(arr))
			case []float32:
				arr := make([]float32, len(list))
				for i, str := range list {
					var (
						num float64
						str = strings.TrimSpace(str)
					)
					num, err = strconv.ParseFloat(str, 32)
					if err != nil {
						return fmt.Errorf("strconv.ParseFloat: %s; src=%s", err, str)
					}
					arr[i] = float32(num)
				}
				field.Set(reflect.ValueOf(arr))
			case []*multipart.FileHeader:
				if len(s.files[inputName]) > 0 {
					field.Set(reflect.ValueOf(s.files[inputName]))
				}
			default:
				return fmt.Errorf("field '%s' has list of unsupported type", inputName)
			}
		}
	} else {
		str := strings.TrimSpace(s.values.Get(inputName))
		switch field.Interface().(type) {
		case string:
			field.SetString(str)
		case int:
			var num int64
			num, err = strconv.ParseInt(str, 10, 0)
			if err != nil {
				return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
			}
			field.SetInt(num)
		case int64:
			var num int64
			num, err = strconv.ParseInt(str, 10, 64)
			if err != nil {
				return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
			}
			field.SetInt(num)
		case int32:
			var num int64
			num, err = strconv.ParseInt(str, 10, 32)
			if err != nil {
				return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
			}
			field.SetInt(num)
		case int16:
			var num int64
			num, err = strconv.ParseInt(str, 10, 16)
			if err != nil {
				return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
			}
			field.SetInt(num)
		case int8:
			var num int64
			num, err = strconv.ParseInt(str, 10, 8)
			if err != nil {
				return fmt.Errorf("strconv.ParseInt: %s; src=%s", err, str)
			}
			field.SetInt(num)
		case uint:
			var num uint64
			num, err = strconv.ParseUint(str, 10, 0)
			if err != nil {
				return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
			}
			field.SetUint(num)
		case uint64:
			var num uint64
			num, err = strconv.ParseUint(str, 10, 64)
			if err != nil {
				return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
			}
			field.SetUint(num)
		case uint32:
			var num uint64
			num, err = strconv.ParseUint(str, 10, 32)
			if err != nil {
				return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
			}
			field.SetUint(num)
		case uint16:
			var num uint64
			num, err = strconv.ParseUint(str, 10, 16)
			if err != nil {
				return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
			}
			field.SetUint(num)
		case uint8:
			var num uint64
			num, err = strconv.ParseUint(str, 10, 8)
			if err != nil {
				return fmt.Errorf("strconv.ParseUint: %s; src=%s", err, str)
			}
			field.SetUint(num)
		case float64:
			var num float64
			num, err = strconv.ParseFloat(str, 64)
			if err != nil {
				return fmt.Errorf("strconv.ParseFloat: %s; src=%s", err, str)
			}
			field.SetFloat(num)
		case float32:
			var num float64
			num, err = strconv.ParseFloat(str, 32)
			if err != nil {
				return fmt.Errorf("strconv.ParseFloat: %s; src=%s", err, str)
			}
			field.SetFloat(num)
		case bool:
			if str == "on" {
				field.SetBool(true)
			}
		case *multipart.FileHeader:
			if len(s.files[inputName]) > 0 {
				field.Set(reflect.ValueOf(s.files[inputName][0]))
			}
		default:
			return fmt.Errorf("field '%s' has unsupported type %#T",
				inputName, field.Interface())
		}
	}
	return
}
