package fixme

import "fmt"

type Code string

type ErrorEnvelope struct {
	Error interface{} `json:"error"`
}

type Error struct {
	Code    Code   `json:"code"`
	Message string `json:"message"`
}

func (s Error) Error() string {
	return fmt.Sprintf("%s: %s", s.Code, s.Message)
}

type Field struct {
	Code    Code   `json:"code"`
	Field   string `json:"field"`
	Message string `json:"message"`
	Path    []Leg  `json:"path"`
}

func (s Field) Error() string {
	path := pathToString(s.Path)
	if s.Message != "" {
		return fmt.Sprintf("%s: %s in %s.%s", s.Code, s.Message, path, s.Field)
	}
	return fmt.Sprintf("Error %s in %s.%s", s.Code, path, s.Field)
}

type Item struct {
	Code    Code   `json:"code"`
	List    string `json:"list"`
	Idx     int    `json:"idx"`
	Message string `json:"message"`
	Path    []Leg  `json:"path"`
}

func (s Item) Error() string {
	path := pathToString(s.Path)
	if s.Message != "" {
		return fmt.Sprintf("%s: %s in %s.%s[%d]", s.Code, s.Message, path, s.List, s.Idx)
	}
	return fmt.Sprintf("Error %s in %s.%s[%d]", s.Code, path, s.List, s.Idx)
}

type Leg struct {
	Field string `json:"field"`
	List  string `json:"list"`
	Idx   int    `json:"idx"`
}

func PrependField(err error, field string) error {
	leg := Leg{
		Field: field,
	}

	switch x := err.(type) {
	case Field:
		x.Path = append([]Leg{leg}, x.Path...)
		return x

	case Item:
		x.Path = append([]Leg{leg}, x.Path...)
		return x

	default:
		return err
	}
}

func PrependItem(err error, list string, idx int) error {
	leg := Leg{
		List: list,
		Idx:  idx,
	}

	switch x := err.(type) {
	case Field:
		x.Path = append([]Leg{leg}, x.Path...)
		return x

	case Item:
		x.Path = append([]Leg{leg}, x.Path...)
		return x

	default:
		return err
	}
}

func pathToString(legs []Leg) (str string) {
	for _, leg := range legs {
		if leg.Field != "" {
			str += fmt.Sprintf(".%s", leg.Field)
		} else {
			str += fmt.Sprintf(".%s[%d]", leg.List, leg.Idx)
		}
	}
	return
}

type Fixme struct {
	errorMessages map[Code]string
}

func New(errorMessages map[Code]string) *Fixme {
	if errorMessages == nil {
		panic("errorMessages option is nil")
	}

	s := new(Fixme)
	s.errorMessages = errorMessages
	return s
}

func (s *Fixme) Field(code Code, field string) error {
	msg, ok := s.errorMessages[code]
	if !ok {
		msg = fmt.Sprintf("error code: %s", code)
	}

	return Field{
		Code:    code,
		Field:   field,
		Message: msg,
	}
}

func (s *Fixme) Item(code Code, list string, idx int) error {
	msg, ok := s.errorMessages[code]
	if !ok {
		msg = fmt.Sprintf("error code: %s", code)
	}

	return Item{
		Code:    code,
		List:    list,
		Idx:     idx,
		Message: msg,
	}
}

func (s *Fixme) Error(code Code) error {
	msg, ok := s.errorMessages[code]
	if !ok {
		msg = fmt.Sprintf("error code: %s", code)
	}

	return Error{
		Code:    code,
		Message: msg,
	}
}

func (s *Fixme) CustomError(code Code, message string) error {
	if message == "" {
		var ok bool
		message, ok = s.errorMessages[code]
		if !ok {
			message = fmt.Sprintf("error code: %s", code)
		}
	}
	return Error{
		Code:    code,
		Message: message,
	}
}

func (s *Fixme) CustomField(code Code, message string, field string) error {
	if message == "" {
		var ok bool
		message, ok = s.errorMessages[code]
		if !ok {
			message = fmt.Sprintf("error code: %s", code)
		}
	}
	return Field{
		Code:    code,
		Message: message,
		Field:   field,
	}
}

func (s *Fixme) CustomItem(code Code, message string, list string, idx int) error {
	if message == "" {
		var ok bool
		message, ok = s.errorMessages[code]
		if !ok {
			message = fmt.Sprintf("error code: %s", code)
		}
	}
	return Item{
		Code:    code,
		Message: message,
		List:    list,
		Idx:     idx,
	}
}
