// Dev seeder.
//
// Idempotent fixture loader for the local dev/sim stack. Always loads:
//
//   - baseline user dev@warmbly.com / password123 with one org, one
//     shared worker, two connected email accounts (warmup-pooled), and
//     a sample webhook endpoint pointing at the docker-compose
//     webhook-sink service for end-to-end testing
//   - dev-org enrichment (seed.SeedDevOrg): four warmed mailboxes, labels
//     with real bindings, ~30 contacts, an actively sending campaign with
//     14 days of funnel + stats history, a live unified inbox, CRM,
//     reply templates, AI credits, and notifications
//
// When SEED_RICH=true (default in docker-compose), also loads:
//
//   - 3 orgs across tiers: Acme (free shared), Beta (premium shared),
//     Gamma (dedicated)
//   - 3 workers matching the worker hostnames in docker-compose.yml
//   - 6 email accounts spread across workers, joined to the right warmup pool
//   - one Beta campaign with a 2-step sequence and 10 contacts
//     (2 unsubscribed, so suppression behavior is observable in the UI)
//
// When SEED_FULL=true (also default in docker-compose), additionally runs
// the comprehensive seed in internal/seed: paid plans + Stripe-style
// subscriptions, admin/manager/viewer team users, reply templates, a full
// CRM (pipelines/deals/tasks/notes/activity), an API key, admin audit log
// entries, and an enterprise inquiry. Disjoint from SEED_RICH's IDs, so
// the two can coexist.
//
// Re-running the binary is safe: every insert is guarded by an EXISTS check
// or an ON CONFLICT clause.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/warmbly/warmbly/internal/infrastructure/db"
	"github.com/warmbly/warmbly/internal/pkg/argon2"
	"github.com/warmbly/warmbly/internal/seed"
)

// Stable UUIDs so fixture data is referenceable across runs. The dev
// user/org/worker anchors live in internal/seed so the dev-org enrichment
// (seed.SeedDevOrg) targets the same rows.
var (
	userDev   = seed.DevUserID
	userAcme  = uuid.MustParse("11111111-0000-0000-0000-000000000002")
	userBeta  = uuid.MustParse("11111111-0000-0000-0000-000000000003")
	userGamma = uuid.MustParse("11111111-0000-0000-0000-000000000004")

	orgDev   = seed.DevOrgID
	orgAcme  = uuid.MustParse("22222222-0000-0000-0000-000000000002")
	orgBeta  = uuid.MustParse("22222222-0000-0000-0000-000000000003")
	orgGamma = uuid.MustParse("22222222-0000-0000-0000-000000000004")

	// Match docker-compose.yml worker hostnames so seeded workers correspond
	// to running containers.
	workerShared    = seed.DevWorkerID
	workerPremium   = uuid.MustParse("10c8f5e4-1c39-5b2a-9c8b-3d2f0a8b1a02")
	workerDedicated = uuid.MustParse("10c8f5e4-1c39-5b2a-9c8b-3d2f0a8b1a03")
)

func main() {
	dsn := os.Getenv("PRIMARY_DB")
	if dsn == "" {
		dsn = "postgres://warmbly:warmbly@localhost:5432/warmbly_dev?sslmode=disable"
	}

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer pool.Close()

	// Apply embedded migrations before seeding. `make seed` is documented to
	// assume migrations are already applied, but running them here makes it
	// robust standalone: without it, new tables (e.g. crm_task_types) are
	// missing and the seed aborts partway, which looks like "nothing seeded".
	if err := db.RunMigrations(dsn); err != nil {
		log.Fatalf("migrations: %v", err)
	}

	if err := seedBaseline(ctx, pool); err != nil {
		log.Fatalf("baseline: %v", err)
	}

	// Enrich the dev org into a mid-flight workspace (mailboxes, labels,
	// contacts, an active campaign with history, unibox, CRM, credits).
	if err := seed.SeedDevOrg(ctx, pool); err != nil {
		log.Fatalf("dev org: %v", err)
	}
	fmt.Println("dev org enriched: 4 mailboxes, 30 contacts, active campaign with 14d history, unibox, CRM, credits")

	if os.Getenv("SEED_RICH") == "true" {
		if err := seedRich(ctx, pool); err != nil {
			log.Fatalf("rich: %v", err)
		}
	}

	if os.Getenv("SEED_FULL") == "true" {
		result, err := seed.Run(ctx, pool)
		if err != nil {
			log.Fatalf("full: %v", err)
		}
		if err := upsertDevTrialSubscription(ctx, pool); err != nil {
			log.Fatalf("dev trial subscription: %v", err)
		}
		result.Print(os.Stdout)
	}

	fmt.Println("seed complete")
}

