package models

import (
	"time"

	"github.com/google/uuid"
	"golang.org/x/oauth2"
)

type InboxProvider string

const (
	InboxProviderGoogle   InboxProvider = "gmail"
	InboxProviderOutlook  InboxProvider = "outlook"
	InboxProviderSMTPIMAP InboxProvider = "smtp_imap"
)

// Sending-domain authentication states, mirroring the email_accounts.auth_state
// CHECK constraint. "unknown" is deliberately distinct from "failing": it means
// not checked yet or the DNS lookup could not complete, and never gates.
const (
	AuthStateUnknown = "unknown"
	AuthStatePassing = "passing"
	AuthStateFailing = "failing"
)

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

	Name           string `json:"name"`
	SignaturePlain string `json:"signature_plain"`
	SignatureHTML  string `json:"signature_html"`
	SignatureSync  bool   `json:"signature_sync"`
	SignatureCode  bool   `json:"signature_code"`

	Provider string `json:"provider"`
	Status   string `json:"status"`

	LastSyncedAt time.Time `json:"last_synced_at"`
	LastID       *int64    `json:"last_id"`

	CampaignLimit int    `json:"campaign_limit"`
	MinWaitTime   int    `json:"min_wait_time"`
	ReplyTo       string `json:"reply_to"`

	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at"`

	// Sending-domain authentication (SPF/DKIM/DMARC), refreshed by the
	// background auth-check sweep. AuthState is "unknown" until checked (or
	// when a DNS lookup failed transiently), distinct from a real "failing".
	// A sustained "failing" gates cold sending and warmup; see
	// DomainAuthBlocked for when that becomes enforceable.
	AuthState       string     `json:"auth_state"`
	AuthSPF         bool       `json:"auth_spf"`
	AuthDKIM        bool       `json:"auth_dkim"`
	AuthDMARC       bool       `json:"auth_dmarc"`
	AuthDMARCPolicy string     `json:"auth_dmarc_policy,omitempty"`
	AuthReason      string     `json:"auth_reason,omitempty"`
	AuthCheckedAt   *time.Time `json:"auth_checked_at,omitempty"`
	// AuthFailingSince is when the domain entered "failing". The grace window
	// runs from here, so a resolver hiccup or a record broken minutes ago
	// cannot stop sending immediately.
	AuthFailingSince *time.Time `json:"auth_failing_since,omitempty"`

	Warmup          *time.Time `json:"warmup"`
	WarmupPausedAt  *time.Time `json:"warmup_paused_at"`
	WarmupBase      int        `json:"warmup_base"`
	WarmupMax       int        `json:"warmup_max"`
	WarmupIncrease  int        `json:"warmup_increase"`
	WarmupReplyRate int        `json:"warmup_reply_rate"`
	WarmupTag       string     `json:"warmup_tag"`
	WarmupPoolType  string     `json:"warmup_pool_type"`
	WarmupStartTime string     `json:"warmup_start_time"`
	WarmupEndTime   string     `json:"warmup_end_time"`
	WarmupDays      int        `json:"warmup_days"`

	Timezone string `json:"timezone"`

	// SaveToSent applies to SMTP/IMAP mailboxes only: after a send, the worker
	// APPENDs a copy to the mailbox's Sent folder. Gmail and Outlook file their
	// own copy, so the flag is ignored for them.
	SaveToSent bool `json:"save_to_sent"`

	Tags []string `json:"tags"`

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

// IsWarmingActive reports whether the mailbox is actively warming up: warmup
// has been enabled (anchor set) and is not currently paused. The scheduler,
// task runner, and analytics all key off this rather than the raw Warmup
// pointer so a paused mailbox is treated as "not sending normal warmup" while
// still preserving its ramp progress.
func (e *Email) IsWarmingActive() bool {
	return e.Warmup != nil && e.WarmupPausedAt == nil
}

// DomainAuthBlocked reports whether this mailbox's sending domain has been
// failing authentication long enough to stop cold sends and warmup sends.
// Only a sustained "failing" gates: "unknown", an unstamped clock, and
// anything inside the grace window all pass through.
func (e *Email) DomainAuthBlocked(now time.Time, grace time.Duration) bool {
	if e.AuthState != AuthStateFailing || e.AuthFailingSince == nil {
		return false
	}
	return !now.Before(e.AuthFailingSince.Add(grace))
}

// IsWarmupPaused reports whether warmup is enabled but paused. A paused
// mailbox keeps its ramp progress (the anchor is shifted forward on resume).
func (e *Email) IsWarmupPaused() bool {
	return e.Warmup != nil && e.WarmupPausedAt != nil
}

// EmailAuthTarget is a mailbox due for a sending-domain authentication check,
// returned to the background sweep. Auth is a per-domain property, so the sweep
// dedupes these by the domain part of Email before running DNS lookups.
type EmailAuthTarget struct {
	ID    uuid.UUID
	Email string
}

