package models

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"time"

	"github.com/google/uuid"
)

// TimeInterval is a sending window within a single day, expressed in minutes
// since local midnight. End is exclusive-ish and must be > Start, <= 1440.
type TimeInterval struct {
	Start int `json:"start"`
	End   int `json:"end"`
}

// ScheduleWindows is a campaign's per-day sending schedule, indexed by
// time.Weekday (0=Sunday .. 6=Saturday). A nil/empty day means "no sending that
// day". When non-empty it is the authoritative schedule and supersedes the
// legacy Days/StartTime/EndTime fields. Persisted as a jsonb array-of-7.
type ScheduleWindows [7][]TimeInterval

// IsEmpty reports whether no day carries any interval (treated as "unset").
func (w ScheduleWindows) IsEmpty() bool {
	for _, day := range w {
		if len(day) > 0 {
			return false
		}
	}
	return true
}

// DaySpan returns the earliest start and latest end across a weekday's
// intervals (ok=false when that day has none). Used for even send distribution.
func (w ScheduleWindows) DaySpan(weekday int) (start, end int, ok bool) {
	if weekday < 0 || weekday > 6 || len(w[weekday]) == 0 {
		return 0, 0, false
	}
	start, end = w[weekday][0].Start, w[weekday][0].End
	for _, iv := range w[weekday][1:] {
		if iv.Start < start {
			start = iv.Start
		}
		if iv.End > end {
			end = iv.End
		}
	}
	return start, end, true
}

// Value implements driver.Valuer — marshals to jsonb, or NULL when empty (so an
// empty schedule reverts to the legacy day/time derivation).
func (w ScheduleWindows) Value() (driver.Value, error) {
	if w.IsEmpty() {
		return nil, nil
	}
	return json.Marshal(w)
}

// Scan implements sql.Scanner — reads the jsonb column (NULL → empty).
func (w *ScheduleWindows) Scan(src any) error {
	if src == nil {
		*w = ScheduleWindows{}
		return nil
	}
	var b []byte
	switch v := src.(type) {
	case []byte:
		b = v
	case string:
		b = []byte(v)
	default:
		return fmt.Errorf("ScheduleWindows: unsupported scan type %T", src)
	}
	if len(b) == 0 {
		*w = ScheduleWindows{}
		return nil
	}
	var parsed ScheduleWindows
	if err := json.Unmarshal(b, &parsed); err != nil {
		return err
	}
	*w = parsed
	return nil
}

// Campaign kinds. A sequence is the multi-step default; a one-time email is
// one message to an audience with no follow-ups. Both send through the same
// pacer and caps; the kind is fixed at creation.
const (
	CampaignKindSequence = "sequence"
	CampaignKindOneTime  = "one_time"
)

// ValidCampaignKind reports whether k is a known campaign kind.
func ValidCampaignKind(k string) bool {
	return k == CampaignKindSequence || k == CampaignKindOneTime
}