// baseline

func seedBaseline(ctx context.Context, pool *pgxpool.Pool) error {
	if err := upsertUser(ctx, pool, userDev, "Dev", "User", "dev@warmbly.com", "password123"); err != nil {
		return err
	}
	if err := upsertOrg(ctx, pool, orgDev, "Dev's Organization", "dev", userDev); err != nil {
		return err
	}

	// Worker for dev accounts. Matches the docker-compose shared worker
	// hostname so the running worker container can pick the assignments up.
	if err := upsertWorker(ctx, pool, workerShared, "shared-1", "10.0.0.11", "shared", true, true); err != nil {
		return fmt.Errorf("dev worker: %w", err)
	}

	// Connect two email accounts on the dev org. Without these a fresh
	// stack has no mailboxes to test campaigns or warmup against, which
	// is what `make seed` users hit immediately after `make up`.
	devAccounts := []struct {
		id        uuid.UUID
		email     string
		name      string
		warmupTag string
		poolType  string
	}{
		{seed.DevMailboxSendID, "dev.send@warmbly.test", "Dev Sender", "dev-warmup-a", "premium"},
		{seed.DevMailboxOutboundID, "dev.outbound@warmbly.test", "Dev Outbound", "dev-warmup-b", "premium"},
	}
	for _, a := range devAccounts {
		if err := upsertEmailAccount(ctx, pool, a.id, userDev, orgDev, workerShared, a.email, a.name, a.warmupTag, a.poolType); err != nil {
			return fmt.Errorf("dev email_account %s: %w", a.email, err)
		}
		if err := joinWarmupPool(ctx, pool, a.id, a.poolType); err != nil {
			return fmt.Errorf("dev pool join %s: %w", a.email, err)
		}
	}
	// Dev-org contacts that match the unibox senders below, so opening those
	// threads resolves to a real contact in the CRM panel instead of
	// "Not a known contact". The mailer-daemon bounce stays a non-contact.
	devContacts := []struct {
		id             uuid.UUID
		first, last    string
		email, company string
	}{
		{uuid.MustParse("66666666-0000-0000-0000-0000dddc0001"), "Aiden", "Park", "aiden.park@northwind.test", "Northwind"},
		{uuid.MustParse("66666666-0000-0000-0000-0000dddc0002"), "Beth", "Chen", "beth.chen@initech.test", "Initech"},
		{uuid.MustParse("66666666-0000-0000-0000-0000dddc0003"), "Carlos", "Diaz", "carlos.diaz@piedpiper.test", "Pied Piper"},
	}
	for _, c := range devContacts {
		if err := upsertContact(ctx, pool, c.id, userDev, orgDev, c.first, c.last, c.email, c.company, true); err != nil {
			return fmt.Errorf("dev contact %s: %w", c.email, err)
		}
	}

	if err := seedLegacyUnibox(ctx, pool, []legacyUniboxEmail{
		{
			id: uuid.MustParse("77777777-0000-0000-0000-000000000101"), userID: userDev,
			emailID: devAccounts[0].id, threadID: "seed-dev-thread-reply",
			messageID: "<seed-dev-reply@northwind.test>", uid: 101,
			from: []string{"Aiden Park <aiden.park@northwind.test>"}, to: []string{"Dev Sender <dev.send@warmbly.test>"},
			subject: "Re: Dev seed inbox check", snippet: "This unread reply is here so the unified inbox can be tested immediately after make seed.",
			internalDate: "2026-05-30T09:20:00Z",
		},
		{
			id: uuid.MustParse("77777777-0000-0000-0000-000000000102"), userID: userDev,
			emailID: devAccounts[1].id, threadID: "seed-dev-thread-bounce",
			messageID: "<seed-dev-bounce@mailer-daemon.test>", uid: 201,
			from: []string{"Mail Delivery Subsystem <mailer-daemon@warmbly.test>"}, to: []string{"Dev Outbound <dev.outbound@warmbly.test>"},
			subject: "Delivery Status Notification (Failure)", snippet: "A seeded bounce-style message for testing filters, unread counts, and message detail fallback.",
			internalDate: "2026-05-29T16:05:00Z",
		},
	}); err != nil {
		return fmt.Errorf("dev unibox: %w", err)
	}

	// Sample webhook endpoint. The URL targets a local sink (e.g. an
	// ngrok-style capture or webhook-sink container) so devs can observe
	// outbound deliveries without external setup. Disabled by default so a
	// fresh stack never accidentally retries a nonexistent endpoint forever.
	if err := upsertWebhookEndpoint(ctx, pool,
		uuid.MustParse("77777777-0000-0000-0000-000000000001"),
		orgDev,
		"http://webhook-sink:8080/in",
		"Local dev sink (disabled — enable in /webhooks UI)",
		false,
	); err != nil {
		return fmt.Errorf("dev webhook endpoint: %w", err)
	}

	fmt.Println("baseline ok: dev@warmbly.com / password123")
	fmt.Println("  dev webhook : sample endpoint at http://webhook-sink:8080/in (disabled by default)")
	return nil
}

