package model

import (
	"fmt"
	"time"

	"github.com/go-errors/errors"

	"github.com/google/uuid"
	"github.com/oapi-codegen/nullable"
	"github.com/phishingclub/phishingclub/data"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/lure"
	"github.com/phishingclub/phishingclub/utils"
	"github.com/phishingclub/phishingclub/validate"
	"github.com/phishingclub/phishingclub/vo"
)

// Campaign is a phishing Campaign entity
type Campaign struct {
	ID                  nullable.Nullable[uuid.UUID]                 `json:"id"`
	CreatedAt           *time.Time                                   `json:"createdAt"`
	UpdatedAt           *time.Time                                   `json:"updatedAt"`
	CloseAt             nullable.Nullable[time.Time]                 `json:"closeAt"`
	ClosedAt            nullable.Nullable[time.Time]                 `json:"closedAt"`
	AnonymizeAt         nullable.Nullable[time.Time]                 `json:"anonymizeAt"`
	AnonymizedAt        nullable.Nullable[time.Time]                 `json:"anonymizedAt"`
	DataAnonymizeAt     nullable.Nullable[time.Time]                 `json:"dataAnonymizeAt"`
	DataAnonymizedAt    nullable.Nullable[time.Time]                 `json:"dataAnonymizedAt"`
	SortField           nullable.Nullable[vo.CampaignSortField]      `json:"sortField"`
	SortOrder           nullable.Nullable[vo.CampaignSendingOrder]   `json:"sortOrder"`
	SendStartAt         nullable.Nullable[time.Time]                 `json:"sendStartAt"`
	SendEndAt           nullable.Nullable[time.Time]                 `json:"sendEndAt"`
	ScheduleAt          nullable.Nullable[time.Time]                 `json:"scheduleAt"`
	ConstraintWeekDays  nullable.Nullable[vo.CampaignWeekDays]       `json:"constraintWeekDays"`
	ConstraintStartTime nullable.Nullable[vo.CampaignTimeConstraint] `json:"constraintStartTime"`
	ConstraintEndTime   nullable.Nullable[vo.CampaignTimeConstraint] `json:"constraintEndTime"`

	// jitter is persisted to the database only when ScheduleAt is set (late-scheduling),
	// so the task runner can apply it when schedule() is called hours later.
	// For immediately-scheduled campaigns jitter lives in memory only and is never written to the DB.
	JitterMin nullable.Nullable[int] `json:"jitterMin,omitempty"`
	JitterMax nullable.Nullable[int] `json:"jitterMax,omitempty"`

	Name nullable.Nullable[vo.String64] `json:"name"`

	SaveSubmittedData   nullable.Nullable[bool] `json:"saveSubmittedData"`
	SaveBrowserMetadata nullable.Nullable[bool] `json:"saveBrowserMetadata"`
	IsAnonymous         nullable.Nullable[bool] `json:"isAnonymous"`
	IsTest              nullable.Nullable[bool] `json:"isTest"`
	Obfuscate           nullable.Nullable[bool] `json:"obfuscate"`
	// deprecated: use Webhooks array instead for multiple webhooks with per-webhook settings
	WebhookIncludeData nullable.Nullable[string]    `json:"webhookIncludeData,omitempty"`
	WebhookEvents      nullable.Nullable[int]       `json:"webhookEvents,omitempty"`
	WebhookID          nullable.Nullable[uuid.UUID] `json:"webhookID,omitempty"`

	TemplateID        nullable.Nullable[uuid.UUID]    `json:"templateID"`
	Template          *CampaignTemplate               `json:"template"`
	CompanyID         nullable.Nullable[uuid.UUID]    `json:"companyID"`
	Company           *Company                        `json:"company"`
	RecipientGroups   []*RecipientGroup               `json:"recipientGroups"`
	RecipientGroupIDs nullable.Nullable[[]*uuid.UUID] `json:"recipientGroupIDs,omitempty"`
	AllowDeny         []*AllowDeny                    `json:"allowDeny"`
	AllowDenyIDs      nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"`
	DenyPageID        nullable.Nullable[uuid.UUID]    `json:"denyPageID,omitempty"`
	DenyPage          *Page                           `json:"denyPage"`
	EvasionPageID     nullable.Nullable[uuid.UUID]    `json:"evasionPageID,omitempty"`
	EvasionPage       *Page                           `json:"evasionPage"`

	// webhooks configuration with per-webhook settings
	Webhooks nullable.Nullable[[]*CampaignWebhook] `json:"webhooks,omitempty"`

	// snapshotted from the campaign template while the campaign holds no
	// recipients. read only, see ToDBMap.
	LureURLMode    nullable.Nullable[string] `json:"lureURLMode"`
	LureCodeAlgo   nullable.Nullable[string] `json:"lureCodeAlgo"`
	LureCodeLength nullable.Nullable[int]    `json:"lureCodeLength"`

	// HasCustomLureCodes decides whether the recipient table shows the code
	// column, which pagination makes impossible to derive from one page.
	HasCustomLureCodes bool `json:"hasCustomLureCodes"`

	// must not be set by a user
	NotableEventID   nullable.Nullable[uuid.UUID] `json:"notableEventID"`
	NotableEventName string                       `json:"notableEventName"`
}

