/**
 * Freight Broker Leads — the rules (Nash, 2026-09-23).
 *
 * "These brokers are technically our leads that we can go and invite to
 * start using our platform… manage the leads, send them invites… create
 * campaigns… automate emails and schedule emails and put how many to go at
 * the time." Pure functions only: stages, segments, send windows, warm-up
 * ramps, step timing, stop rules, funnel maths. I/O lives in
 * src/lib/data/broker-leads.ts.
 */
import type { BrokerAgentLead } from '@/types/db'

/* ----------------------------------------------------------------- kinds */

/**
 * One lead engine, two kinds (2026-09-25). A broker lead is a freight broker
 * with a pre-verified brokerage; a carrier lead is a dispatcher or driver who
 * used the source system without a carrier relationship. Same tables, same
 * campaigns, same templates machinery — scoped by `lead_kind`.
 */
export type LeadKind = 'broker' | 'carrier'
export type CarrierLeadType = 'dispatcher' | 'driver' | 'both'
export const CARRIER_LEAD_TYPES: CarrierLeadType[] = ['dispatcher', 'driver', 'both']
export const CARRIER_LEAD_TYPE_LABELS: Record<CarrierLeadType, string> = { dispatcher: 'Dispatcher', driver: 'Driver', both: 'Dispatcher + driver' }

export interface LeadKindConfig {
  kind: LeadKind
  /** Page title. */
  label: string
  /** One lead, singular noun. */
  noun: string
  /** The company the lead belongs to: a brokerage or a carrier. */
  companyNoun: string
  path: string
  apiBase: string
  surface: 'broker_leads' | 'carrier_leads'
  campaignType: 'broker_recruitment' | 'carrier_recruitment'
  category: 'broker_opportunities' | 'carrier_opportunities'
  settingsId: 1 | 2
  templateKeys: { invitation: string; reminder: string; finalNotice: string }
  convertedLabel: string
  csvName: string
}

export const LEAD_KINDS: Record<LeadKind, LeadKindConfig> = {
  broker: {
    kind: 'broker', label: 'Freight Broker Leads', noun: 'freight broker', companyNoun: 'brokerage', path: '/admin/broker-leads', apiBase: '/api/admin/broker-leads', surface: 'broker_leads',
    campaignType: 'broker_recruitment', category: 'broker_opportunities', settingsId: 1,
    templateKeys: { invitation: 'broker_lead_invitation', reminder: 'broker_lead_reminder', finalNotice: 'broker_lead_final_notice' }, convertedLabel: 'Relation confirmed', csvName: 'freight-broker-leads',
  },
  carrier: {
    kind: 'carrier', label: 'Carrier Leads', noun: 'carrier lead', companyNoun: 'company', path: '/admin/carrier-leads', apiBase: '/api/admin/carrier-leads', surface: 'carrier_leads',
    campaignType: 'carrier_recruitment', category: 'carrier_opportunities', settingsId: 2,
    templateKeys: { invitation: 'carrier_lead_invitation', reminder: 'carrier_lead_reminder', finalNotice: 'carrier_lead_final_notice' }, convertedLabel: 'Active user', csvName: 'carrier-leads',
  },
}

export function leadKindConfig(kind: LeadKind | string | null | undefined): LeadKindConfig {
  return kind === 'carrier' ? LEAD_KINDS.carrier : LEAD_KINDS.broker
}

/* ---------------------------------------------------------------- stages */

export type LeadStage =
  | 'not_invited' | 'invited' | 'opened' | 'clicked' | 'signed_up' | 'converted' | 'declined'
  | 'bounced' | 'unsubscribed' | 'suppressed' | 'manual_review'

export const LEAD_STAGES: LeadStage[] = ['not_invited', 'invited', 'opened', 'clicked', 'signed_up', 'converted', 'declined', 'bounced', 'unsubscribed', 'suppressed', 'manual_review']

export const LEAD_STAGE_LABELS: Record<LeadStage, string> = {
  not_invited: 'Not invited', invited: 'Invited', opened: 'Opened', clicked: 'Clicked', signed_up: 'Signed up', converted: 'Relation confirmed', declined: 'Declined',
  bounced: 'Bounced', unsubscribed: 'Unsubscribed', suppressed: 'Suppressed', manual_review: 'Manual review',
}

