/**
 * Pilot Network — domain rules for the Pilot Invitation Manager
 * (brief "Build the Pilot Invitation Manager Under Moderator Tools" +
 * Nash's answers, 2026-09-14). Pure data and pure functions — no I/O — so
 * the API routes, the daily runner, the import scripts and the tests all
 * share one set of rules.
 */

import { renderTemplate, DASHBOARD_LINKS } from '@/lib/email-templates'

/* ------------------------------ Statuses ------------------------------ */

/** §11 status filter, in the brief's order. */
export const INVITATION_STATUSES = [
  'imported',
  'ready_for_review',
  'approved_for_invitation',
  'invitation_sent',
  'follow_up_sent',
  'claimed',
  'opted_in',
  'unsubscribed',
  'invalid',
  'duplicate',
  'do_not_contact',
] as const
export type InvitationStatus = (typeof INVITATION_STATUSES)[number]

export const INVITATION_STATUS_LABELS: Record<InvitationStatus, string> = {
  imported: 'Imported',
  ready_for_review: 'Ready for Review',
  approved_for_invitation: 'Approved for Invitation',
  invitation_sent: 'Invitation Sent',
  follow_up_sent: 'Follow-Up Sent',
  claimed: 'Claimed',
  opted_in: 'Opted In',
  unsubscribed: 'Unsubscribed',
  invalid: 'Invalid',
  duplicate: 'Duplicate',
  do_not_contact: 'Do Not Contact',
}

export type ClaimStatus = 'unclaimed' | 'claimed'
export type AccountType = 'unknown' | 'pilot_driver' | 'pilot_company'
export type OptInStatus = 'not_opted_in' | 'opted_in' | 'unsubscribed'

/** §22 — never the same thing. */
export const DATA_STATUSES = ['imported', 'user_confirmed', 'hha_reviewed', 'document_verified'] as const
export type DataStatus = (typeof DATA_STATUSES)[number]
export const DATA_STATUS_LABELS: Record<DataStatus, string> = {
  imported: 'Imported',
  user_confirmed: 'User confirmed',
  hha_reviewed: 'HeavyHaul Agent reviewed',
  document_verified: 'Document verified',
}

/** §23 suppression reasons. */
export const SUPPRESSION_REASONS = ['unsubscribed', 'invalid_email', 'bounced', 'duplicate', 'do_not_contact', 'complaint', 'manual_block'] as const
export type SuppressionReason = (typeof SUPPRESSION_REASONS)[number]

export type CampaignStatus = 'draft' | 'running' | 'paused' | 'stopped' | 'completed'
export type RecipientStatus = 'pending' | 'sent' | 'follow_up_sent' | 'done' | 'skipped'

export type CapabilityGroup = 'position' | 'certification'

/* ------------------------------- Shapes ------------------------------- */

export interface CapabilityType {
  id: string
  key: string
  label: string
  group: CapabilityGroup
  sort_order: number
  active: boolean
}

export interface ProfileCapability {
  capability_type_id: string
  key: string
  label: string
  group: CapabilityGroup
  data_status: DataStatus
}

export interface PilotProfile {
  id: string
  name: string
  company_name: string | null
  email: string
  phone: string | null
  primary_state: string
  related_states: string[]
  account_type: AccountType
  claim_status: ClaimStatus
  invitation_status: InvitationStatus
  email_opt_in_status: OptInStatus
  sms_opt_in_status: OptInStatus
  source: string
  source_reference: string | null
  last_contacted_at: string | null
  next_follow_up_at: string | null
  claimed_user_id: string | null
  claimed_company_id: string | null
  claimed_at: string | null
  admin_notes: string | null
  claim_token: string | null
  unsubscribe_token: string
  created_at: string
  updated_at: string
  capabilities: ProfileCapability[]
  /** Present when the email is on the suppression list. */
  suppression_reason?: SuppressionReason | null
}

export interface PilotCampaign {
  id: string
  name: string
  state: string
  template_key: string
  follow_up_template_key: string | null
  daily_send_limit: number
  follow_up_delay_days: number
  start_date: string
  status: CampaignStatus
  created_by: string | null
  created_at: string
  updated_at: string
}

/* ------------------------- Send eligibility (§19) ------------------------- */

/** Statuses that block any further email — §19 and §23 together. */
export const BLOCKED_STATUSES: readonly InvitationStatus[] = ['claimed', 'opted_in', 'unsubscribed', 'invalid', 'duplicate', 'do_not_contact']

export type SendBlock =
  | 'suppressed'
  | 'claimed'
  | 'unsubscribed'
  | 'invalid'
  | 'duplicate'
  | 'do_not_contact'
  | 'opted_in'
  | 'not_approved'