// Validate checks if the campaign has a valid state
const (
	WebhookDataLevelNone  = "none"
	WebhookDataLevelBasic = "basic"
	WebhookDataLevelFull  = "full"
)

// WebhookEventsToBinary converts array of event names to bitwise int
func WebhookEventsToBinary(events []string) int {
	if len(events) == 0 {
		return 0 // 0 means all events
	}
	binary := 0
	for _, event := range events {
		if bit, ok := data.WebhookEventToBit[event]; ok {
			binary |= bit
		}
	}
	return binary
}

// WebhookEventsFromBinary converts bitwise int to array of event names
func WebhookEventsFromBinary(binary int) []string {
	if binary == 0 {
		return []string{} // empty array means all events
	}
	events := []string{}
	for eventName, bit := range data.WebhookEventToBit {
		if binary&bit != 0 {
			events = append(events, eventName)
		}
	}
	return events
}

// IsWebhookEventEnabled checks if an event should trigger webhook
func IsWebhookEventEnabled(webhookEvents int, eventName string) bool {
	// 0 means all events enabled (backward compatible)
	if webhookEvents == 0 {
		return true
	}
	// check if the event bit is set
	if bit, ok := data.WebhookEventToBit[eventName]; ok {
		return webhookEvents&bit != 0
	}
	return false
}

