package models

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/utils"
)

// Segment is a saved, reusable audience: a list of conditions over contacts
// plus per-contact manual overrides. Membership is evaluated at read time.
type Segment struct {
	ID             uuid.UUID          `json:"id"`
	OrganizationID uuid.UUID          `json:"organization_id"`
	CreatedBy      *uuid.UUID         `json:"created_by,omitempty"`
	Name           string             `json:"name"`
	Description    string             `json:"description"`
	Color          string             `json:"color"`
	Match          SegmentMatch       `json:"match"`
	Conditions     []SegmentCondition `json:"conditions"`

	// ContactCount is the live membership size; IncludedCount and
	// ExcludedCount are the manual overrides. Populated by reads.
	ContactCount  int `json:"contact_count"`
	IncludedCount int `json:"included_count"`
	ExcludedCount int `json:"excluded_count"`

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

// SegmentMatch says whether every condition or any condition must hold.
type SegmentMatch string

const (
	SegmentMatchAll SegmentMatch = "all"
	SegmentMatchAny SegmentMatch = "any"
)

// SegmentMemberMode is a manual override on one contact.
type SegmentMemberMode string

const (
	SegmentMemberInclude SegmentMemberMode = "include"
	SegmentMemberExclude SegmentMemberMode = "exclude"
	// SegmentMemberAuto clears the override so the conditions decide again.
	SegmentMemberAuto SegmentMemberMode = "auto"
)

// SegmentCondition is one predicate. Field picks the column or derived value,
// Operator the comparison; scalar operators read Value, list operators read
// Values. Custom fields are addressed as "custom.<key>".
type SegmentCondition struct {
	Field    string   `json:"field"`
	Operator string   `json:"operator"`
	Value    string   `json:"value,omitempty"`
	Values   []string `json:"values,omitempty"`
}

// SegmentFieldKind groups fields by the operators they accept.
type SegmentFieldKind string

const (
	SegmentFieldText     SegmentFieldKind = "text"
	SegmentFieldEnum     SegmentFieldKind = "enum"
	SegmentFieldBool     SegmentFieldKind = "bool"
	SegmentFieldDate     SegmentFieldKind = "date"
	SegmentFieldNumber   SegmentFieldKind = "number"
	SegmentFieldCategory SegmentFieldKind = "category"
	SegmentFieldCampaign SegmentFieldKind = "campaign"
	SegmentFieldSegment  SegmentFieldKind = "segment"
)

// Segment condition operators.
const (
	SegOpEquals        = "equals"
	SegOpNotEquals     = "not_equals"
	SegOpContains      = "contains"
	SegOpNotContains   = "not_contains"
	SegOpStartsWith    = "starts_with"
	SegOpEndsWith      = "ends_with"
	SegOpIsEmpty       = "is_empty"
	SegOpIsNotEmpty    = "is_not_empty"
	SegOpIn            = "in"
	SegOpNotIn         = "not_in"
	SegOpIsTrue        = "is_true"
	SegOpIsFalse       = "is_false"
	SegOpBefore        = "before"
	SegOpAfter         = "after"
	SegOpWithinDays    = "within_days"
	SegOpNotWithinDays = "not_within_days"
	SegOpGT            = "gt"
	SegOpGTE           = "gte"
	SegOpLT            = "lt"
	SegOpLTE           = "lte"
)

// SegmentFieldSpec describes one filterable field for validation and for the
// dashboard's condition builder (GET /segments/fields).
type SegmentFieldSpec struct {
	Field string           `json:"field"`
	Label string           `json:"label"`
	Group string           `json:"group"`
	Kind  SegmentFieldKind `json:"kind"`
	// Options lists the accepted values of an enum field.
	Options []string `json:"options,omitempty"`
}

// SegmentFieldCatalog is every non-custom field a condition may name.
var SegmentFieldCatalog = []SegmentFieldSpec{
	{Field: "first_name", Label: "First name", Group: "Contact", Kind: SegmentFieldText},
	{Field: "last_name", Label: "Last name", Group: "Contact", Kind: SegmentFieldText},
	{Field: "email", Label: "Email", Group: "Contact", Kind: SegmentFieldText},
	{Field: "email_domain", Label: "Email domain", Group: "Contact", Kind: SegmentFieldText},
	{Field: "phone", Label: "Phone", Group: "Contact", Kind: SegmentFieldText},
	{Field: "subscribed", Label: "Subscribed", Group: "Contact", Kind: SegmentFieldBool},
	{Field: "suppressed", Label: "On the suppression list", Group: "Contact", Kind: SegmentFieldBool},
	{Field: "source", Label: "Source", Group: "Contact", Kind: SegmentFieldEnum, Options: []string{"unknown", "manual", "campaign", "import", "sheet_sync", "api", "ai_assistant", "form"}},
	{Field: "verification_status", Label: "Verification status", Group: "Contact", Kind: SegmentFieldEnum, Options: []string{"valid", "risky", "invalid", "unknown"}},
	{Field: "is_catch_all", Label: "Catch-all domain", Group: "Contact", Kind: SegmentFieldBool},
	{Field: "esp_provider", Label: "Email provider", Group: "Contact", Kind: SegmentFieldEnum, Options: []string{"gmail", "outlook", "other"}},
	{Field: "created_at", Label: "Created", Group: "Contact", Kind: SegmentFieldDate},
	{Field: "updated_at", Label: "Updated", Group: "Contact", Kind: SegmentFieldDate},
	{Field: "category", Label: "Category", Group: "Contact", Kind: SegmentFieldCategory},

	{Field: "company", Label: "Company name", Group: "Company", Kind: SegmentFieldText},

	{Field: "campaign", Label: "In campaign", Group: "Campaign activity", Kind: SegmentFieldCampaign},
	{Field: "campaign_count", Label: "Number of campaigns", Group: "Campaign activity", Kind: SegmentFieldNumber},
	{Field: "emails_sent", Label: "Emails sent", Group: "Email engagement", Kind: SegmentFieldNumber},
	{Field: "emails_opened", Label: "Emails opened", Group: "Email engagement", Kind: SegmentFieldNumber},
	{Field: "emails_clicked", Label: "Links clicked", Group: "Email engagement", Kind: SegmentFieldNumber},
	{Field: "emails_replied", Label: "Replies", Group: "Email engagement", Kind: SegmentFieldNumber},
	{Field: "emails_bounced", Label: "Bounces", Group: "Email engagement", Kind: SegmentFieldNumber},
	{Field: "last_sent_at", Label: "Last email sent", Group: "Email engagement", Kind: SegmentFieldDate},
	{Field: "last_opened_at", Label: "Last open", Group: "Email engagement", Kind: SegmentFieldDate},
	{Field: "last_clicked_at", Label: "Last click", Group: "Email engagement", Kind: SegmentFieldDate},
	{Field: "last_replied_at", Label: "Last reply", Group: "Email engagement", Kind: SegmentFieldDate},

	{Field: "segment", Label: "In segment", Group: "Segments", Kind: SegmentFieldSegment},
}

// SegmentCustomFieldPrefix addresses a contact custom field: "custom.industry".
const SegmentCustomFieldPrefix = "custom."

// Segment validation limits.
const (
	SegmentMaxConditions  = 50
	SegmentMaxListValues  = 200
	SegmentMaxNameLen     = 120
	SegmentMaxDescLen     = 1000
	SegmentMaxValueLen    = 500
	SegmentMaxNestingDeep = 5
	SegmentsPerOrgMax     = 200
)

var segmentColorRe = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)