type Campaign struct {
	ID             uuid.UUID  `json:"id"`
	UserID         string     `json:"user_id"`
	OrganizationID *uuid.UUID `json:"organization_id,omitempty"`

	Name        string `json:"name"`
	Description string `json:"description"`
	Status      string `json:"status"`
	Kind        string `json:"kind"`

	StopOnReply       bool `json:"stop_on_reply"`
	OpenTracking      bool `json:"open_tracking"`
	LinkTracking      bool `json:"link_tracking"`
	TextOnly          bool `json:"text_only"`
	DailyLimit        int  `json:"daily_limit"`
	UnsubscribeHeader bool `json:"unsubscribe_header"`
	RiskyEmails       bool `json:"risky_emails"`
	// UnsubscribeMode is the in-body opt-out: inherit | text | link | off.
	UnsubscribeMode string `json:"unsubscribe_mode"`

	CC  []string `json:"cc"`
	BCC []string `json:"bcc"`

	StartDate *time.Time `json:"start_date"`
	EndDate   *time.Time `json:"end_date"`
	Timezone  string     `json:"timezone"`
	Days      uint8      `json:"days"`
	StartTime string     `json:"start_time"`
	EndTime   string     `json:"end_time"`

	// ScheduleWindows, when non-empty, is the authoritative per-day sending
	// schedule (supersedes Days/StartTime/EndTime). Indexed by time.Weekday.
	ScheduleWindows ScheduleWindows `json:"schedule_windows"`

	EmailTags []string `json:"email_tags"`
	Folders   []string `json:"folders"`

	ContactOrderBy    string  `json:"contact_order_by"`
	ContactOrderDir   string  `json:"contact_order_dir"`
	ContactOrderField *string `json:"contact_order_field,omitempty"`

	// Sending-account selection. SenderStrategy is "tags" (default — accounts
	// resolved from EmailTags) or "explicit" (the campaign_senders list).
	// RotationMode picks how volume spreads across the chosen mailboxes.
	SenderStrategy string           `json:"sender_strategy"`
	RotationMode   string           `json:"rotation_mode"`
	Senders        []CampaignSender `json:"senders,omitempty"` // loaded on demand, not in the base SELECT

	// Per-campaign daily ramp-up. Applied only via min() against the per-mailbox
	// cap, so it can never raise volume above the cold cap. RampLevel/RampLevelDate
	// are server-managed (persisted across pause/resume).
	RampEnabled   bool       `json:"ramp_enabled"`
	RampStart     int        `json:"ramp_start"`
	RampIncrement int        `json:"ramp_increment"`
	RampCeiling   int        `json:"ramp_ceiling"`
	RampLevel     int        `json:"ramp_level"`
	RampLevelDate *time.Time `json:"ramp_level_date,omitempty"`

	// ESP/provider matching: off | prefer | strict.
	ESPMatchMode string `json:"esp_match_mode"`

	// New-lead throttle. MaxNewLeadsPerDay 0 = unlimited (current behavior).
	MaxNewLeadsPerDay  int  `json:"max_new_leads_per_day"`
	PrioritizeNewLeads bool `json:"prioritize_new_leads"`

	// Auto-pause guardrails. Rates are evaluated over a rolling window and the
	// campaign is paused the moment a band is breached, rather than waiting for
	// a mailbox provider to react first. A rate threshold of 0 disables that
	// rule; GuardrailMinSample keeps a small sample from tripping anything.
	//
	// Bounce and complaint rates are CEILINGS (pause when at or above); the
	// reply rate is a FLOOR (pause when below), because a campaign at volume
	// that nobody answers is spending sender reputation for nothing.
	GuardrailEnabled          bool       `json:"guardrail_enabled"`
	GuardrailBounceRateMax    float64    `json:"guardrail_bounce_rate_max"`
	GuardrailComplaintRateMax float64    `json:"guardrail_complaint_rate_max"`
	GuardrailReplyRateMin     float64    `json:"guardrail_reply_rate_min"`
	GuardrailMinSample        int        `json:"guardrail_min_sample"`
	GuardrailWindowDays       int        `json:"guardrail_window_days"`
	GuardrailTrippedAt        *time.Time `json:"guardrail_tripped_at,omitempty"`
	GuardrailReason           string     `json:"guardrail_reason,omitempty"`

	// Campaign-scoped tracking-domain override. Honored only when verified;
	// otherwise falls back to the mailbox/default domain.
	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at,omitempty"`

	// Automatic UTM tagging of every link in the email body. Empty source,
	// medium and campaign values mean the defaults ("warmbly", "email", the
	// campaign name); utm_content is always the link's own text.
	UTMTracking bool   `json:"utm_tracking"`
	UTMSource   string `json:"utm_source"`
	UTMMedium   string `json:"utm_medium"`
	UTMCampaign string `json:"utm_campaign"`

	LastStatusChangeAt *time.Time `json:"last_status_change_at,omitempty"`

	UpdatedAt time.Time `json:"updated_at"`
	CreatedAt time.Time `json:"created_at"`
}