// rich fixture

func seedRich(ctx context.Context, pool *pgxpool.Pool) error {
	// users + orgs
	users := []struct {
		id                       uuid.UUID
		first, last, email, pass string
	}{
		{userAcme, "Alex", "Free", "alex@acme.test", "password123"},
		{userBeta, "Beth", "Pro", "beth@beta.test", "password123"},
		{userGamma, "Gus", "Dedicated", "gus@gamma.test", "password123"},
	}
	for _, u := range users {
		if err := upsertUser(ctx, pool, u.id, u.first, u.last, u.email, u.pass); err != nil {
			return fmt.Errorf("user %s: %w", u.email, err)
		}
	}

	orgs := []struct {
		id        uuid.UUID
		name      string
		slug      string
		ownerUser uuid.UUID
	}{
		{orgAcme, "Acme (free)", "acme", userAcme},
		{orgBeta, "Beta (pro)", "beta", userBeta},
		{orgGamma, "Gamma (enterprise)", "gamma", userGamma},
	}
	for _, o := range orgs {
		if err := upsertOrg(ctx, pool, o.id, o.name, o.slug, o.ownerUser); err != nil {
			return fmt.Errorf("org %s: %w", o.slug, err)
		}
	}

	// workers
	workers := []struct {
		id       uuid.UUID
		name     string
		ip       string
		wtype    string
		freeTier bool
		active   bool
	}{
		{workerShared, "shared-1", "10.0.0.11", "shared", true, true},
		{workerPremium, "premium-1", "10.0.0.12", "shared", false, true},
		{workerDedicated, "dedicated-1", "10.0.0.13", "dedicated", false, true},
	}
	for _, w := range workers {
		if err := upsertWorker(ctx, pool, w.id, w.name, w.ip, w.wtype, w.freeTier, w.active); err != nil {
			return fmt.Errorf("worker %s: %w", w.name, err)
		}
	}

	// email accounts: 2 per org, assigned to the matching worker
	type acct struct {
		id, user, org, worker uuid.UUID
		email, name, tag      string
		poolType              string // free / premium / nil for dedicated
	}
	accounts := []acct{
		{uuid.MustParse("33333333-0000-0000-0000-000000000001"), userAcme, orgAcme, workerShared,
			"alex.send@acme.test", "Alex Free", "acme-warmup-a", "free"},
		{uuid.MustParse("33333333-0000-0000-0000-000000000002"), userAcme, orgAcme, workerShared,
			"alex.outbound@acme.test", "Alex Free", "acme-warmup-b", "free"},
		{uuid.MustParse("33333333-0000-0000-0000-000000000003"), userBeta, orgBeta, workerPremium,
			"beth.send@beta.test", "Beth Pro", "beta-warmup-a", "premium"},
		{uuid.MustParse("33333333-0000-0000-0000-000000000004"), userBeta, orgBeta, workerPremium,
			"beth.outbound@beta.test", "Beth Pro", "beta-warmup-b", "premium"},
		{uuid.MustParse("33333333-0000-0000-0000-000000000005"), userGamma, orgGamma, workerDedicated,
			"gus.send@gamma.test", "Gus Dedicated", "gamma-warmup-a", "premium"},
		{uuid.MustParse("33333333-0000-0000-0000-000000000006"), userGamma, orgGamma, workerDedicated,
			"gus.outbound@gamma.test", "Gus Dedicated", "gamma-warmup-b", "premium"},
	}
	for _, a := range accounts {
		if err := upsertEmailAccount(ctx, pool, a.id, a.user, a.org, a.worker, a.email, a.name, a.tag, a.poolType); err != nil {
			return fmt.Errorf("email_account %s: %w", a.email, err)
		}
		if err := joinWarmupPool(ctx, pool, a.id, a.poolType); err != nil {
			return fmt.Errorf("join pool for %s: %w", a.email, err)
		}
	}
	if err := seedLegacyUnibox(ctx, pool, []legacyUniboxEmail{
		{
			id: uuid.MustParse("77777777-0000-0000-0000-000000000201"), userID: userBeta,
			emailID: accounts[2].id, threadID: "seed-beta-thread-meeting",
			messageID: "<seed-beta-meeting@initech.test>", uid: 301,
			from: []string{"Beth Chen <beth.chen@initech.test>"}, to: []string{"Beth Pro <beth.send@beta.test>"},
			subject: "Tuesday works", snippet: "Tuesday at 10 works. Please send the invite and include the placement dashboard example.",
			internalDate: "2026-05-30T08:45:00Z",
		},
		{
			id: uuid.MustParse("77777777-0000-0000-0000-000000000202"), userID: userBeta,
			emailID: accounts[3].id, threadID: "seed-beta-thread-ooo",
			messageID: "<seed-beta-ooo@piedpiper.test>", uid: 401,
			flags: []string{"\\Seen"}, seen: true,
			from: []string{"Carlos Diaz <carlos.diaz@piedpiper.test>"}, to: []string{"Beth Pro <beth.outbound@beta.test>"},
			subject: "Automatic reply: Quick question", snippet: "I am away until Monday with limited access to email.",
			internalDate: "2026-05-28T11:12:00Z",
		},
	}); err != nil {
		return fmt.Errorf("rich unibox: %w", err)
	}

	// campaign + sequence + contacts for Beta
	campaignID := uuid.MustParse("44444444-0000-0000-0000-000000000001")
	if err := upsertCampaign(ctx, pool, campaignID, userBeta, orgBeta, "Beta Cold Outreach Q1"); err != nil {
		return err
	}
	seq1 := uuid.MustParse("55555555-0000-0000-0000-000000000001")
	seq2 := uuid.MustParse("55555555-0000-0000-0000-000000000002")
	if err := upsertSequence(ctx, pool, seq1, campaignID, orgBeta, "Intro", "Quick question, {{first_name}}",
		"Hi {{first_name}}, ...intro body...", 0); err != nil {
		return err
	}
	if err := upsertSequence(ctx, pool, seq2, campaignID, orgBeta, "Follow-up", "Re: Quick question",
		"Hi {{first_name}}, following up...", 3); err != nil {
		return err
	}

	// 10 contacts; index 2 and 7 are unsubscribed to exercise suppression
	for i := 1; i <= 10; i++ {
		cid := uuid.MustParse(fmt.Sprintf("66666666-0000-0000-0000-%012d", i))
		subscribed := i != 2 && i != 7
		if err := upsertContact(ctx, pool, cid, userBeta, orgBeta,
			fmt.Sprintf("Lead%d", i), "Beta",
			fmt.Sprintf("lead%d@prospect.test", i),
			"Prospect Inc", subscribed); err != nil {
			return fmt.Errorf("contact %d: %w", i, err)
		}
		if err := upsertCampaignLead(ctx, pool, campaignID, cid); err != nil {
			return fmt.Errorf("campaign_lead %d: %w", i, err)
		}
	}

	fmt.Println("rich fixture loaded:")
	fmt.Println("  users     dev@warmbly.com, alex@acme.test, beth@beta.test, gus@gamma.test (pw: password123)")
	fmt.Println("  workers   shared / premium / dedicated (match docker-compose hostnames)")
	fmt.Println("  accounts  2 per org, joined to free or premium warmup pool")
	fmt.Println("  campaign  Beta Cold Outreach Q1 (2-step sequence, 10 contacts, 2 unsubscribed)")
	return nil
}