// TrackingDomainTarget is a mailbox with a custom tracking domain that is due
// to be re-resolved. DNS is not static: a domain verified once and never
// re-checked keeps routing links long after its record changed, and a domain
// that failed to verify has no other way back once it propagates.
type TrackingDomainTarget struct {
	ID       uuid.UUID
	Domain   string
	Verified bool
}

// EmailAuthTransition is a mailbox that just entered the failing state, so the
// grace clock started on this pass. The sweep notifies its organization once
// per transition; a domain that stays failing reports nothing on later passes
// because auth_failing_since is preserved, which is the whole dedupe.
type EmailAuthTransition struct {
	ID             uuid.UUID
	Email          string
	OrganizationID *uuid.UUID
}

// Mail connection security modes. TLS is mandatory either way; the difference
// is whether it is negotiated before the protocol greeting or upgraded in-band
// after it. Storing the mode explicitly is what lets a mailbox live on any
// port: inferring it from the port only ever worked for 465/587/993/143.
const (
	// MailSecurityTLS is implicit TLS: the server speaks TLS from the first
	// byte. SMTP 465 (SMTPS), IMAP 993 (IMAPS).
	MailSecurityTLS = "tls"
	// MailSecurityStartTLS is a plaintext greeting upgraded in-band with
	// STARTTLS. SMTP 587/25/2525, IMAP 143.
	MailSecurityStartTLS = "starttls"
)

// ValidMailSecurity reports whether s is a known security mode.
func ValidMailSecurity(s string) bool {
	return s == MailSecurityTLS || s == MailSecurityStartTLS
}

// ResolveSMTPSecurity returns the security mode to dial SMTP with: the stored
// choice when it is set, otherwise the conventional default for the port. The
// fallback keeps mailboxes connected across the rollout, when the stored value
// is empty and events from older workers carry no mode at all.
func ResolveSMTPSecurity(security string, port int) string {
	if ValidMailSecurity(security) {
		return security
	}
	if port == 465 {
		return MailSecurityTLS
	}
	return MailSecurityStartTLS
}

// ResolveIMAPSecurity is ResolveSMTPSecurity for IMAP, where implicit TLS is
// the norm (993) and 143 is the STARTTLS port.
func ResolveIMAPSecurity(security string, port int) string {
	if ValidMailSecurity(security) {
		return security
	}
	if port == 143 {
		return MailSecurityStartTLS
	}
	return MailSecurityTLS
}

type Service struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Host     string `json:"host"`
	Port     int    `json:"port"`
	// Security is the connection mode (see MailSecurity*). Empty means "infer
	// from the port", which is how rows and events written before the field
	// existed behave.
	Security string `json:"security,omitempty"`
}

type Oauth2Service struct {
	Host  string             `json:"host"`
	Port  int                `json:"port"`
	Token oauth2.TokenSource `json:"token"`
}

type SmtpImap struct {
	SMTP *Service `json:"smtp"`
	IMAP *Service `json:"imap"`
}

type Oauth2SmtpImap struct {
	SMTP *Oauth2Service `json:"smtp"`
	IMAP *Oauth2Service `json:"imap"`
}

type NewOauthAccount struct {
	OrganizationID *uuid.UUID
	// Allowance, when set, is enforced again inside the insert transaction
	// under the organization's mailbox lock, so concurrent connects cannot
	// both take the last slot.
	Allowance    *MailboxAllowance
	Provider     InboxProvider
	Name         string
	Email        string
	AccessToken  string
	RefreshToken string
	ExpiresAt    time.Time
}

type NewSMTPIMAPAccount struct {
	OrganizationID *uuid.UUID
	// Allowance: see NewOauthAccount.
	Allowance *MailboxAllowance
	Name      string
	Email     string
	SMTP      *Service
	IMAP      *Service
}

// EmailOnboardingState is stored in Redis for the lifetime of an OAuth round trip.
type EmailOnboardingState struct {
	UserID         string     `json:"user_id"`
	OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
	Provider       string     `json:"provider"`
	Nonce          string     `json:"nonce"`
	// EmailAccountID marks a re-authorization round trip: the finish leg
	// renews this mailbox's tokens instead of connecting a new one.
	EmailAccountID *uuid.UUID `json:"email_account_id,omitempty"`
}

// EmailOnboardingStartResponse is returned from POST /emails/onboarding/oauth/start.
type EmailOnboardingStartResponse struct {
	URL   string `json:"url"`
	State string `json:"state"`
}

type EmailsResult struct {
	Data       []Email    `json:"data"`
	Pagination Pagination `json:"pagination"`
}