func (c *Campaign) Validate() error {
	if err := validate.NullableFieldRequired("name", c.Name); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("sortField", c.SortField); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("sortOrder", c.SortOrder); err != nil {
		return err
	}
	// if a start or end is set, then end must be equal or after the start
	if c.SendStartAt.IsSpecified() && !c.SendStartAt.IsNull() || (c.SendEndAt.IsSpecified() && !c.SendEndAt.IsNull()) {
		if err := validate.NullableFieldRequired("sendStartAt", c.SendStartAt); err != nil {
			return err
		}
		if err := validate.NullableFieldRequired("sendEndAt", c.SendEndAt); err != nil {
			return err
		}
		if c.SendEndAt.MustGet().Before(c.SendStartAt.MustGet()) {
			return validate.WrapErrorWithField(errors.New("send end time must be after start time"), "sendEndAt")
		}
	}
	if err := validate.NullableFieldRequired("templateID", c.TemplateID); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("RecipientGroupIDs", c.RecipientGroupIDs); err != nil {
		return err
	}
	if len(c.RecipientGroupIDs.MustGet()) == 0 {
		return validate.WrapErrorWithField(errors.New("must have at least one recipient group"), "RecipientGroupIDs")
	}
	if err := c.ValidateEvasionAndDenyPages(); err != nil {
		return err
	}

	// if ConstraintWeekDays or ConstraintStartTime or ConstraintEndTime is set, then this is a 'scheduled type'
	// this requires that all fields are set and that end time is equal or after start time
	if (c.ConstraintWeekDays.IsSpecified() && !c.ConstraintWeekDays.IsNull()) ||
		(c.ConstraintStartTime.IsSpecified() && !c.ConstraintStartTime.IsNull()) ||
		(c.ConstraintEndTime.IsSpecified() && !c.ConstraintEndTime.IsNull()) {
		// check required fields are set
		if err := c.ValidateSendTimesSet(); err != nil {
			return err
		}
		if err := validate.NullableFieldRequired("ConstraintWeekDays", c.ConstraintWeekDays); err != nil {
			return err
		}
		if c.ConstraintWeekDays.MustGet().Count() == 0 {
			return validate.WrapErrorWithField(errors.New("must have at least one day selected"), "ConstraintWeekDays")
		}
		if err := validate.NullableFieldRequired("ConstraintStartTime", c.ConstraintStartTime); err != nil {
			return err
		}
		if err := validate.NullableFieldRequired("ConstraintEndTime", c.ConstraintEndTime); err != nil {
			return err
		}
		// check that times and days are valid
		constraintStartTime := c.ConstraintStartTime.MustGet()
		constraintEndTime := c.ConstraintEndTime.MustGet()
		if constraintStartTime.IsAfter(constraintEndTime) {
			return validate.WrapErrorWithField(errors.New("constraint end time must be after start time"), "ConstraintEndTime")
		}
		if constraintStartTime.IsEqual(constraintEndTime) {
			return validate.WrapErrorWithField(errors.New("constraint end time must be after start time"), "ConstraintEndTime")
		}
		startAt := c.SendStartAt.MustGet()
		endAt := c.SendEndAt.MustGet()
		//  check that selected weekdays are within the start and end date
		isWithin := c.ConstraintWeekDays.MustGet().IsWithin(&startAt, &endAt)
		if !isWithin {
			return validate.WrapErrorWithField(
				fmt.Errorf(
					"constraint week days must be within the start (%s) and end date (%s)",
					startAt.Format("2006-01-02"),
					endAt.Format("2006-01-02"),
				),
				"ConstraintWeekDays",
			)
		}
	}
	// ensure closeAt and anonymize is correctly set after other dates if set
	if c.CloseAt.IsSpecified() && !c.CloseAt.IsNull() {
		closeAt := c.CloseAt.MustGet()
		if v, err := c.SendEndAt.Get(); err == nil {
			if closeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("close at must be after end date"), "CloseAt")
			}
		}
	}
	if c.AnonymizeAt.IsSpecified() && !c.AnonymizeAt.IsNull() {
		anonymizeAt := c.AnonymizeAt.MustGet()
		if v, err := c.CloseAt.Get(); err == nil {
			if anonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize at must be after close date"), "AnonymizeAt")
			}
		}
		if v, err := c.SendEndAt.Get(); err != nil {
			if anonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize at must be after end date"), "AnonymizeAt")
			}
		}
		if v, err := c.SendStartAt.Get(); err == nil {
			if anonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize at must be after start date"), "AnonymizeAt")
			}
		}
	}
	if c.DataAnonymizeAt.IsSpecified() && !c.DataAnonymizeAt.IsNull() {
		dataAnonymizeAt := c.DataAnonymizeAt.MustGet()
		if v, err := c.CloseAt.Get(); err == nil {
			if dataAnonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize submitted data at must be after close date"), "DataAnonymizeAt")
			}
		}
		if v, err := c.SendEndAt.Get(); err == nil {
			if dataAnonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize submitted data at must be after end date"), "DataAnonymizeAt")
			}
		}
		if v, err := c.SendStartAt.Get(); err == nil {
			if dataAnonymizeAt.Before(v) {
				return validate.WrapErrorWithField(errors.New("anonymize submitted data at must be after start date"), "DataAnonymizeAt")
			}
		}
	}
	// validate jitter values if specified
	// negative values are allowed for asymmetric jitter (e.g., -10 to 20 means send 10 min early to 20 min late)
	jitterMinSpecified := c.JitterMin.IsSpecified() && !c.JitterMin.IsNull()
	jitterMaxSpecified := c.JitterMax.IsSpecified() && !c.JitterMax.IsNull()

	// both must be specified together or both unspecified
	if jitterMinSpecified != jitterMaxSpecified {
		return validate.WrapErrorWithField(errors.New("jitter min and max must both be specified or both be unspecified"), "jitter")
	}

	if jitterMinSpecified && jitterMaxSpecified {
		jitterMin := c.JitterMin.MustGet()
		jitterMax := c.JitterMax.MustGet()
		if jitterMax < jitterMin {
			return validate.WrapErrorWithField(errors.New("jitter max must be greater than or equal to jitter min"), "jitterMax")
		}
		// validate jitter doesn't exceed campaign duration
		if c.SendStartAt.IsSpecified() && !c.SendStartAt.IsNull() && c.SendEndAt.IsSpecified() && !c.SendEndAt.IsNull() {
			startAt := c.SendStartAt.MustGet()
			endAt := c.SendEndAt.MustGet()
			campaignDuration := endAt.Sub(startAt)

			// check if max jitter (positive) could push beyond end time
			maxJitterDuration := time.Duration(jitterMax) * time.Minute
			if maxJitterDuration > campaignDuration {
				return validate.WrapErrorWithField(
					errors.New("jitter max cannot exceed campaign duration"),
					"jitterMax",
				)
			}

			// check if min jitter (negative) could push before start time
			minJitterDuration := time.Duration(jitterMin) * time.Minute
			if minJitterDuration < -campaignDuration {
				return validate.WrapErrorWithField(
					errors.New("jitter min cannot exceed campaign duration"),
					"jitterMin",
				)
			}
		}
	}
	// must not be set from api consumers
	if c.NotableEventID.IsSpecified() && !c.NotableEventID.IsNull() {
		c.NotableEventID.SetNull()
	}

	// validate webhookIncludeData enum
	if c.WebhookIncludeData.IsSpecified() {
		if level, err := c.WebhookIncludeData.Get(); err == nil {
			if level != WebhookDataLevelNone &&
				level != WebhookDataLevelBasic &&
				level != WebhookDataLevelFull {
				return validate.WrapErrorWithField(
					errors.New("must be 'none', 'basic', or 'full'"),
					"webhookIncludeData",
				)
			}
		}
	}

	// validate webhookEvents is non-negative
	if c.WebhookEvents.IsSpecified() && !c.WebhookEvents.IsNull() {
		if events, err := c.WebhookEvents.Get(); err == nil {
			if events < 0 {
				return validate.WrapErrorWithField(
					errors.New("must be non-negative"),
					"webhookEvents",
				)
			}
		}
	}

	return nil
}