// helpers

func upsertUser(ctx context.Context, pool *pgxpool.Pool, id uuid.UUID, first, last, email, password string) error {
	var exists bool
	if err := pool.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE email=$1)", email).Scan(&exists); err != nil {
		return err
	}
	if exists {
		return nil
	}
	hash, err := argon2.Hash(password)
	if err != nil {
		return err
	}
	_, err = pool.Exec(ctx, `
		INSERT INTO users (id, first_name, last_name, email, password_hash)
		VALUES ($1, $2, $3, $4, $5)
		ON CONFLICT (id) DO NOTHING`,
		id, first, last, email, hash)
	return err
}

func upsertOrg(ctx context.Context, pool *pgxpool.Pool, id uuid.UUID, name, slug string, owner uuid.UUID) error {
	tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `
		INSERT INTO organizations (id, name, slug, owner_user_id)
		VALUES ($1, $2, $3, $4)
		ON CONFLICT (id) DO NOTHING`,
		id, name, slug, owner); err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO organization_members (organization_id, user_id, role, accepted_at)
		VALUES ($1, $2, 'owner', NOW())
		ON CONFLICT DO NOTHING`,
		id, owner); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

func upsertWorker(ctx context.Context, pool *pgxpool.Pool, id uuid.UUID, name, ip, wtype string, freeTier, active bool) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO workers (id, name, ip_addr, worker_type, free_tier, active)
		VALUES ($1, $2, $3, $4, $5, $6)
		ON CONFLICT (id) DO UPDATE SET
			name = EXCLUDED.name,
			ip_addr = EXCLUDED.ip_addr,
			worker_type = EXCLUDED.worker_type,
			free_tier = EXCLUDED.free_tier,
			active = EXCLUDED.active,
			updated_at = NOW()`,
		id, name, ip, wtype, freeTier, active)
	return err
}