export const LEAD_STAGE_TONE: Record<LeadStage, 'neutral' | 'info' | 'success' | 'warn' | 'danger'> = {
  not_invited: 'neutral', invited: 'info', opened: 'info', clicked: 'info', signed_up: 'success', converted: 'success', declined: 'warn',
  bounced: 'danger', unsubscribed: 'danger', suppressed: 'danger', manual_review: 'warn',
}

export type LeadLike = Pick<BrokerAgentLead, 'claim_status' | 'relationship_confirmation_status' | 'invitation_status'> & {
  lead_kind?: LeadKind | null
  signed_up_at?: string | null
  bounced_at?: string | null
  unsubscribed_at?: string | null
  suppressed_at?: string | null
  last_opened_at?: string | null
  last_clicked_at?: string | null
  last_emailed_at?: string | null
}

/** One word for where a lead is in the funnel. Terminal states first, then the furthest engagement. */
export function leadStage(l: LeadLike): LeadStage {
  if (l.claim_status === 'claimed' && l.relationship_confirmation_status === 'confirmed_current') return 'converted'
  if (l.claim_status === 'claimed' && l.relationship_confirmation_status === 'no_longer_current') return 'declined'
  // A carrier lead has no relation to confirm: creating the account is the conversion.
  if (l.lead_kind === 'carrier' && l.signed_up_at) return 'converted'
  if (l.claim_status === 'manual_review') return 'manual_review'
  if (l.suppressed_at) return 'suppressed'
  if (l.unsubscribed_at || l.invitation_status === 'opted_out') return 'unsubscribed'
  if (l.bounced_at) return 'bounced'
  if (l.signed_up_at) return 'signed_up'
  if (l.last_clicked_at) return 'clicked'
  if (l.last_opened_at) return 'opened'
  if (l.last_emailed_at || l.invitation_status === 'invited' || l.invitation_status === 'reminded') return 'invited'
  return 'not_invited'
}

/** Can this lead receive a campaign email at all? (The central email policy still has the last word.) */
export function leadSendBlock(l: LeadLike): string | null {
  const s = leadStage(l)
  if (s === 'converted') return 'already_converted'
  if (s === 'declined') return 'declined'
  if (s === 'manual_review') return 'manual_review'
  if (s === 'suppressed') return 'suppressed'
  if (s === 'unsubscribed') return 'unsubscribed'
  if (s === 'bounced') return 'bounced'
  if (s === 'signed_up') return 'already_signed_up'
  return null
}

/* -------------------------------------------------------------- segments */

export interface SegmentFilters {
  q?: string
  brokerage_id?: string
  states?: string[]
  domain_check?: Array<'matches_brokerage' | 'differs_from_brokerage' | 'brokerage_email_unknown'>
  mailbox_type?: Array<'person' | 'role'>
  stages?: LeadStage[]
  invitation_status?: Array<BrokerAgentLead['invitation_status']>
  claim_status?: Array<BrokerAgentLead['claim_status']>
  tags?: string[]
  has_last_name?: boolean
  last_seen_after?: string   // ISO date — Synchron activity
  last_seen_before?: string
  never_emailed?: boolean
  source_system?: string
  import_batch?: string
  /** Carrier leads: dispatcher / driver / both. */
  lead_type?: CarrierLeadType[]
  /** Carrier leads: still active in the source system. */
  source_active?: boolean
}

export const EMPTY_FILTERS: SegmentFilters = {}

