package model

import (
	"errors"
	"fmt"

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

var ErrBoardIDMismatch = errors.New("Board IDs do not match")

type ErrInvalidCard struct {
	msg string
}

func NewErrInvalidCard(msg string) ErrInvalidCard {
	return ErrInvalidCard{
		msg: msg,
	}
}

func (e ErrInvalidCard) Error() string {
	return fmt.Sprintf("invalid card, %s", e.msg)
}

var ErrNotCardBlock = errors.New("not a card block")

type ErrInvalidFieldType struct {
	field string
}

func (e ErrInvalidFieldType) Error() string {
	return fmt.Sprintf("invalid type for field '%s'", e.field)
}

// Card represents a group of content blocks and properties.
// swagger:model
type Card struct {
	// The id for this card
	// required: false
	ID string `json:"id"`

	// The id for board this card belongs to.
	// required: false
	BoardID string `json:"boardId"`

	// The id for user who created this card
	// required: false
	CreatedBy string `json:"createdBy"`

	// The id for user who last modified this card
	// required: false
	ModifiedBy string `json:"modifiedBy"`

	// The display title
	// required: false
	Title string `json:"title"`

	// An array of content block ids specifying the ordering of content for this card.
	// required: false
	ContentOrder []string `json:"contentOrder"`

	// The icon of the card
	// required: false
	Icon string `json:"icon"`

	// True if this card belongs to a template
	// required: false
	IsTemplate bool `json:"isTemplate"`

	// A map of property ids to property values (option ids, strings, array of option ids)
	// required: false
	Properties map[string]any `json:"properties"`

	// The creation time in milliseconds since the current epoch
	// required: false
	CreateAt int64 `json:"createAt"`

	// The last modified time in milliseconds since the current epoch
	// required: false
	UpdateAt int64 `json:"updateAt"`

	// The deleted time in milliseconds since the current epoch. Set to indicate this card is deleted
	// required: false
	DeleteAt int64 `json:"deleteAt"`
}

// Populate populates a Card with default values.
func (c *Card) Populate() {
	if c.ID == "" {
		c.ID = utils.NewID(utils.IDTypeCard)
	}
	if c.ContentOrder == nil {
		c.ContentOrder = make([]string, 0)
	}
	if c.Properties == nil {
		c.Properties = make(map[string]any)
	}
	now := utils.GetMillis()
	if c.CreateAt == 0 {
		c.CreateAt = now
	}
	if c.UpdateAt == 0 {
		c.UpdateAt = now
	}
}

func (c *Card) PopulateWithBoardID(boardID string) {
	c.BoardID = boardID
	c.Populate()
}

// CheckValid returns an error if the Card has invalid field values.
func (c *Card) CheckValid() error {
	if c.ID == "" {
		return ErrInvalidCard{"ID is missing"}
	}
	if c.BoardID == "" {
		return ErrInvalidCard{"BoardID is missing"}
	}
	if c.ContentOrder == nil {
		return ErrInvalidCard{"ContentOrder is missing"}
	}
	if uniseg.GraphemeClusterCount(c.Icon) > 1 {
		return ErrInvalidCard{"Icon can have only one grapheme"}
	}
	if c.Properties == nil {
		return ErrInvalidCard{"Properties"}
	}
	if c.CreateAt == 0 {
		return ErrInvalidCard{"CreateAt"}
	}
	if c.UpdateAt == 0 {
		return ErrInvalidCard{"UpdateAt"}
	}
	return nil
}

// CardPatch is a patch for modifying cards
// swagger:model
type CardPatch struct {
	// The display title
	// required: false
	Title *string `json:"title"`

	// An array of content block ids specifying the ordering of content for this card.
	// required: false
	ContentOrder *[]string `json:"contentOrder"`

	// The icon of the card
	// required: false
	Icon *string `json:"icon"`

	// A map of property ids to property option ids to be updated
	// required: false
	UpdatedProperties map[string]any `json:"updatedProperties"`
}

// Patch returns an updated version of the card.
func (p *CardPatch) Patch(card *Card) *Card {
	if p.Title != nil {
		card.Title = *p.Title
	}

	if p.ContentOrder != nil {
		card.ContentOrder = *p.ContentOrder
	}

	if p.Icon != nil {
		card.Icon = *p.Icon
	}

	if card.Properties == nil {
		card.Properties = make(map[string]any)
	}

	// if there are properties marked for update, we replace the
	// existing ones or add them
	for propID, propVal := range p.UpdatedProperties {
		card.Properties[propID] = propVal
	}

	return card
}

// CheckValid returns an error if the CardPatch has invalid field values.
func (p *CardPatch) CheckValid() error {
	if p.Icon != nil && uniseg.GraphemeClusterCount(*p.Icon) > 1 {
		return ErrInvalidCard{"Icon can have only one grapheme"}
	}
	return nil
}

// Card2Block converts a card to block using a shallow copy. Not needed once cards are first class entities.
func Card2Block(card *Card) *Block {
	fields := make(map[string]interface{})

	fields["contentOrder"] = card.ContentOrder
	fields["icon"] = card.Icon
	fields["isTemplate"] = card.IsTemplate
	fields["properties"] = card.Properties

	return &Block{
		ID:         card.ID,
		ParentID:   card.BoardID,
		CreatedBy:  card.CreatedBy,
		ModifiedBy: card.ModifiedBy,
		Schema:     1,
		Type:       TypeCard,
		Title:      card.Title,
		Fields:     fields,
		CreateAt:   card.CreateAt,
		UpdateAt:   card.UpdateAt,
		DeleteAt:   card.DeleteAt,
		BoardID:    card.BoardID,
	}
}

// Block2Card converts a block to a card. Not needed once cards are first class entities.
func Block2Card(block *Block) (*Card, error) {
	if block.Type != TypeCard {
		return nil, fmt.Errorf("cannot convert block to card: %w", ErrNotCardBlock)
	}

	contentOrder := make([]string, 0)
	icon := ""
	isTemplate := false
	properties := make(map[string]any)

	if co, ok := block.Fields["contentOrder"]; ok {
		switch arr := co.(type) {
		case []any:
			for _, str := range arr {
				if id, ok := str.(string); ok {
					contentOrder = append(contentOrder, id)
				} else {
					return nil, ErrInvalidFieldType{"contentOrder item"}
				}
			}
		case []string:
			contentOrder = append(contentOrder, arr...)
		default:
			return nil, ErrInvalidFieldType{"contentOrder"}
		}
	}

	if iconAny, ok := block.Fields["icon"]; ok {
		if id, ok := iconAny.(string); ok {
			icon = id
		} else {
			return nil, ErrInvalidFieldType{"icon"}
		}
	}

	if isTemplateAny, ok := block.Fields["isTemplate"]; ok {
		if b, ok := isTemplateAny.(bool); ok {
			isTemplate = b
		} else {
			return nil, ErrInvalidFieldType{"isTemplate"}
		}
	}

	if props, ok := block.Fields["properties"]; ok {
		if propMap, ok := props.(map[string]any); ok {
			for k, v := range propMap {
				properties[k] = v
			}
		} else {
			return nil, ErrInvalidFieldType{"properties"}
		}
	}

	card := &Card{
		ID:           block.ID,
		BoardID:      block.BoardID,
		CreatedBy:    block.CreatedBy,
		ModifiedBy:   block.ModifiedBy,
		Title:        block.Title,
		ContentOrder: contentOrder,
		Icon:         icon,
		IsTemplate:   isTemplate,
		Properties:   properties,
		CreateAt:     block.CreateAt,
		UpdateAt:     block.UpdateAt,
		DeleteAt:     block.DeleteAt,
	}
	card.Populate()
	return card, nil
}

// CardPatch2BlockPatch converts a CardPatch to a BlockPatch. Not needed once cards are first class entities.
func CardPatch2BlockPatch(cardPatch *CardPatch) (*BlockPatch, error) {
	if err := cardPatch.CheckValid(); err != nil {
		return nil, err
	}

	blockPatch := &BlockPatch{
		Title: cardPatch.Title,
	}

	updatedFields := make(map[string]any, 0)

	if cardPatch.ContentOrder != nil {
		updatedFields["contentOrder"] = cardPatch.ContentOrder
	}
	if cardPatch.Icon != nil {
		updatedFields["icon"] = cardPatch.Icon
	}

	properties := make(map[string]any)
	for k, v := range cardPatch.UpdatedProperties {
		properties[k] = v
	}

	if len(properties) != 0 {
		updatedFields["properties"] = cardPatch.UpdatedProperties
	}

	blockPatch.UpdatedFields = updatedFields

	return blockPatch, nil
}