// ValidateSendTimesSet checks that the send start and end times are set
func (c *Campaign) ValidateSendTimesSet() error {
	if err := validate.NullableFieldRequired("sendStartAt", c.SendStartAt); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("sendEndAt", c.SendEndAt); err != nil {
		return err
	}
	return nil
}

// ValidateScheduledType checks times related to a scheduled type campaign
func (c *Campaign) ValidateScheduledTimes() error {
	if err := c.ValidateSendTimesSet(); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("ConstraintWeekDays", c.ConstraintWeekDays); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("ConstraintStartTime", c.ConstraintStartTime); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("ConstraintEndTime", c.ConstraintEndTime); err != nil {
		return err
	}
	return nil
}

// ValidateNoSendTimesSet checks that the send start and end times are not set
func (c *Campaign) ValidateNoSendTimesSet() error {
	if c.SendStartAt.IsSpecified() && !c.SendStartAt.IsNull() {
		return validate.WrapErrorWithField(errors.New("send start time must not be set"), "sendStartAt")
	}
	if c.SendEndAt.IsSpecified() && !c.SendEndAt.IsNull() {
		return validate.WrapErrorWithField(errors.New("send end time must not be set"), "sendEndAt")
	}
	return nil
}

// ValidateEvasionAndDenyPages checks evasion page and deny page requirements
func (c *Campaign) ValidateEvasionAndDenyPages() error {
	// if evasion page is set, deny page must also be set
	if c.EvasionPageID.IsSpecified() && !c.EvasionPageID.IsNull() {
		if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
			return validate.WrapErrorWithField(errors.New("evasion page requires a deny page to be set"), "evasionPage")
		}
	}

	// if allow/deny IDs are set (IP filtering), deny page must be set
	if c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0 {
		if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
			return validate.WrapErrorWithField(errors.New("IP filtering requires a deny page to be set"), "denyPage")
		}
	}

	return nil
}

// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (c *Campaign) ToDBMap() map[string]any {
	m := map[string]any{}
	if c.Name.IsSpecified() {
		m["name"] = nil
		if v, err := c.Name.Get(); err == nil {
			m["name"] = v.String()
		}
	}
	if c.SortField.IsSpecified() {
		m["sort_field"] = nil
		if v, err := c.SortField.Get(); err == nil {
			m["sort_field"] = v.String()
		}
	}
	if c.SortOrder.IsSpecified() {
		m["sort_order"] = nil
		if v, err := c.SortOrder.Get(); err == nil {
			m["sort_order"] = v.String()
		}
	}
	if c.SendStartAt.IsSpecified() {
		m["send_start_at"] = nil
		if v, err := c.SendStartAt.Get(); err == nil {
			m["send_start_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.ConstraintWeekDays.IsSpecified() {
		m["constraint_week_days"] = nil
		if v, err := c.ConstraintWeekDays.Get(); err == nil {
			m["constraint_week_days"] = v.Int()
		}
	}
	if c.ConstraintStartTime.IsSpecified() {
		m["constraint_start_time"] = nil
		if v, err := c.ConstraintStartTime.Get(); err == nil {
			m["constraint_start_time"] = v.String()
		}
	}
	if c.ConstraintEndTime.IsSpecified() {
		m["constraint_end_time"] = nil
		if v, err := c.ConstraintEndTime.Get(); err == nil {
			m["constraint_end_time"] = v.String()
		}
	}
	if c.SendEndAt.IsSpecified() {
		m["send_end_at"] = nil
		if v, err := c.SendEndAt.Get(); err == nil {
			m["send_end_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.ScheduleAt.IsSpecified() {
		m["schedule_at"] = nil
		if v, err := c.ScheduleAt.Get(); err == nil {
			m["schedule_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.CloseAt.IsSpecified() {
		m["close_at"] = nil
		if v, err := c.CloseAt.Get(); err == nil {
			m["close_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.ClosedAt.IsSpecified() {
		m["closed_at"] = nil
		if v, err := c.ClosedAt.Get(); err == nil {
			m["closed_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.AnonymizeAt.IsSpecified() {
		m["anonymize_at"] = nil
		if v, err := c.AnonymizeAt.Get(); err == nil {
			m["anonymize_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.DataAnonymizeAt.IsSpecified() {
		m["data_anonymize_at"] = nil
		if v, err := c.DataAnonymizeAt.Get(); err == nil {
			m["data_anonymize_at"] = utils.RFC3339UTC(v)
		}
	}
	if c.SaveSubmittedData.IsSpecified() {
		m["save_submitted_data"] = false
		if v, err := c.SaveSubmittedData.Get(); err == nil {
			m["save_submitted_data"] = v
		}
	}
	if c.SaveBrowserMetadata.IsSpecified() {
		m["save_browser_metadata"] = false
		if v, err := c.SaveBrowserMetadata.Get(); err == nil {
			m["save_browser_metadata"] = v
		}
	}
	if c.IsTest.IsSpecified() {
		m["is_test"] = false
		if v, err := c.IsTest.Get(); err == nil {
			m["is_test"] = v
		}
	}
	if c.IsAnonymous.IsSpecified() {
		m["is_anonymous"] = false
		if v, err := c.IsAnonymous.Get(); err == nil {
			m["is_anonymous"] = v
		}
	}
	if c.Obfuscate.IsSpecified() {
		m["obfuscate"] = false
		if v, err := c.Obfuscate.Get(); err == nil {
			m["obfuscate"] = v
		}
	}
	if c.WebhookIncludeData.IsSpecified() {
		m["webhook_include_data"] = WebhookDataLevelFull
		if v, err := c.WebhookIncludeData.Get(); err == nil {
			m["webhook_include_data"] = v
		}
	}
	if c.WebhookEvents.IsSpecified() {
		m["webhook_events"] = 0
		if v, err := c.WebhookEvents.Get(); err == nil {
			m["webhook_events"] = v
		}
	}
	if c.TemplateID.IsSpecified() {
		m["campaign_template_id"] = nil
		if v, err := c.TemplateID.Get(); err == nil {
			m["campaign_template_id"] = v.String()
		}
	}
	if c.CompanyID.IsSpecified() {
		if c.CompanyID.IsNull() {
			m["company_id"] = nil
		} else {
			m["company_id"] = c.CompanyID.MustGet()
		}
	}
	if c.DenyPageID.IsSpecified() {
		m["deny_page_id"] = nil
		if v, err := c.DenyPageID.Get(); err == nil {
			m["deny_page_id"] = v.String()
		}
	}
	if c.EvasionPageID.IsSpecified() {
		m["evasion_page_id"] = nil
		if v, err := c.EvasionPageID.Get(); err == nil {
			m["evasion_page_id"] = v.String()
		}
	}
	if c.WebhookID.IsSpecified() {
		m["webhook_id"] = nil
		if v, err := c.WebhookID.Get(); err == nil {
			m["webhook_id"] = v.String()
		}
	}
	if v, err := c.NotableEventID.Get(); err == nil {
		m["notable_event_id"] = v.String()
	}
	if c.JitterMin.IsSpecified() {
		m["jitter_min"] = nil
		if v, err := c.JitterMin.Get(); err == nil {
			m["jitter_min"] = v
		}
	}
	if c.JitterMax.IsSpecified() {
		m["jitter_max"] = nil
		if v, err := c.JitterMax.Get(); err == nil {
			m["jitter_max"] = v
		}
	}
	// the lure settings are left out. a create or update request carries whatever
	// a caller put in those fields, so the snapshot is written only by
	// SetLureSettingsByID, from the template, at the first schedule.

	return m
}

// LureSettings returns the snapshotted lure URL settings, falling back to the
// defaults for campaigns created before the columns existed.
//
// Each value is rechecked rather than trusted, so anything unusable becomes a
// default here instead of failing the schedule during allocation.
func (c *Campaign) LureSettings() (mode string, algorithm lure.Algorithm, length int) {
	mode = data.LureURLModeQuery
	algorithm = lure.DefaultAlgorithm
	length = lure.DefaultLength
	if v, err := c.LureURLMode.Get(); err == nil && data.IsValidLureURLMode(v) {
		mode = v
	}
	if v, err := c.LureCodeAlgo.Get(); err == nil && lure.IsValidAlgorithm(lure.Algorithm(v)) {
		algorithm = lure.Algorithm(v)
	}
	if v, err := c.LureCodeLength.Get(); err == nil && v >= lure.MinLength && v <= lure.MaxLength {
		length = v
	}
	return mode, algorithm, length
}

// UsesLureCodePath reports whether this campaign's lure URLs carry the recipient
// as a path segment.
func (c *Campaign) UsesLureCodePath() bool {
	mode, _, _ := c.LureSettings()
	return mode == data.LureURLModePath
}

// Close sets the close at timestamp to now
// dont confuse with method Closed
func (c *Campaign) Close() error {
	if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() {
		return errs.ErrCampaignAlreadyClosed
	}
	if c.CloseAt.IsSpecified() && !c.CloseAt.IsNull() {
		return errs.ErrCampaignAlreadySetToClose
	}
	c.CloseAt.Set(time.Now().UTC())
	return nil
}

// Closed sets the closed at timestamp to now
// dont confuse with method Close
func (c *Campaign) Closed() error {
	if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() {
		return errs.ErrCampaignAlreadyClosed
	}
	c.ClosedAt.Set(time.Now().UTC())
	return nil
}

// Anonymize sets the anonymized at timestamp
func (c *Campaign) Anonymize() error {
	if c.AnonymizedAt.IsSpecified() && !c.AnonymizedAt.IsNull() {
		return errs.ErrCampaignAlreadyAnonymized
	}
	c.AnonymizedAt.Set(time.Now().UTC())
	return nil
}

// IsActive returns true if the campaign is active
func (c *Campaign) IsActive() bool {
	now := time.Now()
	if c.ClosedAt.IsSpecified() && !c.ClosedAt.IsNull() && c.ClosedAt.MustGet().Before(now) {
		return false
	}
	return true
}

// IsSelfManaged returns true if the campaign is self managed
func (c *Campaign) IsSelfManaged() bool {
	return c.SendStartAt.IsSpecified() && c.SendStartAt.IsNull() && c.SendEndAt.IsSpecified() && c.SendEndAt.IsNull()
}