/** Parse ?q=&state=TX,FL&stage=… into filters. Everything optional; unknown values are dropped. */
export function filtersFromParams(get: (k: string) => string | null): SegmentFilters {
  const list = (k: string) => (get(k) ?? '').split(',').map((s) => s.trim()).filter(Boolean)
  const f: SegmentFilters = {}
  const q = (get('q') ?? '').trim(); if (q) f.q = q
  const b = get('brokerage'); if (b) f.brokerage_id = b
  const st = list('state').map((s) => s.toUpperCase()); if (st.length) f.states = st
  const dc = list('domain').filter((v): v is NonNullable<SegmentFilters['domain_check']>[number] => ['matches_brokerage', 'differs_from_brokerage', 'brokerage_email_unknown'].includes(v)); if (dc.length) f.domain_check = dc
  const mb = list('mailbox').filter((v): v is 'person' | 'role' => v === 'person' || v === 'role'); if (mb.length) f.mailbox_type = mb
  const sg = list('stage').filter((v): v is LeadStage => (LEAD_STAGES as string[]).includes(v)); if (sg.length) f.stages = sg
  const tg = list('tag'); if (tg.length) f.tags = tg
  if (get('has_last_name') === '1') f.has_last_name = true
  if (get('never_emailed') === '1') f.never_emailed = true
  const a = get('seen_after'); if (a && /^\d{4}-\d{2}-\d{2}$/.test(a)) f.last_seen_after = a
  const bf = get('seen_before'); if (bf && /^\d{4}-\d{2}-\d{2}$/.test(bf)) f.last_seen_before = bf
  const src = get('source'); if (src) f.source_system = src
  const batch = get('batch'); if (batch) f.import_batch = batch
  const lt = list('type').filter((v): v is CarrierLeadType => (CARRIER_LEAD_TYPES as string[]).includes(v)); if (lt.length) f.lead_type = lt
  if (get('active') === '1') f.source_active = true
  return f
}

export function filtersToParams(f: SegmentFilters): string {
  const p = new URLSearchParams()
  if (f.q) p.set('q', f.q)
  if (f.brokerage_id) p.set('brokerage', f.brokerage_id)
  if (f.states?.length) p.set('state', f.states.join(','))
  if (f.domain_check?.length) p.set('domain', f.domain_check.join(','))
  if (f.mailbox_type?.length) p.set('mailbox', f.mailbox_type.join(','))
  if (f.stages?.length) p.set('stage', f.stages.join(','))
  if (f.tags?.length) p.set('tag', f.tags.join(','))
  if (f.has_last_name) p.set('has_last_name', '1')
  if (f.never_emailed) p.set('never_emailed', '1')
  if (f.last_seen_after) p.set('seen_after', f.last_seen_after)
  if (f.last_seen_before) p.set('seen_before', f.last_seen_before)
  if (f.source_system) p.set('source', f.source_system)
  if (f.import_batch) p.set('batch', f.import_batch)
  if (f.lead_type?.length) p.set('type', f.lead_type.join(','))
  if (f.source_active) p.set('active', '1')
  return p.toString()
}

/** Plain-English description of a segment, for the campaign card and the audit log. */
export function describeSegment(f: SegmentFilters): string {
  const parts: string[] = []
  if (f.q) parts.push(`matching "${f.q}"`)
  if (f.brokerage_id) parts.push('one brokerage')
  if (f.states?.length) parts.push(`in ${f.states.join(', ')}`)
  if (f.stages?.length) parts.push(f.stages.map((s) => LEAD_STAGE_LABELS[s].toLowerCase()).join(' or '))
  if (f.domain_check?.length) parts.push(f.domain_check.map((d) => ({ matches_brokerage: 'email matches brokerage', differs_from_brokerage: 'email differs from brokerage', brokerage_email_unknown: 'brokerage email unknown' })[d]).join(' or '))
  if (f.mailbox_type?.length) parts.push(f.mailbox_type.length === 1 ? (f.mailbox_type[0] === 'person' ? 'people only' : 'role mailboxes only') : '')
  if (f.tags?.length) parts.push(`tagged ${f.tags.join(', ')}`)
  if (f.has_last_name) parts.push('with a last name')
  if (f.never_emailed) parts.push('never emailed')
  if (f.last_seen_after) parts.push(`active since ${f.last_seen_after}`)
  if (f.last_seen_before) parts.push(`last active before ${f.last_seen_before}`)
  if (f.source_system) parts.push(`from ${f.source_system}`)
  if (f.lead_type?.length) parts.push(f.lead_type.map((t) => CARRIER_LEAD_TYPE_LABELS[t].toLowerCase()).join(' or '))
  if (f.source_active) parts.push('still active in the source')
  return parts.filter(Boolean).join(', ') || 'every lead'
}