/**
 * Why a profile must NOT be emailed right now, or null when it may be.
 * `requireApproval`: campaigns and "Send Invitation" only email profiles an
 * admin approved (§12 "Approve for Invitation" precedes "Send Invitation");
 * a resend or a follow-up skips that check because the first send already
 * proved approval.
 */
export function sendBlockFor(
  p: Pick<PilotProfile, 'invitation_status' | 'claim_status' | 'email_opt_in_status'> & { suppression_reason?: SuppressionReason | null },
  opts: { requireApproval: boolean },
): SendBlock | null {
  if (p.suppression_reason) return 'suppressed'
  if (p.claim_status === 'claimed' || p.invitation_status === 'claimed') return 'claimed'
  if (p.email_opt_in_status === 'unsubscribed' || p.invitation_status === 'unsubscribed') return 'unsubscribed'
  if (p.invitation_status === 'invalid') return 'invalid'
  if (p.invitation_status === 'duplicate') return 'duplicate'
  if (p.invitation_status === 'do_not_contact') return 'do_not_contact'
  if (p.invitation_status === 'opted_in') return 'opted_in'
  if (opts.requireApproval && !['approved_for_invitation', 'invitation_sent', 'follow_up_sent'].includes(p.invitation_status)) return 'not_approved'
  return null
}

export const SEND_BLOCK_LABELS: Record<SendBlock, string> = {
  suppressed: 'On the suppression list',
  claimed: 'Profile already claimed',
  unsubscribed: 'Unsubscribed',
  invalid: 'Marked invalid',
  duplicate: 'Marked duplicate',
  do_not_contact: 'Marked Do Not Contact',
  opted_in: 'Already opted in',
  not_approved: 'Not approved for invitation',
}

/** §19: one follow-up, N days after the initial send, then stop. */
export function followUpDueAt(sentAtIso: string, delayDays: number): string {
  const d = new Date(sentAtIso)
  d.setUTCDate(d.getUTCDate() + Math.max(0, Math.floor(delayDays)))
  return d.toISOString()
}

/** The calendar day (UTC) a send counts against — the daily limit is per day (§18, answer 5). */
export function sendDayKey(iso: string): string {
  return iso.slice(0, 10)
}

/* ---------------------- Duplicate detection (§28) ---------------------- */

export function normalizeEmail(email: string): string {
  return email.trim().toLowerCase()
}

/** Digits only; a leading US "1" is dropped so (480) 244-2636 = 1-480-244-2636. */
export function normalizePhone(phone: string | null | undefined): string | null {
  if (!phone) return null
  let digits = phone.replace(/\D/g, '')
  if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1)
  return digits.length >= 7 ? digits : null
}

