// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package model

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/mattermost/focalboard/server/utils"
)

var ErrInvalidBoardBlock = errors.New("invalid board block")
var ErrInvalidPropSchema = errors.New("invalid property schema")
var ErrInvalidProperty = errors.New("invalid property")
var ErrInvalidPropertyValue = errors.New("invalid property value")
var ErrInvalidPropertyValueType = errors.New("invalid property value type")
var ErrInvalidDate = errors.New("invalid date property")

// PropValueResolver allows PropDef.GetValue to further decode property values, such as
// looking up usernames from ids.
type PropValueResolver interface {
	GetUserByID(userID string) (*User, error)
}

// BlockProperties is a map of Prop's keyed by property id.
type BlockProperties map[string]BlockProp

// BlockProp represent a property attached to a block (typically a card).
type BlockProp struct {
	ID    string `json:"id"`
	Index int    `json:"index"`
	Name  string `json:"name"`
	Value string `json:"value"`
}

// PropSchema is a map of PropDef's keyed by property id.
type PropSchema map[string]PropDef

// PropDefOption represents an option within a property definition.
type PropDefOption struct {
	ID    string `json:"id"`
	Index int    `json:"index"`
	Color string `json:"color"`
	Value string `json:"value"`
}

// PropDef represents a property definition as defined in a board's Fields member.
type PropDef struct {
	ID      string                   `json:"id"`
	Index   int                      `json:"index"`
	Name    string                   `json:"name"`
	Type    string                   `json:"type"`
	Options map[string]PropDefOption `json:"options"`
}

// GetValue resolves the value of a property if the passed value is an ID for an option,
// otherwise returns the original value.
func (pd PropDef) GetValue(v interface{}, resolver PropValueResolver) (string, error) {
	switch pd.Type {
	case "select":
		// v is the id of an option
		id, ok := v.(string)
		if !ok {
			return "", ErrInvalidPropertyValueType
		}
		opt, ok := pd.Options[id]
		if !ok {
			return "", ErrInvalidPropertyValue
		}
		return strings.ToUpper(opt.Value), nil

	case "date":
		// v is a JSON string
		date, ok := v.(string)
		if !ok {
			return "", ErrInvalidPropertyValueType
		}
		return pd.ParseDate(date)

	case "person":
		// v is a userid
		userID, ok := v.(string)
		if !ok {
			return "", ErrInvalidPropertyValueType
		}
		if resolver != nil {
			user, err := resolver.GetUserByID(userID)
			if err != nil {
				return "", err
			}
			if user == nil {
				return userID, nil
			}
			return user.Username, nil
		}
		return userID, nil

	case "multiSelect":
		// v is a slice of strings containing option ids
		ms, ok := v.([]interface{})
		if !ok {
			return "", ErrInvalidPropertyValueType
		}
		var sb strings.Builder
		prefix := ""
		for _, optid := range ms {
			id, ok := optid.(string)
			if !ok {
				return "", ErrInvalidPropertyValueType
			}
			opt, ok := pd.Options[id]
			if !ok {
				return "", ErrInvalidPropertyValue
			}
			sb.WriteString(prefix)
			prefix = ", "
			sb.WriteString(strings.ToUpper(opt.Value))
		}
		return sb.String(), nil
	}
	return fmt.Sprintf("%v", v), nil
}

func (pd PropDef) ParseDate(s string) (string, error) {
	// s is a JSON snippet of the form: {"from":1642161600000, "to":1642161600000} in milliseconds UTC
	// The UI does not yet support date ranges.
	var m map[string]int64
	if err := json.Unmarshal([]byte(s), &m); err != nil {
		return s, err
	}
	tsFrom, ok := m["from"]
	if !ok {
		return s, ErrInvalidDate
	}
	date := utils.GetTimeForMillis(tsFrom).Format("January 02, 2006")
	tsTo, ok := m["to"]
	if ok {
		date += " -> " + utils.GetTimeForMillis(tsTo).Format("January 02, 2006")
	}
	return date, nil
}

// ParsePropertySchema parses a board block's `Fields` to extract the properties
// schema for all cards within the board.
// The result is provided as a map for quick lookup, and the original order is
// preserved via the `Index` field.
func ParsePropertySchema(board *Board) (PropSchema, error) {
	schema := make(map[string]PropDef)

	for i, prop := range board.CardProperties {
		pd := PropDef{
			ID:      getMapString("id", prop),
			Index:   i,
			Name:    getMapString("name", prop),
			Type:    getMapString("type", prop),
			Options: make(map[string]PropDefOption),
		}
		optsIface, ok := prop["options"]
		if ok {
			opts, ok := optsIface.([]interface{})
			if !ok {
				return nil, ErrInvalidPropSchema
			}
			for j, propOptIface := range opts {
				propOpt, ok := propOptIface.(map[string]interface{})
				if !ok {
					return nil, ErrInvalidPropSchema
				}
				po := PropDefOption{
					ID:    getMapString("id", propOpt),
					Index: j,
					Value: getMapString("value", propOpt),
					Color: getMapString("color", propOpt),
				}
				pd.Options[po.ID] = po
			}
		}
		schema[pd.ID] = pd
	}
	return schema, nil
}

func getMapString(key string, m map[string]interface{}) string {
	iface, ok := m[key]
	if !ok {
		return ""
	}

	s, ok := iface.(string)
	if !ok {
		return ""
	}
	return s
}

// ParseProperties parses a block's `Fields` to extract the properties. Properties typically exist on
// card blocks.  A resolver can optionally be provided to fetch usernames for `person` prop type.
func ParseProperties(block *Block, schema PropSchema, resolver PropValueResolver) (BlockProperties, error) {
	props := make(map[string]BlockProp)

	if block == nil {
		return props, nil
	}

	// `properties` contains a map (untyped at this point).
	propsIface, ok := block.Fields["properties"]
	if !ok {
		return props, nil // this is expected for blocks that don't have any properties.
	}

	blockProps, ok := propsIface.(map[string]interface{})
	if !ok {
		return props, fmt.Errorf("`properties` field wrong type: %w", ErrInvalidProperty)
	}

	if len(blockProps) == 0 {
		return props, nil
	}

	for k, v := range blockProps {
		s := fmt.Sprintf("%v", v)

		prop := BlockProp{
			ID:    k,
			Name:  k,
			Value: s,
		}

		def, ok := schema[k]
		if ok {
			val, err := def.GetValue(v, resolver)
			if err != nil {
				return props, fmt.Errorf("could not parse property value (%s): %w", fmt.Sprintf("%v", v), err)
			}
			prop.Name = def.Name
			prop.Value = val
			prop.Index = def.Index
		}
		props[k] = prop
	}
	return props, nil
}