// CampaignSender is one mailbox in an explicit-strategy campaign's sender pool.
type CampaignSender struct {
	EmailAccountID uuid.UUID  `json:"email_account_id"`
	Weight         int        `json:"weight"`
	LastSentAt     *time.Time `json:"last_sent_at,omitempty"`
	Enabled        bool       `json:"enabled"`
}

// CampaignSenderInput is the write shape for PUT /campaigns/:id/senders.
type CampaignSenderInput struct {
	EmailAccountID uuid.UUID `json:"email_account_id"`
	Weight         *int      `json:"weight,omitempty"`
	Enabled        *bool     `json:"enabled,omitempty"`
}

type MiniCampaign struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type CampaignsResult struct {
	Data       []Campaign `json:"data"`
	Pagination Pagination `json:"pagination"`
}

// CampaignsOverview backs the campaigns browser sidebar: status-bucket
// counts plus per-folder totals for the org. Paused sums every paused_*
// variant.
type CampaignsOverview struct {
	Total     int64                 `json:"total"`
	Active    int64                 `json:"active"`
	Paused    int64                 `json:"paused"`
	Draft     int64                 `json:"draft"`
	Completed int64                 `json:"completed"`
	OneTime   int64                 `json:"one_time"`
	Folders   []CampaignFolderCount `json:"folders"`
}

// CampaignEstimate is the request for POST /campaigns-estimate: how many
// contacts a set of segments resolves to and how long a sender pool needs
// to reach them under the per-mailbox caps. Nothing is written.
type CampaignEstimate struct {
	SegmentIDs  []string   `json:"segment_ids"`
	EmailTagIDs []string   `json:"email_tag_ids,omitempty"`
	DailyLimit  *int       `json:"daily_limit,omitempty"`
	Days        *uint8     `json:"days,omitempty"`
	Timezone    *string    `json:"timezone,omitempty"`
	StartDate   *time.Time `json:"start_date,omitempty"`
}

// CampaignEstimateResult is the projection. DailyCapacity is the pool's
// per-day ceiling under the campaign limit; RemainingToday subtracts what the
// mailboxes already sent today. SendingDays is how many sending days the
// audience needs and EstimatedFinishAt the calendar day the last send lands
// on, both nil when the pool has no capacity.
type CampaignEstimateResult struct {
	Recipients        int        `json:"recipients"`
	Mailboxes         int        `json:"mailboxes"`
	DailyCapacity     int        `json:"daily_capacity"`
	RemainingToday    int        `json:"remaining_today"`
	SendingDays       *int       `json:"sending_days"`
	EstimatedFinishAt *time.Time `json:"estimated_finish_at"`
}

type CampaignFolderCount struct {
	FolderID uuid.UUID `json:"folder_id"`
	Total    int64     `json:"total"`
}