// SegmentFieldSpecFor resolves a condition's field to its spec. Custom fields
// resolve to a synthetic text spec.
func SegmentFieldSpecFor(field string) (SegmentFieldSpec, bool) {
	if strings.HasPrefix(field, SegmentCustomFieldPrefix) {
		key := utils.NormalizeJSONKey(strings.TrimPrefix(field, SegmentCustomFieldPrefix))
		if key == "" || !utils.IsValidJSONKey(key) {
			return SegmentFieldSpec{}, false
		}
		return SegmentFieldSpec{Field: SegmentCustomFieldPrefix + key, Label: key, Group: "Custom field", Kind: SegmentFieldText}, true
	}
	for _, s := range SegmentFieldCatalog {
		if s.Field == field {
			return s, true
		}
	}
	return SegmentFieldSpec{}, false
}

// OperatorsForKind lists the operators a field kind accepts.
func OperatorsForKind(kind SegmentFieldKind) []string {
	switch kind {
	case SegmentFieldText:
		return []string{SegOpEquals, SegOpNotEquals, SegOpContains, SegOpNotContains, SegOpStartsWith, SegOpEndsWith, SegOpIsEmpty, SegOpIsNotEmpty}
	case SegmentFieldEnum:
		return []string{SegOpIn, SegOpNotIn}
	case SegmentFieldBool:
		return []string{SegOpIsTrue, SegOpIsFalse}
	case SegmentFieldDate:
		return []string{SegOpBefore, SegOpAfter, SegOpWithinDays, SegOpNotWithinDays, SegOpIsEmpty, SegOpIsNotEmpty}
	case SegmentFieldNumber:
		return []string{SegOpEquals, SegOpNotEquals, SegOpGT, SegOpGTE, SegOpLT, SegOpLTE}
	case SegmentFieldCategory, SegmentFieldCampaign:
		return []string{SegOpIn, SegOpNotIn, SegOpIsEmpty, SegOpIsNotEmpty}
	case SegmentFieldSegment:
		return []string{SegOpIn, SegOpNotIn}
	}
	return nil
}

