// Package metadata is used for manipulating the cozyMetadata field of the
// documents.
package metadata

import (
	"errors"
	"time"
)

// MetadataVersion represents the CozyMetadata version used.
const MetadataVersion = 1

// ErrSlugEmpty is returned when an UpdatedByApp entry is created with and empty
// slug
var ErrSlugEmpty = errors.New("Slug cannot be empty")

// UpdatedByAppEntry represents a modification made by an application to the
// document
type UpdatedByAppEntry struct {
	Slug     string    `json:"slug"`               // Slug of the app
	Date     time.Time `json:"date"`               // Date of the update
	Version  string    `json:"version,omitempty"`  // Version identifier of the app
	Instance string    `json:"instance,omitempty"` // URL of the instance
}

// CozyMetadata holds all the metadata of a document
type CozyMetadata struct {
	// Name or identifier for the version of the schema used by this document
	DocTypeVersion string `json:"doctypeVersion"`
	// Version of the cozyMetadata
	MetadataVersion int `json:"metadataVersion"`
	// Tell if the document if part of the favorites
	Favorite bool `json:"favorite,omitempty"`
	// Creation date of the cozy document
	CreatedAt time.Time `json:"createdAt"`
	// Slug of the app or konnector which created the document
	CreatedByApp string `json:"createdByApp,omitempty"`
	// Version identifier of the app
	CreatedByAppVersion string `json:"createdByAppVersion,omitempty"`
	// Last modification date of the cozy document
	UpdatedAt time.Time `json:"updatedAt"`
	// List of objects representing the applications which modified the cozy document
	UpdatedByApps []*UpdatedByAppEntry `json:"updatedByApps,omitempty"`
	// Identifier of the account in io.cozy.accounts (for konnectors)
	SourceAccount string `json:"sourceAccount,omitempty"`
	// Identifier unique to the account targeted by the connector (login most of the time)
	SourceIdentifier string `json:"sourceAccountIdentifier,omitempty"`
}

// New initializes a new CozyMetadata structure
func New() *CozyMetadata {
	now := time.Now()
	return &CozyMetadata{
		MetadataVersion: MetadataVersion,
		CreatedAt:       now,
		UpdatedAt:       now,
	}
}

// NewWithApp initializes a CozyMetadata with a slug and a version
// Version is optional
func NewWithApp(slug, version, doctypeVersion string) (*CozyMetadata, error) {
	if slug == "" {
		return nil, ErrSlugEmpty
	}
	md := New()
	md.CreatedByApp = slug
	if version != "" {
		md.CreatedByAppVersion = version
	}
	md.DocTypeVersion = doctypeVersion

	err := md.UpdatedByApp(slug, version)
	if err != nil {
		return nil, err
	}
	return md, nil
}

// Clone clones a CozyMetadata struct
func (cm *CozyMetadata) Clone() *CozyMetadata {
	cloned := *cm
	cloned.UpdatedByApps = make([]*UpdatedByAppEntry, len(cm.UpdatedByApps))
	copy(cloned.UpdatedByApps, cm.UpdatedByApps)
	return &cloned
}

// EnsureCreatedFields ensures that empty fields are filled, otherwise use
// the default metadata values during the creation process
func (cm *CozyMetadata) EnsureCreatedFields(defaultMetadata *CozyMetadata) {
	if cm.UpdatedAt.IsZero() {
		cm.UpdatedAt = defaultMetadata.UpdatedAt
	}
	if cm.CreatedByApp == "" {
		cm.CreatedByApp = defaultMetadata.CreatedByApp
	}
	if cm.DocTypeVersion == "" {
		cm.DocTypeVersion = defaultMetadata.DocTypeVersion
	}
	if cm.MetadataVersion == 0 {
		cm.MetadataVersion = defaultMetadata.MetadataVersion
	}
	if cm.UpdatedByApps == nil {
		cm.UpdatedByApps = defaultMetadata.UpdatedByApps
	}
}

// ChangeUpdatedAt updates the UpdatedAt timestamp
func (cm *CozyMetadata) ChangeUpdatedAt() {
	cm.UpdatedAt = time.Now()
}

// UpdatedByApp updates an entry either by updating the struct if the
// slug/version already exists or by appending a new entry to the list
func (cm *CozyMetadata) UpdatedByApp(slug, version string) error {
	if slug == "" {
		return ErrSlugEmpty
	}

	now := time.Now()
	cm.UpdatedAt = now
	updated := &UpdatedByAppEntry{Slug: slug, Date: now, Version: version}
	for i, entry := range cm.UpdatedByApps {
		if entry.Slug == slug {
			cm.UpdatedByApps[i] = updated
			return nil
		}
	}

	// The entry has not been found, adding it
	cm.UpdatedByApps = append(cm.UpdatedByApps, updated)
	return nil
}