type UpdateCampaign struct {
	Name        *string `json:"name"`
	Description *string `json:"description"`
	Status      *string `json:"status,omitempty"`

	StopOnReply       *bool   `json:"stop_on_reply"`
	OpenTracking      *bool   `json:"open_tracking"`
	LinkTracking      *bool   `json:"link_tracking"`
	TextOnly          *bool   `json:"text_only"`
	DailyLimit        *int    `json:"daily_limit"`
	UnsubscribeHeader *bool   `json:"unsubscribe_header"`
	RiskyEmails       *bool   `json:"risky_emails"`
	UnsubscribeMode   *string `json:"unsubscribe_mode"`

	CC  []string `json:"cc"`
	BCC []string `json:"bcc"`

	// Absent leaves the stored date untouched; an explicit null clears it
	// ("start now" / "no end date"), matching the validation error's promise.
	StartDate NullableTime `json:"start_date"`
	EndDate   NullableTime `json:"end_date"`
	Timezone  *string      `json:"timezone"`
	Days      *uint8       `json:"days"`
	StartTime *string      `json:"start_time"`
	EndTime   *string      `json:"end_time"`

	// Authoritative per-day schedule. When sent, supersedes Days/StartTime/EndTime.
	ScheduleWindows *ScheduleWindows `json:"schedule_windows,omitempty"`

	EmailTags []string `json:"email_tags"`
	Folders   []string `json:"folders"`

	ContactOrderBy    *string `json:"contact_order_by"`
	ContactOrderDir   *string `json:"contact_order_dir"`
	ContactOrderField *string `json:"contact_order_field"`

	// Net-new send controls. The explicit sender LIST is edited via
	// PUT /campaigns/:id/senders; only the strategy/mode toggles ride PATCH.
	SenderStrategy *string `json:"sender_strategy,omitempty"`
	RotationMode   *string `json:"rotation_mode,omitempty"`

	RampEnabled   *bool `json:"ramp_enabled,omitempty"`
	RampStart     *int  `json:"ramp_start,omitempty"`
	RampIncrement *int  `json:"ramp_increment,omitempty"`
	RampCeiling   *int  `json:"ramp_ceiling,omitempty"`

	ESPMatchMode       *string `json:"esp_match_mode,omitempty"`
	MaxNewLeadsPerDay  *int    `json:"max_new_leads_per_day,omitempty"`
	PrioritizeNewLeads *bool   `json:"prioritize_new_leads,omitempty"`
	TrackingDomain     *string `json:"tracking_domain,omitempty"`

	UTMTracking *bool   `json:"utm_tracking,omitempty"`
	UTMSource   *string `json:"utm_source,omitempty"`
	UTMMedium   *string `json:"utm_medium,omitempty"`
	UTMCampaign *string `json:"utm_campaign,omitempty"`

	// Auto-pause guardrails. GuardrailTrippedAt/Reason are server-owned and
	// are cleared when the campaign is started again, so they are not settable
	// here.
	GuardrailEnabled          *bool    `json:"guardrail_enabled,omitempty"`
	GuardrailBounceRateMax    *float64 `json:"guardrail_bounce_rate_max,omitempty"`
	GuardrailComplaintRateMax *float64 `json:"guardrail_complaint_rate_max,omitempty"`
	GuardrailReplyRateMin     *float64 `json:"guardrail_reply_rate_min,omitempty"`
	GuardrailMinSample        *int     `json:"guardrail_min_sample,omitempty"`
	GuardrailWindowDays       *int     `json:"guardrail_window_days,omitempty"`
}

// TouchesSchedule reports whether the patch changes anything the campaign's
// next wakeup time is computed from.
func (u *UpdateCampaign) TouchesSchedule() bool {
	return u.StartDate.Set || u.EndDate.Set || u.Timezone != nil || u.Days != nil ||
		u.StartTime != nil || u.EndTime != nil || u.ScheduleWindows != nil
}