// SegmentWrite is the create/update body.
type SegmentWrite struct {
	Name        *string             `json:"name,omitempty"`
	Description *string             `json:"description,omitempty"`
	Color       *string             `json:"color,omitempty"`
	Match       *SegmentMatch       `json:"match,omitempty"`
	Conditions  *[]SegmentCondition `json:"conditions,omitempty"`
}

// SegmentPreview is the body of POST /segments/preview: an unsaved definition
// to count. ID, when set, keeps that segment's manual overrides in the count.
type SegmentPreview struct {
	ID         *uuid.UUID         `json:"id,omitempty"`
	Match      SegmentMatch       `json:"match"`
	Conditions []SegmentCondition `json:"conditions"`
}

// SegmentMembersWrite sets a manual override on a batch of contacts.
type SegmentMembersWrite struct {
	Contacts []string          `json:"contacts"`
	Mode     SegmentMemberMode `json:"mode"`
}

// SegmentAddToCampaign enrols the segment's current members as leads.
type SegmentAddToCampaign struct {
	CampaignID string `json:"campaign_id"`
}

// SegmentAddToCampaignResult reports how many leads were actually new.
type SegmentAddToCampaignResult struct {
	CampaignID uuid.UUID `json:"campaign_id"`
	Added      int       `json:"added"`
	Members    int       `json:"members"`
	// Status is the campaign's status at enrol time, for the wake/restart decision.
	Status string `json:"-"`
}

// CampaignSegmentsMax bounds how many segments one campaign can link.
const CampaignSegmentsMax = 20

// CampaignSegmentsWrite replaces a campaign's linked segments.
type CampaignSegmentsWrite struct {
	SegmentIDs []string `json:"segment_ids"`
}

// CampaignSegmentLink is one segment linked to a campaign, for the Leads tab;
// the counts are live: members now, members that are leads, members held out.
type CampaignSegmentLink struct {
	SegmentID    uuid.UUID `json:"segment_id"`
	Name         string    `json:"name"`
	Color        string    `json:"color"`
	Description  string    `json:"description"`
	ContactCount int       `json:"contact_count"`
	LeadCount    int       `json:"lead_count"`
	HeldOutCount int       `json:"held_out_count"`
	LinkedAt     time.Time `json:"linked_at"`
}

// LinkedCampaign is one campaign that has segments attached, for the sync
// sweep and the targeted per-segment syncs.
type LinkedCampaign struct {
	CampaignID     uuid.UUID
	OrganizationID uuid.UUID
	Status         string
}

// ContactSegment is one segment a contact belongs to, with its override.
type ContactSegment struct {
	ID    uuid.UUID `json:"id"`
	Name  string    `json:"name"`
	Color string    `json:"color"`
	// Mode is "include" or "exclude" when the contact carries a manual
	// override, empty when the conditions alone decide.
	Mode SegmentMemberMode `json:"mode,omitempty"`
	// Member is whether the contact is currently in the segment.
	Member bool `json:"member"`
}

// SegmentOverride is one manually included or excluded contact.
type SegmentOverride struct {
	ContactID uuid.UUID         `json:"contact_id"`
	FirstName string            `json:"first_name"`
	LastName  string            `json:"last_name"`
	Email     string            `json:"email"`
	Company   string            `json:"company"`
	Mode      SegmentMemberMode `json:"mode"`
	CreatedAt time.Time         `json:"created_at"`
}

// SegmentOverridesMax bounds one overrides listing.
const SegmentOverridesMax = 500

// ValidateSegmentName trims and bounds the name.
func ValidateSegmentName(name string) (string, *errx.Error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return "", errx.New(errx.BadRequest, "segment name is required")
	}
	if len(name) > SegmentMaxNameLen {
		return "", errx.New(errx.BadRequest, fmt.Sprintf("segment name must be at most %d characters", SegmentMaxNameLen))
	}
	return name, nil
}