// TrackingDomainStatus is the state of a custom open/click tracking domain.
// The backend resolves the record on save and on an explicit verify; Verified
// is true once the customer's subdomain points at this install's tracking host.
//
// Everything below Verified is diagnostic. A bare verified flag left the
// customer with a "Pending DNS" badge and nothing to act on, which is what
// issue #173 was.
type TrackingDomainStatus struct {
	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at"`

	// CNAMETarget is the value to put in the CNAME: this install's tracking
	// host (TRACKING_DOMAIN). Empty means the install has no tracking host, so
	// there is nothing to point at and nothing can verify.
	CNAMETarget string `json:"cname_target"`

	// Status is stable and machine-readable: verified, unset, no_target,
	// not_found, wrong_target, lookup_error, or pending when the value is
	// stored state rather than a fresh lookup.
	Status string `json:"status"`

	// Message explains Status in one sentence and is safe to show as-is.
	Message string `json:"message"`

	// Observed is what DNS actually returned, so a customer can compare it
	// with what they typed.
	Observed string `json:"observed,omitempty"`

	// TrackingHostUnresolvable reports that the record is correct but this
	// install's tracking host has no DNS record of its own, so nothing will be
	// recorded. That is an operator fault, not a customer one.
	TrackingHostUnresolvable bool `json:"tracking_host_unresolvable"`
}

type UpdateEmail struct {
	Name *string `json:"name"`

	SignaturePlain *string `json:"signature_plain"`
	SignatureHTML  *string `json:"signature_html"`
	SignatureSync  *bool   `json:"signature_sync"`
	SignatureCode  *bool   `json:"signature_code"`

	Status *string `json:"status"` // active, inactive, revoked

	CampaignLimit *int    `json:"campaign_limit"`
	MinWaitTime   *int    `json:"min_wait_time"`
	ReplyTo       *string `json:"reply_to"`

	Warmup          *bool   `json:"warmup"`
	WarmupBase      *int    `json:"warmup_base"`
	WarmupMax       *int    `json:"warmup_max"`
	WarmupIncrease  *int    `json:"warmup_increase"`
	WarmupReplyRate *int    `json:"warmup_reply_rate"`
	WarmupTag       *string `json:"warmup_tag"`
	WarmupStartTime *string `json:"warmup_start_time"`
	WarmupEndTime   *string `json:"warmup_end_time"`
	WarmupDays      *int    `json:"warmup_days"`

	// Timezone is the mailbox's own IANA zone, which its sending behaviour and
	// business-hours window are evaluated in. Empty means not configured, so
	// only the campaign's window applies.
	Timezone *string `json:"timezone"`

	// SaveToSent controls the Sent folder copy on SMTP/IMAP mailboxes. Turn it
	// off when the submission server files its own copy, or the folder ends up
	// with two of everything.
	SaveToSent *bool `json:"save_to_sent"`

	Tags []string `json:"tags"`
}

// BulkEmailTags adds and removes tags across many mailboxes in one call (the
// mailboxes list bulk bar). Mailbox ids the caller doesn't own and tag ids
// they haven't defined are ignored rather than erroring, so a stale
// selection can't fail the whole batch.
type BulkEmailTags struct {
	EmailIDs   []string `json:"email_ids" binding:"required,min=1,max=1000"`
	AddTags    []string `json:"add_tags" binding:"max=100"`
	RemoveTags []string `json:"remove_tags" binding:"max=100"`
}

// MailboxBulkRowStatus is the per-row outcome of a bulk SMTP/IMAP connect.
type MailboxBulkRowStatus string

const (
	// MailboxBulkConnected: the mailbox was validated and connected.
	MailboxBulkConnected MailboxBulkRowStatus = "connected"
	// MailboxBulkSkipped: the mailbox was already connected, so re-uploading
	// a file is safe.
	MailboxBulkSkipped MailboxBulkRowStatus = "skipped"
	// MailboxBulkFailed: the row was refused; Code says why.
	MailboxBulkFailed MailboxBulkRowStatus = "failed"
)

// MailboxBulkRow is one row's answer. Row echoes the caller's own row number
// so the dashboard can hand back the failed lines of the file it uploaded.
type MailboxBulkRow struct {
	Row     int                  `json:"row"`
	Email   string               `json:"email"`
	Status  MailboxBulkRowStatus `json:"status"`
	Code    string               `json:"code,omitempty"`
	Message string               `json:"message,omitempty"`
	ID      *uuid.UUID           `json:"id,omitempty"`
}

// MailboxBulkSummary counts the batch.
type MailboxBulkSummary struct {
	Total     int `json:"total"`
	Connected int `json:"connected"`
	Skipped   int `json:"skipped"`
	Failed    int `json:"failed"`
}

// MailboxBulkResult is the answer to POST /emails/onboarding/smtp-imap/bulk.
type MailboxBulkResult struct {
	Data    []MailboxBulkRow   `json:"data"`
	Summary MailboxBulkSummary `json:"summary"`
	// Allowance is the workspace's mailbox allowance after the batch, so the
	// dashboard can say how many more rows will fit without another call.
	Allowance *MailboxAllowance `json:"allowance,omitempty"`
}