// CreateCampaign is the payload accepted by POST /campaigns. Name is required;
// every other field is optional and only applied if the caller sent a non-nil
// value. The wizard sends everything at once; the simple modal can still send
// just {name, description} and get sane defaults.
type CreateCampaign struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	// Kind defaults to "sequence". A "one_time" campaign accepts a single
	// email step here and refuses further ones later.
	Kind *string `json:"kind,omitempty"`

	// Sending rules / tracking
	StopOnReply       *bool   `json:"stop_on_reply,omitempty"`
	OpenTracking      *bool   `json:"open_tracking,omitempty"`
	LinkTracking      *bool   `json:"link_tracking,omitempty"`
	TextOnly          *bool   `json:"text_only,omitempty"`
	DailyLimit        *int    `json:"daily_limit,omitempty"`
	UnsubscribeHeader *bool   `json:"unsubscribe_header,omitempty"`
	RiskyEmails       *bool   `json:"risky_emails,omitempty"`
	UnsubscribeMode   *string `json:"unsubscribe_mode,omitempty"`

	CC  []string `json:"cc,omitempty"`
	BCC []string `json:"bcc,omitempty"`

	// Schedule
	StartDate *time.Time `json:"start_date,omitempty"`
	EndDate   *time.Time `json:"end_date,omitempty"`
	Timezone  *string    `json:"timezone,omitempty"`
	Days      *uint8     `json:"days,omitempty"`
	StartTime *string    `json:"start_time,omitempty"`
	EndTime   *string    `json:"end_time,omitempty"`

	// Authoritative per-day schedule. When sent, supersedes Days/StartTime/EndTime.
	ScheduleWindows *ScheduleWindows `json:"schedule_windows,omitempty"`

	// Sender pool — accepts UUIDs already created by the user.
	EmailTagIDs []string `json:"email_tag_ids,omitempty"`
	FolderIDs   []string `json:"folder_ids,omitempty"`

	// Sending-account selection + rotation (net-new). When sender_strategy is
	// "explicit", Senders is the mailbox pool; otherwise EmailTagIDs are used.
	SenderStrategy *string               `json:"sender_strategy,omitempty"`
	RotationMode   *string               `json:"rotation_mode,omitempty"`
	Senders        []CampaignSenderInput `json:"senders,omitempty"`

	// Per-campaign daily ramp-up (net-new). ramp_level is server-owned.
	RampEnabled   *bool `json:"ramp_enabled,omitempty"`
	RampStart     *int  `json:"ramp_start,omitempty"`
	RampIncrement *int  `json:"ramp_increment,omitempty"`
	RampCeiling   *int  `json:"ramp_ceiling,omitempty"`

	// ESP/provider matching + new-lead throttle + tracking-domain override.
	ESPMatchMode       *string `json:"esp_match_mode,omitempty"`
	MaxNewLeadsPerDay  *int    `json:"max_new_leads_per_day,omitempty"`
	PrioritizeNewLeads *bool   `json:"prioritize_new_leads,omitempty"`
	TrackingDomain     *string `json:"tracking_domain,omitempty"`

	// Automatic UTM tagging (off unless sent). Empty values keep the defaults.
	UTMTracking *bool   `json:"utm_tracking,omitempty"`
	UTMSource   *string `json:"utm_source,omitempty"`
	UTMMedium   *string `json:"utm_medium,omitempty"`
	UTMCampaign *string `json:"utm_campaign,omitempty"`

	// Initial sequences (in order) — caller can also create them after.
	Sequences []CreateSequenceInput `json:"steps,omitempty"`

	// A/B variants for the first sequence — useful for "create + test" in one shot.
	Variants []CreateCampaignABVariantRequest `json:"variants,omitempty"`

	// Advanced overrides (bounce/intent/dashboard/etc) — see AdvancedOutreachSettings.
	AdvancedOverrides *AdvancedOutreachSettings `json:"advanced_overrides,omitempty"`
}

// CreateSequenceInput is one step in a sequence. Used during initial campaign
// creation; matches UpdateSequence shape so the wizard can reuse the editor.
type CreateSequenceInput struct {
	Name      string `json:"name"`
	Subject   string `json:"subject"`
	BodyPlain string `json:"body_plain"`
	BodyHTML  string `json:"body_html"`
	BodySync  *bool  `json:"body_sync,omitempty"`
	BodyCode  *bool  `json:"body_code,omitempty"`
	WaitAfter *int   `json:"wait_after,omitempty"`
}

// StartCampaignOptions qualifies a start request.
type StartCampaignOptions struct {
	// AcknowledgeListRisk launches past the bounce-risk gate: the member has
	// read the projection and takes the risk (the list may have been verified
	// elsewhere).
	AcknowledgeListRisk bool `json:"acknowledge_list_risk"`
	// Automatic marks a start the platform initiated (a resume after
	// verification), which skips the member-facing cooldown.
	Automatic bool `json:"-"`
}