// ValidateSegmentColor accepts a #rrggbb color.
func ValidateSegmentColor(color string) (string, *errx.Error) {
	color = strings.ToLower(strings.TrimSpace(color))
	if !segmentColorRe.MatchString(color) {
		return "", errx.New(errx.BadRequest, "color must be a #rrggbb value")
	}
	return color, nil
}

// ValidateSegmentMatch accepts all|any.
func ValidateSegmentMatch(m SegmentMatch) *errx.Error {
	if m != SegmentMatchAll && m != SegmentMatchAny {
		return errx.New(errx.BadRequest, "match must be all or any")
	}
	return nil
}

// ValidateSegmentConditions normalizes every condition in place and rejects
// anything the SQL builder would not know how to compile. selfID, when set,
// refuses a segment that references itself.
func ValidateSegmentConditions(conds []SegmentCondition, selfID *uuid.UUID) *errx.Error {
	if len(conds) > SegmentMaxConditions {
		return errx.New(errx.BadRequest, fmt.Sprintf("at most %d conditions per segment", SegmentMaxConditions))
	}
	for i := range conds {
		c := &conds[i]
		c.Field = strings.TrimSpace(c.Field)
		c.Operator = strings.TrimSpace(c.Operator)
		spec, ok := SegmentFieldSpecFor(c.Field)
		if !ok {
			return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: unknown field %q", i+1, c.Field))
		}
		c.Field = spec.Field
		if !containsString(OperatorsForKind(spec.Kind), c.Operator) {
			return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: operator %q is not valid for %s", i+1, c.Operator, spec.Label))
		}
		if len(c.Value) > SegmentMaxValueLen {
			return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: value too long", i+1))
		}
		if len(c.Values) > SegmentMaxListValues {
			return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: at most %d values", i+1, SegmentMaxListValues))
		}
		switch c.Operator {
		case SegOpIsEmpty, SegOpIsNotEmpty, SegOpIsTrue, SegOpIsFalse:
			c.Value, c.Values = "", nil
			continue
		case SegOpIn, SegOpNotIn:
			c.Value = ""
			if len(c.Values) == 0 {
				return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: pick at least one value", i+1))
			}
		default:
			c.Values = nil
			if strings.TrimSpace(c.Value) == "" {
				return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: a value is required", i+1))
			}
		}
		switch spec.Kind {
		case SegmentFieldEnum:
			for _, v := range c.Values {
				if !containsString(spec.Options, v) {
					return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: %q is not a valid %s", i+1, v, spec.Label))
				}
			}
		case SegmentFieldCategory, SegmentFieldCampaign, SegmentFieldSegment:
			for _, v := range c.Values {
				id, err := uuid.Parse(v)
				if err != nil {
					return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: %q is not a valid id", i+1, v))
				}
				if spec.Kind == SegmentFieldSegment && selfID != nil && id == *selfID {
					return errx.New(errx.BadRequest, "a segment cannot reference itself")
				}
			}
		case SegmentFieldNumber:
			n, err := strconv.Atoi(strings.TrimSpace(c.Value))
			if err != nil || n < 0 {
				return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: value must be a whole number", i+1))
			}
			c.Value = strconv.Itoa(n)
		case SegmentFieldDate:
			switch c.Operator {
			case SegOpWithinDays, SegOpNotWithinDays:
				n, err := strconv.Atoi(strings.TrimSpace(c.Value))
				if err != nil || n < 1 || n > 3650 {
					return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: days must be between 1 and 3650", i+1))
				}
				c.Value = strconv.Itoa(n)
			default:
				t, err := parseSegmentDate(c.Value)
				if err != nil {
					return errx.New(errx.BadRequest, fmt.Sprintf("condition %d: value must be a date (YYYY-MM-DD)", i+1))
				}
				c.Value = t.UTC().Format(time.RFC3339)
			}
		}
	}
	return nil
}

// SegmentReferences lists the other segments the conditions depend on.
func SegmentReferences(conds []SegmentCondition) []uuid.UUID {
	var out []uuid.UUID
	for _, c := range conds {
		if c.Field != "segment" {
			continue
		}
		for _, v := range c.Values {
			if id, err := uuid.Parse(v); err == nil {
				out = append(out, id)
			}
		}
	}
	return out
}

func parseSegmentDate(v string) (time.Time, error) {
	v = strings.TrimSpace(v)
	if t, err := time.Parse(time.RFC3339, v); err == nil {
		return t, nil
	}
	return time.Parse("2006-01-02", v)
}

func containsString(list []string, v string) bool {
	for _, s := range list {
		if s == v {
			return true
		}
	}
	return false
}