func upsertEmailAccount(ctx context.Context, pool *pgxpool.Pool, id, userID, orgID, workerID uuid.UUID,
	email, name, warmupTag, poolType string) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO email_accounts (
			id, user_id, organization_id, worker_id,
			email, name,
			signature_plain, signature_html,
			provider, status,
			warmup_tag, warmup_pool_type
		) VALUES (
			$1, $2, $3, $4,
			$5, $6,
			'', '',
			'smtp_imap', 'active',
			$7, $8
		)
		ON CONFLICT (id) DO NOTHING`,
		id, userID, orgID, workerID, email, name, warmupTag, poolType)
	return err
}

func joinWarmupPool(ctx context.Context, pool *pgxpool.Pool, accountID uuid.UUID, poolType string) error {
	if poolType == "" {
		return nil
	}
	_, err := pool.Exec(ctx, `
		INSERT INTO warmup_pool_participants (pool_id, email_account_id)
		SELECT id, $1 FROM warmup_pools WHERE pool_type = $2::warmup_pool_type
		ON CONFLICT DO NOTHING`,
		accountID, poolType)
	return err
}

func upsertDevTrialSubscription(ctx context.Context, pool *pgxpool.Pool) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO subscriptions (
			id, user_id, organization_id, plan_id,
			stripe_customer_id, status,
			free_trial_started_at, free_trial_ends_at,
			is_enterprise, created_at, updated_at
		) VALUES (
			$1, $2, $3, $4,
			'', 'trialing',
			NOW(), NOW() + INTERVAL '14 days',
			FALSE, NOW(), NOW()
		)
		ON CONFLICT (organization_id) DO UPDATE SET
			plan_id = EXCLUDED.plan_id,
			status = 'trialing',
			free_trial_started_at = EXCLUDED.free_trial_started_at,
			free_trial_ends_at = EXCLUDED.free_trial_ends_at,
			updated_at = NOW()
	`, uuid.MustParse("88888888-0000-0000-0000-000000000001"), userDev, orgDev, seed.PlanFreeTrialID)
	return err
}

type legacyUniboxEmail struct {
	id           uuid.UUID
	userID       uuid.UUID
	emailID      uuid.UUID
	threadID     string
	messageID    string
	uid          uint32
	flags        []string
	from         []string
	to           []string
	subject      string
	snippet      string
	seen         bool
	internalDate string
}