/** Applied client-side to an already-loaded row set (server does the same in SQL). */
export function leadMatchesFilters(l: BrokerAgentLead & { state?: string | null; mailbox_type?: string | null; email_domain_check?: string | null; tags?: string[] | null; source_last_seen_at?: string | null; source_system?: string | null; last_emailed_at?: string | null; brokerage_name?: string | null; lead_type?: string | null; company_name?: string | null; source_status_active?: boolean | null }, f: SegmentFilters): boolean {
  // The "brokerage" filter is the company key: a company id for broker leads, the company name for carrier leads.
  if (f.brokerage_id && l.broker_company_id !== f.brokerage_id && (l.company_name ?? '') !== f.brokerage_id) return false
  if (f.lead_type?.length && !f.lead_type.includes((l.lead_type ?? '') as CarrierLeadType)) return false
  if (f.source_active && !l.source_status_active) return false
  if (f.states?.length && !f.states.includes((l.state ?? '').toUpperCase())) return false
  if (f.domain_check?.length && !f.domain_check.includes((l.email_domain_check ?? '') as never)) return false
  if (f.mailbox_type?.length && !f.mailbox_type.includes((l.mailbox_type ?? 'person') as never)) return false
  if (f.stages?.length && !f.stages.includes(leadStage(l))) return false
  if (f.invitation_status?.length && !f.invitation_status.includes(l.invitation_status)) return false
  if (f.claim_status?.length && !f.claim_status.includes(l.claim_status)) return false
  if (f.tags?.length && !f.tags.every((t) => (l.tags ?? []).includes(t))) return false
  if (f.has_last_name && !l.last_name) return false
  if (f.never_emailed && l.last_emailed_at) return false
  if (f.last_seen_after && (!l.source_last_seen_at || l.source_last_seen_at.slice(0, 10) < f.last_seen_after)) return false
  if (f.last_seen_before && (!l.source_last_seen_at || l.source_last_seen_at.slice(0, 10) > f.last_seen_before)) return false
  if (f.source_system && l.source_system !== f.source_system) return false
  if (f.import_batch && l.import_batch !== f.import_batch) return false
  if (f.q) {
    const q = f.q.toLowerCase()
    const hay = [l.first_name, l.last_name, l.email, l.broker_company_name, l.brokerage_name, l.company_name, l.mc_number, l.phone].filter(Boolean).join(' ').toLowerCase()
    if (!hay.includes(q)) return false
  }
  return true
}

/* ----------------------------------------------------------- time zones */

/** Majority time zone per US state — good enough for a business-hours send window. */
export const STATE_TIMEZONE: Record<string, string> = {
  AL: 'America/Chicago', AK: 'America/Anchorage', AZ: 'America/Phoenix', AR: 'America/Chicago', CA: 'America/Los_Angeles', CO: 'America/Denver',
  CT: 'America/New_York', DE: 'America/New_York', DC: 'America/New_York', FL: 'America/New_York', GA: 'America/New_York', HI: 'Pacific/Honolulu',
  ID: 'America/Boise', IL: 'America/Chicago', IN: 'America/Indiana/Indianapolis', IA: 'America/Chicago', KS: 'America/Chicago', KY: 'America/New_York',
  LA: 'America/Chicago', ME: 'America/New_York', MD: 'America/New_York', MA: 'America/New_York', MI: 'America/Detroit', MN: 'America/Chicago',
  MS: 'America/Chicago', MO: 'America/Chicago', MT: 'America/Denver', NE: 'America/Chicago', NV: 'America/Los_Angeles', NH: 'America/New_York',
  NJ: 'America/New_York', NM: 'America/Denver', NY: 'America/New_York', NC: 'America/New_York', ND: 'America/Chicago', OH: 'America/New_York',
  OK: 'America/Chicago', OR: 'America/Los_Angeles', PA: 'America/New_York', RI: 'America/New_York', SC: 'America/New_York', SD: 'America/Chicago',
  TN: 'America/Chicago', TX: 'America/Chicago', UT: 'America/Denver', VT: 'America/New_York', VA: 'America/New_York', WA: 'America/Los_Angeles',
  WV: 'America/New_York', WI: 'America/Chicago', WY: 'America/Denver', PR: 'America/Puerto_Rico',
}
export const DEFAULT_TIMEZONE = 'America/New_York'

