// Structs to temporarily hold audit data

package models

import (
	"encoding/json"
	"strconv"
	"time"

	"code.justin.tv/foundation/history-worker/report"
)

const (
	MAX_DESCRIPTION = 1024
	// RFC3339Nano doesn't work here because it drops 0-only milliseconds, which causes
	// ElasticSearch to throw a parse error
	// Specifying 000 instead of 999 for the fractional seconds portion forces a 0 millisecond
	// value to be output as 000 instead of not being output at all.  Same goes for micro and
	// nanoseconds.
	// (In golang you specify a datetime format by showing how the datetime
	// January 2nd, 2006 3:04:05 PM MST would be expressed in that format)
	ISO8601_NANO_FORMAT   = "2006-01-02T15:04:05.000000000Z07:00"
	ISO8601_MICRO_FORMAT  = "2006-01-02T15:04:05.000000Z07:00"
	ISO8601_MILLI_FORMAT  = "2006-01-02T15:04:05.000Z07:00"
	ISO8601_SECOND_FORMAT = "2006-01-02T15:04:05Z07:00"
)

type Audit struct {
	Uuid          string      `json:"uuid"`
	Action        string      `json:"action"`
	UserType      string      `json:"user_type"`
	UserId        string      `json:"user_id"`
	ResourceType  string      `json:"resource_type"`
	ResourceId    string      `json:"resource_id"`
	Description   string      `json:"description"`
	ExpiredAt     string      `json:"expired_at,omitempty"`
	CreatedAt     string      `json:"created_at"`
	ExpiredAtTime time.Time   `json:"-"`
	CreatedAtTime time.Time   `json:"-"`
	Expiry        int64       `json:"expiry,omitempty"`
	Changes       []ChangeSet `json:"changes"`
}

// JSON representation of the audit record
func (audit *Audit) Body() string {
	bolB, err := json.Marshal(audit)

	if err != nil {
		return ""
	}

	return string(bolB)
}

// DynamoDb's unix format for expiration time
func (audit *Audit) DynamoExpiredAt() string {
	return strconv.FormatInt(audit.ExpiredAtTime.Unix(), 10)
}

// Need to support parsing different fractional seconds being sent by clients
func (audit *Audit) parseCreatedAt() (time.Time, error) {
	t, err := time.Parse(ISO8601_NANO_FORMAT, audit.CreatedAt)

	if err != nil {
		err = nil
		t, err = time.Parse(ISO8601_MILLI_FORMAT, audit.CreatedAt)
	}

	if err != nil {
		err = nil
		t, err = time.Parse(ISO8601_MICRO_FORMAT, audit.CreatedAt)
	}

	if err != nil {
		err = nil
		t, err = time.Parse(ISO8601_SECOND_FORMAT, audit.CreatedAt)
	}

	if err != nil {
		// Can't return nil for a time.Time, so convention is to return the 0 time
		return time.Time{}, report.NewWarningAlert("invalid format for creation time")
	}

	return t, nil
}

// Validates the audit record & returns error on validation failure
func (audit *Audit) Validate() error {
	if audit.CreatedAt == "" {
		audit.CreatedAt = time.Now().UTC().Format(ISO8601_NANO_FORMAT)
	}

	t, err := audit.parseCreatedAt()

	if err != nil {
		return err
	}

	audit.CreatedAtTime = t
	audit.CreatedAt = audit.CreatedAtTime.UTC().Format(ISO8601_NANO_FORMAT) // Ensuring that the time is always stored in UTC

	if audit.CreatedAtTime.Unix() <= 0 {
		return report.NewWarningAlert("invalid creation time")
	}

	if audit.Expiry <= 0 {
		return report.NewWarningAlert("expiry should be a positive integer")
	}

	audit.ExpiredAtTime = audit.CreatedAtTime.Add(time.Duration(audit.Expiry) * time.Second)
	audit.ExpiredAt = audit.ExpiredAtTime.Format(ISO8601_NANO_FORMAT)

	if len(audit.Uuid) != 36 {
		return report.NewWarningAlert("uuid should be a 36 characters long string")
	}

	if audit.Action == "" {
		return report.NewWarningAlert("action cannot be empty")
	}

	if audit.UserType == "" {
		return report.NewWarningAlert("user_type cannot be empty")
	}

	if audit.UserId == "" {
		return report.NewWarningAlert("user_id cannot be empty")
	}

	if len(audit.Description) > MAX_DESCRIPTION {
		return report.NewWarningAlert("description is too long. Maximum length allowed: " + strconv.Itoa(MAX_DESCRIPTION))
	}

	for _, change := range audit.Changes {
		if change.Attribute == "" {
			return report.NewWarningAlert("attribute cannot be empty for changes")
		}
	}

	return nil
}