func seedLegacyUnibox(ctx context.Context, pool *pgxpool.Pool, rows []legacyUniboxEmail) error {
	for _, row := range rows {
		if _, err := pool.Exec(ctx, `
			INSERT INTO unibox_mailboxes (email_id, uid_validity, mailbox, attributes, highestmodseq, updated_at)
			VALUES ($1, $2, 'INBOX', ARRAY['\HasNoChildren'], 1, NOW())
			ON CONFLICT (email_id, uid_validity) DO UPDATE SET
				mailbox = EXCLUDED.mailbox,
				attributes = EXCLUDED.attributes,
				highestmodseq = EXCLUDED.highestmodseq,
				updated_at = NOW()
		`, row.emailID, row.uid); err != nil {
			return err
		}

		internalDate, err := time.Parse(time.RFC3339, row.internalDate)
		if err != nil {
			return err
		}
		if row.flags == nil {
			row.flags = []string{}
		}

		_, err = pool.Exec(ctx, `
			INSERT INTO unibox_emails (
				id, user_id, email_id, mailbox, thread_id, message_id,
				gmail_id, parent_id, uid, mod_seq,
				flags, bcc, cc, from_addr, in_reply_to, reply_to,
				to_addr, subject, size, internal_date, sent_date,
				snippet, seen, created_at, updated_at
			) VALUES (
				$1, $2, $3, 0, $4, $5,
				'', '', $6, 1,
				$7, '{}', '{}', $8, '{}', '{}',
				$9, $10, $11, $12, $12,
				$13, $14, NOW(), NOW()
			)
			ON CONFLICT (id) DO UPDATE SET
				email_id = EXCLUDED.email_id,
				thread_id = EXCLUDED.thread_id,
				message_id = EXCLUDED.message_id,
				uid = EXCLUDED.uid,
				flags = EXCLUDED.flags,
				from_addr = EXCLUDED.from_addr,
				to_addr = EXCLUDED.to_addr,
				subject = EXCLUDED.subject,
				size = EXCLUDED.size,
				internal_date = EXCLUDED.internal_date,
				sent_date = EXCLUDED.sent_date,
				snippet = EXCLUDED.snippet,
				seen = EXCLUDED.seen,
				updated_at = NOW()
		`, row.id, row.userID, row.emailID, row.threadID, row.messageID, row.uid,
			row.flags, row.from, row.to, row.subject, int64(len(row.snippet)),
			internalDate, row.snippet, row.seen)
		if err != nil {
			return err
		}
	}
	return nil
}

func upsertCampaign(ctx context.Context, pool *pgxpool.Pool, id, userID, orgID uuid.UUID, name string) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO campaigns (
			id, user_id, organization_id, name, description,
			status, days, start_time, end_time, timezone,
			updated_at, created_at
		) VALUES (
			$1, $2, $3, $4, 'Seeded sample campaign',
			'active', 31, '09:00', '17:00', 'UTC',
			NOW(), NOW()
		)
		ON CONFLICT (id) DO NOTHING`,
		id, userID, orgID, name)
	return err
}

func upsertSequence(ctx context.Context, pool *pgxpool.Pool, id, campaignID, orgID uuid.UUID,
	name, subject, body string, waitAfter int) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO sequences (
			id, campaign_id, organization_id, name, subject,
			body_plain, body_html, wait_after
		) VALUES (
			$1, $2, $3, $4, $5,
			$6, $6, $7
		)
		ON CONFLICT (id) DO NOTHING`,
		id, campaignID, orgID, name, subject, body, waitAfter)
	return err
}

func upsertContact(ctx context.Context, pool *pgxpool.Pool, id, userID, orgID uuid.UUID,
	first, last, email, company string, subscribed bool) error {
	custom, _ := json.Marshal(map[string]any{})
	_, err := pool.Exec(ctx, `
		INSERT INTO contacts (
			id, user_id, organization_id,
			first_name, last_name, email, company, phone,
			custom_fields, subscribed
		) VALUES (
			$1, $2, $3,
			$4, $5, $6, $7, '',
			$8, $9
		)
		ON CONFLICT (id) DO NOTHING`,
		id, userID, orgID, first, last, email, company, custom, subscribed)
	return err
}

// upsertWebhookEndpoint inserts a sample webhook subscription. Uses a
// deterministic UUID so re-running the seed is idempotent. The secret is
// fixed in dev — never use this value in production.
func upsertWebhookEndpoint(ctx context.Context, pool *pgxpool.Pool, id, orgID uuid.UUID, url, description string, enabled bool) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO webhook_endpoints (
			id, organization_id, url, description, secret, event_types, enabled
		) VALUES (
			$1, $2, $3, $4, 'whsec_dev_seed_do_not_use_in_prod', '{}', $5
		)
		ON CONFLICT (id) DO NOTHING`,
		id, orgID, url, description, enabled)
	return err
}

func upsertCampaignLead(ctx context.Context, pool *pgxpool.Pool, campaignID, contactID uuid.UUID) error {
	_, err := pool.Exec(ctx, `
		INSERT INTO campaign_leads (campaign_id, contact_id)
		VALUES ($1, $2)
		ON CONFLICT DO NOTHING`,
		campaignID, contactID)
	return err
}