export function timezoneForLead(state: string | null | undefined, window: SendWindow | null | undefined): string {
  if (window?.timezone && window.timezone !== 'lead') return window.timezone
  return STATE_TIMEZONE[(state ?? '').toUpperCase()] ?? DEFAULT_TIMEZONE
}

/** Local wall-clock parts of an instant in a zone (no library). */
export function localParts(at: Date, timeZone: string): { weekday: number; hour: number; minute: number; ymd: string } {
  const fmt = new Intl.DateTimeFormat('en-US', { timeZone, hour12: false, weekday: 'short', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
  const p = Object.fromEntries(fmt.formatToParts(at).map((x) => [x.type, x.value]))
  const weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(p.weekday)
  return { weekday, hour: Number(p.hour) % 24, minute: Number(p.minute), ymd: `${p.year}-${p.month}-${p.day}` }
}

/** Offset (ms) of a zone at an instant: local wall clock minus UTC. */
function zoneOffsetMs(at: Date, timeZone: string): number {
  const fmt = new Intl.DateTimeFormat('en-US', { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
  const p = Object.fromEntries(fmt.formatToParts(at).map((x) => [x.type, x.value]))
  const asUtc = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(p.hour) % 24, Number(p.minute), Number(p.second))
  return asUtc - at.getTime()
}

/* ------------------------------------------------------------ windows */

export interface SendWindow { days: number[]; start_hour: number; end_hour: number; timezone: 'lead' | string }
export const DEFAULT_SEND_WINDOW: SendWindow = { days: [1, 2, 3, 4, 5], start_hour: 9, end_hour: 17, timezone: 'lead' }

export function normalizeWindow(w: Partial<SendWindow> | null | undefined): SendWindow {
  const days = Array.isArray(w?.days) ? w!.days.map(Number).filter((d) => d >= 0 && d <= 6) : DEFAULT_SEND_WINDOW.days
  const start = Number.isFinite(w?.start_hour) ? Math.min(23, Math.max(0, Number(w!.start_hour))) : DEFAULT_SEND_WINDOW.start_hour
  const end = Number.isFinite(w?.end_hour) ? Math.min(24, Math.max(1, Number(w!.end_hour))) : DEFAULT_SEND_WINDOW.end_hour
  return { days: days.length ? [...new Set(days)].sort() : DEFAULT_SEND_WINDOW.days, start_hour: start, end_hour: end > start ? end : start + 1, timezone: w?.timezone || 'lead' }
}

/** Is `at` inside the window in `timeZone`? */
export function windowOpen(w: SendWindow, at: Date, timeZone: string): boolean {
  const p = localParts(at, timeZone)
  return w.days.includes(p.weekday) && p.hour >= w.start_hour && p.hour < w.end_hour
}

/** The first instant at or after `from` when the window is open in `timeZone` (searching up to 14 days). */
export function nextWindowStart(w: SendWindow, from: Date, timeZone: string): Date {
  if (windowOpen(w, from, timeZone)) return from
  for (let d = 0; d < 15; d++) {
    const probe = new Date(from.getTime() + d * 86_400_000)
    const p = localParts(probe, timeZone)
    if (!w.days.includes(p.weekday)) continue
    if (d === 0 && p.hour >= w.end_hour) continue
    // local start_hour on that day → instant
    const [y, m, day] = p.ymd.split('-').map(Number)
    const guess = new Date(Date.UTC(y, m - 1, day, w.start_hour, 0, 0))
    const at = new Date(guess.getTime() - zoneOffsetMs(guess, timeZone))
    if (at.getTime() >= from.getTime()) return at
  }
  return from
}

/* --------------------------------------------------------------- ramp */

export interface Ramp { start: number; step: number; max: number }
export const DEFAULT_RAMP: Ramp = { start: 50, step: 50, max: 300 }

export function normalizeRamp(r: Partial<Ramp> | null | undefined): Ramp | null {
  if (!r || !Number.isFinite(r.start)) return null
  const start = Math.max(1, Number(r.start)), step = Math.max(0, Number(r.step ?? 0)), max = Math.max(start, Number(r.max ?? start))
  return { start, step, max }
}

/**
 * How many emails a campaign may send on its N-th sending day (0-based),
 * given its hard daily limit and an optional warm-up ramp. Warm-up protects
 * the new updates domain: start small, grow every day, never above the limit.
 */
export function dailyAllowance(c: { daily_send_limit: number; ramp?: Ramp | null }, dayIndex: number): number {
  const limit = Math.max(0, c.daily_send_limit)
  if (!c.ramp) return limit
  const ramped = Math.min(c.ramp.max, c.ramp.start + c.ramp.step * Math.max(0, dayIndex))
  return Math.min(limit, ramped)
}

/** 0-based index of `today` among the campaign's sending days since its first run. */
export function sendingDayIndex(firstRunDate: string | null, today: string): number {
  if (!firstRunDate || firstRunDate > today) return 0
  return Math.max(0, Math.round((Date.parse(today) - Date.parse(firstRunDate)) / 86_400_000))
}

/* --------------------------------------------------------------- steps */

export interface CampaignStep { step: number; template_key: string; delay_days: number }

export function normalizeSteps(steps: unknown): CampaignStep[] {
  if (!Array.isArray(steps)) return []
  return steps
    .map((s) => ({ template_key: String((s as CampaignStep)?.template_key ?? '').trim(), delay_days: Math.max(0, Number((s as CampaignStep)?.delay_days ?? 0) || 0) }))
    .filter((s) => s.template_key)
    .map((s, i) => ({ step: i + 1, ...s }))
}

/** When the next step becomes due: sent time + delay days, then pushed into the next open window. */
export function stepDueAt(sentAt: Date, delayDays: number, w: SendWindow, timeZone: string): Date {
  const raw = new Date(sentAt.getTime() + Math.max(0, delayDays) * 86_400_000)
  return nextWindowStart(w, raw, timeZone)
}

/** Frequency cap across every campaign: at most one email per lead every `capDays`. */
export function frequencyCapOk(lastEmailedAt: string | null | undefined, capDays: number, now: Date): boolean {
  if (!lastEmailedAt || capDays <= 0) return true
  return now.getTime() - Date.parse(lastEmailedAt) >= capDays * 86_400_000
}

export function frequencyCapReleaseAt(lastEmailedAt: string, capDays: number): Date {
  return new Date(Date.parse(lastEmailedAt) + capDays * 86_400_000)
}

/* ------------------------------------------------------------ campaign */

export type CampaignStatus = 'draft' | 'running' | 'paused' | 'stopped' | 'completed'

export const CAMPAIGN_STATUS_LABELS: Record<CampaignStatus, string> = { draft: 'Draft', running: 'Running', paused: 'Paused', stopped: 'Stopped', completed: 'Completed' }

export function campaignTransition(from: CampaignStatus, action: 'approve' | 'pause' | 'resume' | 'stop' | 'complete'): { ok: true; to: CampaignStatus } | { ok: false; error: string } {
  switch (action) {
    case 'approve': return from === 'draft' || from === 'paused' ? { ok: true, to: 'running' } : { ok: false, error: 'Only a draft or paused campaign can be started.' }
    case 'pause': return from === 'running' ? { ok: true, to: 'paused' } : { ok: false, error: 'Only a running campaign can be paused.' }
    case 'resume': return from === 'paused' ? { ok: true, to: 'running' } : { ok: false, error: 'Only a paused campaign can be resumed.' }
    case 'stop': return from === 'running' || from === 'paused' || from === 'draft' ? { ok: true, to: 'stopped' } : { ok: false, error: 'This campaign is already finished.' }
    case 'complete': return from === 'running' ? { ok: true, to: 'completed' } : { ok: false, error: 'Only a running campaign completes.' }
  }
}

/** What must be true before a campaign may start. */
export function campaignReadiness(c: { name: string; steps: CampaignStep[]; start_date: string | null; daily_send_limit: number; enrolled: number; templatesActive: Record<string, boolean> }): string[] {
  const problems: string[] = []
  if (!c.name.trim()) problems.push('Give the campaign a name.')
  if (c.steps.length === 0) problems.push('Add at least one step with a template.')
  for (const s of c.steps) if (c.templatesActive[s.template_key] === false) problems.push(`Template "${s.template_key}" is inactive.`)
  for (const s of c.steps) if (!(s.template_key in c.templatesActive)) problems.push(`Template "${s.template_key}" does not exist.`)
  if (!c.start_date) problems.push('Set a start date.')
  if (c.daily_send_limit <= 0) problems.push('The daily limit must be at least 1.')
  if (c.enrolled === 0) problems.push('Enrol the audience first — nobody is in this campaign yet.')
  return problems
}

/** Should the campaign stop sending to this recipient now, and why. */
export function recipientStopReason(lead: LeadLike): string | null {
  const block = leadSendBlock(lead)
  if (!block) return null
  return block
}

/* --------------------------------------------------------------- funnel */

export interface FunnelCounts { enrolled: number; sent: number; delivered: number; opened: number; clicked: number; signed_up: number; converted: number; bounced: number; unsubscribed: number }

export function funnelRates(f: FunnelCounts): Record<keyof FunnelCounts, number> & { delivered_pct: number; open_pct: number; click_pct: number; signup_pct: number; conversion_pct: number; bounce_pct: number } {
  const pct = (n: number, d: number) => (d > 0 ? Math.round((n / d) * 1000) / 10 : 0)
  return {
    ...f,
    delivered_pct: pct(f.delivered, f.sent), open_pct: pct(f.opened, f.delivered), click_pct: pct(f.clicked, f.delivered),
    signup_pct: pct(f.signed_up, f.sent), conversion_pct: pct(f.converted, f.sent), bounce_pct: pct(f.bounced, f.sent),
  }
}

/* ------------------------------------------------------------ templates */

/** Variables a lead campaign template may use (on top of the platform-wide ones). */
export const LEAD_TEMPLATE_VARIABLES = [
  'first_name', 'last_name', 'full_name', 'brokerage_name', 'mc_number', 'join_link', 'freight_broker_dashboard_link', 'unsubscribe_link', 'preferences_link', 'support_email', 'company_footer',
] as const
export const CARRIER_LEAD_TEMPLATE_VARIABLES = [
  'first_name', 'last_name', 'full_name', 'company_name', 'lead_type', 'join_link', 'unsubscribe_link', 'preferences_link', 'support_email', 'company_footer',
] as const
export function leadTemplateVariables(kind: LeadKind): readonly string[] {
  return kind === 'carrier' ? CARRIER_LEAD_TEMPLATE_VARIABLES : LEAD_TEMPLATE_VARIABLES
}

export const LEAD_TEMPLATE_KEYS = ['broker_lead_invitation', 'broker_lead_reminder', 'broker_lead_final_notice'] as const
export const CARRIER_LEAD_TEMPLATE_KEYS = ['carrier_lead_invitation', 'carrier_lead_reminder', 'carrier_lead_final_notice'] as const

export function leadTemplateData(l: { first_name: string | null; last_name: string | null; broker_company_name: string | null; mc_number: string | null; email: string; company_name?: string | null; lead_type?: string | null }, joinLink: string): Record<string, string> {
  const first = (l.first_name ?? '').trim()
  const last = (l.last_name ?? '').trim()
  return {
    first_name: first || 'there', last_name: last, full_name: [first, last].filter(Boolean).join(' ') || l.email,
    brokerage_name: l.broker_company_name ?? '', mc_number: l.mc_number ?? '', join_link: joinLink,
    company_name: l.company_name ?? '', lead_type: l.lead_type ? CARRIER_LEAD_TYPE_LABELS[l.lead_type as CarrierLeadType] ?? l.lead_type : '',
  }
}

/** The platform role a carrier lead's account starts with, and where its dashboard is. */
export function carrierLeadRole(leadType: string | null | undefined): { role: 'dispatcher' | 'driver'; next: string } {
  return leadType === 'driver' ? { role: 'driver', next: '/ct-dashboard' } : { role: 'dispatcher', next: '/cd-dashboard' }
}

/* ------------------------------------------------------------- names */

/** "Terry" → first only; "Mary Ann Smith" → first "Mary Ann"? No — first word is the first name, the rest the last name. */
export function splitName(raw: string): { first: string; last: string } {
  const n = raw.replace(/[,;]+/g, ' ').replace(/\s+/g, ' ').trim()
  if (!n) return { first: '', last: '' }
  const [first, ...rest] = n.split(' ')
  return { first, last: rest.join(' ') }
}