/** Letters and digits only, lower-case — "A Plus Expeditor's" = "a plus expeditors". */
export function normalizeText(value: string | null | undefined): string {
  return (value ?? '').toLowerCase().replace(/['’]/g, '').replace(/[^a-z0-9]+/g, ' ').trim()
}

export interface ImportRow {
  name: string
  company?: string | null
  phone?: string | null
  email: string
  state: string
  capabilities: string[]
}

export type DuplicateMatch = { by: 'email' | 'phone' | 'name_company'; profile: { id: string } } | null

/**
 * §28 match order: email → phone → name + company. Name + company only when
 * both are present on both sides — a bare name is not enough to merge two
 * people. Returns the FIRST match in that order.
 */
export function findDuplicate<T extends { id: string; email: string; phone_digits: string | null; name: string; company_name: string | null }>(
  row: ImportRow,
  existing: T[],
): { by: 'email' | 'phone' | 'name_company'; profile: T } | null {
  const email = normalizeEmail(row.email)
  const byEmail = existing.find((p) => normalizeEmail(p.email) === email)
  if (byEmail) return { by: 'email', profile: byEmail }
  const phone = normalizePhone(row.phone)
  if (phone) {
    const byPhone = existing.find((p) => p.phone_digits === phone)
    if (byPhone) return { by: 'phone', profile: byPhone }
  }
  const name = normalizeText(row.name)
  const company = normalizeText(row.company)
  if (name && company) {
    const byNameCompany = existing.find((p) => normalizeText(p.name) === name && normalizeText(p.company_name) === company)
    if (byNameCompany) return { by: 'name_company', profile: byNameCompany }
  }
  return null
}

/** §28: the same pilot in AZ, NM and TX is ONE profile with three states. */
export function mergeStates(existing: string[], state: string): string[] {
  const s = state.trim().toUpperCase()
  return existing.includes(s) ? existing : [...existing, s]
}

/** Capability label → stable key ("High Pole" → "high_pole"). */
export function capabilityKey(label: string): string {
  return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
}

/* -------------------------- Template rendering (§15) -------------------------- */

export const PILOT_TEMPLATE_VARIABLES = [
  'name',
  'company_name',
  'state',
  'states',
  'capabilities',
  'claim_profile_link',
  'unsubscribe_link',
  'support_email',
  'cta_text',
  // Fixed per-user-type dashboard links (2026-09-19).
  'pilot_dispatch_dashboard_link',
  'pilot_driver_dashboard_link',
] as const

export interface PilotTemplateData {
  name: string
  company_name: string | null
  state: string
  states: string[]
  capabilities: string[]
  claim_profile_link: string
  unsubscribe_link: string
  support_email: string
  cta_text: string
}

export interface PilotTemplateParts {
  subject: string
  body: string
  cta_text?: string | null
  footer?: string | null
  unsubscribe_text?: string | null
}

export interface RenderedPilotEmail {
  subject: string
  text: string
}

/**
 * Renders subject + body + footer + unsubscribe wording for one lead.
 * §15: company may be empty — when it is, EVERY line that mentions
 * `{{company_name}}` is removed (the editor says so next to the variable),
 * so nothing renders as "Company: " or "at , ". Keep the company on its own
 * line in templates and the rest of the text is untouched.
 */
export function renderPilotTemplate(parts: PilotTemplateParts, data: PilotTemplateData): RenderedPilotEmail {
  const vars: Record<string, string> = {
    name: data.name,
    company_name: data.company_name ?? '',
    state: data.state,
    states: data.states.join(', '),
    capabilities: data.capabilities.join(', '),
    claim_profile_link: data.claim_profile_link,
    unsubscribe_link: data.unsubscribe_link,
    support_email: data.support_email,
    cta_text: parts.cta_text?.trim() || data.cta_text,
    pilot_dispatch_dashboard_link: DASHBOARD_LINKS.pilot_dispatch_dashboard_link,
    pilot_driver_dashboard_link: DASHBOARD_LINKS.pilot_driver_dashboard_link,
  }
  const render = (text: string) => renderTemplate(dropEmptyCompanyLines(text, data.company_name), vars)
  const sections = [render(parts.body)]
  if (parts.footer?.trim()) sections.push(render(parts.footer))
  if (parts.unsubscribe_text?.trim()) sections.push(render(parts.unsubscribe_text))
  return { subject: render(parts.subject), text: sections.join('\n\n') }
}

function dropEmptyCompanyLines(text: string, company: string | null): string {
  if (company && company.trim()) return text
  return text
    .split('\n')
    .filter((line) => !line.includes('{{company_name}}'))
    .join('\n')
}

/** True when a rendered text still carries a `{{variable}}` — a template bug to surface, never to send. */
export function hasUnresolvedVariables(text: string): boolean {
  return /\{\{\w+\}\}/.test(text)
}

/* ------------------------------ Analytics (§25) ------------------------------ */

export interface PilotNetworkAnalytics {
  total_imported: number
  invitations_sent: number
  claimed: number
  opted_in: number
  unsubscribed: number
  bounced: number
  bounce_rate: number | null
  claim_conversion_rate: number | null
  by_state: Array<{ state: string; count: number }>
  by_capability: Array<{ label: string; count: number }>
  /**
   * Email performance (Nash, 2026-09-23: "see the statistics on how many
   * emails made it, how many created"). From email_messages + the ZeptoMail
   * webhooks, for this pipeline's campaign type. `available` is false until
   * migration 0028 is applied.
   */
  email: EmailPerformance
}

export interface EmailPerformance {
  available: boolean
  attempted: number
  sent: number
  delivered: number
  opened: number
  clicked: number
  hard_bounced: number
  soft_bounced: number
  complained: number
  suppressed: number
  failed: number
  unsubscribed: number
  delivered_rate: number | null
  open_rate: number | null
  click_rate: number | null
  bounce_rate: number | null
  by_template: Array<{ template_key: string; version: number | null; sent: number; delivered: number; opened: number; clicked: number }>
  by_day: Array<{ day: string; sent: number; delivered: number; opened: number; clicked: number }>
}

export function ratio(numerator: number, denominator: number): number | null {
  return denominator > 0 ? Math.round((numerator / denominator) * 1000) / 10 : null
}

/** Nothing sent yet (dev preview, tests, and before migration 0028). */
export const EMPTY_EMAIL_PERFORMANCE: EmailPerformance = {
  available: false, attempted: 0, sent: 0, delivered: 0, opened: 0, clicked: 0, hard_bounced: 0, soft_bounced: 0, complained: 0, suppressed: 0, failed: 0, unsubscribed: 0,
  delivered_rate: null, open_rate: null, click_rate: null, bounce_rate: null, by_template: [], by_day: [],
}
