import 'server-only'

import { createAdminClient } from '@/lib/supabase/admin'
import { suspendCompanyRulesForMember } from '@/lib/data/auto-participant-rules'
import { sendPlatformEmail } from '@/lib/email/send'
import { isMissingColumn } from '@/lib/db-compat'
import { emailPattern } from '@/lib/like'
import { getCompanyContext, listCompanyAudit, listCompanyMembers, logCompanyEvent, raiseVerification } from '@/lib/data/companies'
import { addMembershipRole, logRoleAction, syncMembershipRoleStatus } from '@/lib/data/modes'
import { CARRIER_COMPANY_ROLES, ROLE_LABELS, type RoleType } from '@/lib/domain/modes'
import {
  COMPANY_AUDIT, canManageCarrierCompany, carrierRoleLabel, companyInfoView, membershipTransition, permissionLevel,
  relationshipStatus, type CompanyInfoView, type EditableCompanyField, type RejectionReason,
} from '@/lib/domain/carrier-company'
import { BROKER_AUDIT, brokerRoleLabel } from '@/lib/domain/broker-company'
import { BULK_INVITE_LIMIT, type InvitedPerson } from '@/lib/domain/company-invites'
import type {
  Company, CompanyAuditEvent, CompanyClaim, CompanyInvitation, CompanyMembership, CompanyPermissionLevel, CompanyVerificationRecord,
  CompanyVerificationSource, OfficialEmailStatus,
} from '@/types/db'
import type { SessionUser } from '@/lib/auth'

/**
 * Company Info data access (2026-09-19). Rules live in
 * src/lib/domain/carrier-company.ts; this reads and writes. Every read and
 * write tolerates migration 0021 not being applied: new columns are dropped
 * from the write and re-tried, never a 500.
 *
 * The membership service is shared by the Carrier and Broker dashboards —
 * the PAGES stay role-specific; what differs per company type (role words,
 * email events, dashboard link) is the `flow` below, chosen from
 * company.company_type. Broker-only pieces (historical records, the
 * verification email log) live in src/lib/data/broker-company.ts.
 */

export interface RelationshipFlow {
  memberNoun: string
  roleLabel: (role: CompanyMembership['role']) => string
  /** Role written on a new membership for this company type. */
  memberRole: 'carrier_dispatcher' | 'freight_broker'
  events: { requested: string; approved: string; rejected: string; revoked: string; reminder: string; adminGranted: string | null; invitation: string; bulkInvitation: string }
  audit: { requested: string; approved: string; rejected: string; revoked: string; reminderSent: string; invited: string; bulkInvited: string; reinstated: string }
}

export function flowFor(company: Pick<Company, 'company_type'>): RelationshipFlow {
  if (company.company_type === 'broker') {
    return {
      memberNoun: 'Freight Broker',
      memberRole: 'freight_broker',
      roleLabel: brokerRoleLabel,
      events: {
        requested: 'broker_relationship_verification_request', approved: 'broker_relationship_verified', rejected: 'broker_relationship_rejected',
        revoked: 'broker_relationship_revoked', reminder: 'broker_relationship_reminder', adminGranted: 'broker_company_admin_invitation', invitation: 'broker_company_invitation', bulkInvitation: 'broker_company_bulk_invitation',
      },
      audit: { requested: BROKER_AUDIT.requested, approved: BROKER_AUDIT.approved, rejected: BROKER_AUDIT.rejected, revoked: BROKER_AUDIT.revoked, reminderSent: COMPANY_AUDIT.reminderSent, invited: BROKER_AUDIT.invited, bulkInvited: BROKER_AUDIT.bulkInvited, reinstated: BROKER_AUDIT.reinstated },
    }
  }
  return {
    memberNoun: 'Carrier Dispatcher',
    memberRole: 'carrier_dispatcher',
    roleLabel: carrierRoleLabel,
    events: {
      requested: 'company_relationship_requested', approved: 'company_relationship_approved', rejected: 'company_relationship_rejected',
      revoked: 'company_relationship_revoked', reminder: 'company_verification_reminder', adminGranted: null, invitation: 'company_dispatcher_invitation', bulkInvitation: 'company_dispatcher_bulk_invitation',
    },
    audit: { requested: COMPANY_AUDIT.requested, approved: COMPANY_AUDIT.approved, rejected: COMPANY_AUDIT.rejected, revoked: COMPANY_AUDIT.revoked, reminderSent: COMPANY_AUDIT.reminderSent, invited: COMPANY_AUDIT.invited, bulkInvited: COMPANY_AUDIT.bulkInvited, reinstated: COMPANY_AUDIT.reinstated },
  }
}

const MIGRATION_0021_COLUMNS = [
  'permission_level', 'requested_at', 'approved_by_user_id', 'rejected_at', 'rejected_by', 'rejected_by_user_id',
  'rejection_reason', 'rejection_note', 'revoked_by_user_id', 'verification_method', 'verification_provider', 'updated_at',
  'official_email_status', 'official_email_verification_source', 'official_email_verified_at', 'verification_source',
  'official_data_synced_at', 'operations_phone', 'operations_email', 'dispatch_contact', 'preferred_contact', 'description',
]

/** Update, and if a 0021 column (or the 0021 role value) is missing on this database, retry with the 0013 shape. */
async function tolerantUpdate(table: 'company_memberships' | 'companies', id: string, full: Record<string, unknown>, base: Record<string, unknown>) {
  const admin = createAdminClient()
  const first = await admin.from(table).update(full).eq('id', id)
  if (!first.error) return { ok: true, degraded: false }
  const pre0021 = isMissingColumn(first.error, ...MIGRATION_0021_COLUMNS) || /company_memberships_role_check/.test(first.error.message)
  if (!pre0021) return { ok: false, error: first.error.message }
  const second = await admin.from(table).update(base).eq('id', id)
  if (second.error) return { ok: false, error: second.error.message }
  return { ok: true, degraded: true }
}

/* ---------------- read ---------------- */

export interface PersonRow {
  membershipId: string
  userId: string
  name: string
  email: string
  phone: string | null
  role: string
  roleLabel: string
  relationship: 'pending' | 'verified' | 'rejected' | 'revoked'
  permission: CompanyPermissionLevel
  requestedAt: string
  approvedAt: string | null
  approvedBy: string | null
  rejectedAt: string | null
  rejectionReason: string | null
  revokedAt: string | null
  revokedBy: string | null
  /** The user's own words from the claim (MC typed, method), when there is one. */
  claimMethod: string | null
  /** Active membership_roles.role_type values behind this relationship (D15: the carrier title lives here). */
  roleTypes: RoleType[]
  /** The title to show — "Safety Manager", "Carrier Dispatcher" — null when no membership_roles row exists yet. */
  roleTypeLabel: string | null
}

/** The membership_roles row a carrier invite / approval writes when nobody chose a title (D15 default). */
export function defaultRoleTypeFor(company: Pick<Company, 'company_type'>): RoleType {
  return company.company_type === 'carrier' ? 'carrier_dispatcher' : 'freight_broker'
}

/** D15: a carrier company hands out the five dispatcher-power titles and Carrier Driver; anything else is refused. */
export function roleTypeAllowedFor(company: Pick<Company, 'company_type'>, roleType: RoleType): boolean {
  return company.company_type === 'carrier' ? CARRIER_COMPANY_ROLES.includes(roleType) : roleType === 'freight_broker'
}

export interface CarrierCompanyInfo {
  /** False until migration 0013 is applied. */
  available: boolean
  view: CompanyInfoView
  isPlatformAdmin: boolean
  canManage: boolean
  company: Company | null
  membership: CompanyMembership | null
  claim: CompanyClaim | null
  /** For the pending screen: where the request went and when it expires. */
  pendingInfo: { sentTo: string | null; requestedAt: string; expiresAt: string | null; method: string | null } | null
  /** Management view only. */
  pendingRequests: PersonRow[]
  verifiedDispatchers: PersonRow[]
  pastRelationships: PersonRow[]
  audit: CompanyAuditEvent[]
  verificationRecords: CompanyVerificationRecord[]
  /** HeavyHaul admin only: every carrier company, for the support picker. */
  adminCompanies: { id: string; display_name: string; mc_number: string | null; dot_number: string | null }[]
  /** Company Admin invitations still waiting for the person to sign up (migration 0024). */
  invitations: InvitedPerson[]
}

const EMPTY: CarrierCompanyInfo = {
  available: true, view: 'none', isPlatformAdmin: false, canManage: false, company: null, membership: null, claim: null,
  pendingInfo: null, pendingRequests: [], verifiedDispatchers: [], pastRelationships: [], audit: [], verificationRecords: [], adminCompanies: [], invitations: [],
}

export async function loadCarrierCompanyInfo(user: SessionUser, opts: { companyId?: string | null } = {}): Promise<CarrierCompanyInfo> {
  const admin = createAdminClient()
  const isPlatformAdmin = user.role === 'admin'

  if (isPlatformAdmin) {
    const list = await admin
      .from('companies')
      .select('id, display_name, mc_number, dot_number')
      .eq('company_type', 'carrier')
      .is('merged_into', null)
      .order('display_name')
    if (list.error) return { ...EMPTY, available: false, isPlatformAdmin: true }
    const adminCompanies = (list.data ?? []) as CarrierCompanyInfo['adminCompanies']
    const chosenId = opts.companyId && adminCompanies.some((c) => c.id === opts.companyId) ? opts.companyId : adminCompanies[0]?.id ?? null
    if (!chosenId) return { ...EMPTY, isPlatformAdmin: true, view: 'company_admin', canManage: true, adminCompanies }
    const { data: company } = await admin.from('companies').select('*').eq('id', chosenId).maybeSingle()
    const management = await loadManagement(chosenId)
    return {
      ...EMPTY, isPlatformAdmin: true, view: 'company_admin', canManage: true, adminCompanies,
      company: (company ?? null) as Company | null, ...management,
    }
  }

  let ctx = await getCompanyContext(user.id)
  if (!ctx.available) return { ...EMPTY, available: false }
  // Invited by a Company Admin before having an account? Verified now (2026-09-19).
  if (ctx.membership?.status !== 'approved' && (await claimCompanyInvitation(user, 'carrier'))) ctx = await getCompanyContext(user.id)
  const membership = ctx.membership
  const view = companyInfoView({ membership, viewerIsPlatformAdmin: false })
  const company = ctx.company
  const canManage = !!company && canManageCarrierCompany({ membership, companyId: company.id, viewerIsPlatformAdmin: false })
  const pendingInfo =
    view === 'pending' && membership
      ? {
          sentTo: ctx.claim?.approval_sent_to ?? company?.corporate_email ?? null,
          requestedAt: membership.requested_at ?? ctx.claim?.created_at ?? membership.created_at,
          expiresAt: ctx.claim?.expires_at ?? null,
          method: ctx.claim?.verification_method ?? null,
        }
      : null
  const management = canManage && company ? await loadManagement(company.id) : {}
  return { ...EMPTY, view, company, membership, claim: ctx.claim, canManage, pendingInfo, ...management }
}

export async function loadManagement(companyId: string, roleLabel: (r: CompanyMembership['role']) => string = carrierRoleLabel): Promise<Pick<CarrierCompanyInfo, 'pendingRequests' | 'verifiedDispatchers' | 'pastRelationships' | 'audit' | 'verificationRecords' | 'invitations'>> {
  const admin = createAdminClient()
  const [{ memberships, claims, profiles }, audit, records, open, roleRows] = await Promise.all([
    listCompanyMembers(companyId),
    listCompanyAudit(companyId, 60),
    admin.from('company_verification_records').select('*').eq('company_id', companyId).order('created_at', { ascending: false }).limit(20),
    admin.from('company_invitations').select('*').eq('company_id', companyId).eq('status', 'invited').order('created_at', { ascending: false }).limit(300),
    // D15: the member's title is the membership_roles row, not the legacy role column (missing table → no titles).
    admin.from('membership_roles').select('membership_id, role_type').eq('company_id', companyId).eq('status', 'active').order('added_at', { ascending: true }),
  ])
  const rolesByMembership = new Map<string, RoleType[]>()
  for (const r of (roleRows.data ?? []) as Array<{ membership_id: string; role_type: RoleType }>) {
    rolesByMembership.set(r.membership_id, [...(rolesByMembership.get(r.membership_id) ?? []), r.role_type])
  }
  const latestClaim = new Map<string, CompanyClaim>()
  for (const c of claims) if (!latestClaim.has(c.user_id)) latestClaim.set(c.user_id, c)
  const rows: PersonRow[] = memberships.map((m) => {
    const p = profiles.get(m.user_id)
    const claim = latestClaim.get(m.user_id)
    const rel = relationshipStatus(m.status)
    const roleTypes = rolesByMembership.get(m.id) ?? []
    return {
      membershipId: m.id,
      userId: m.user_id,
      name: p?.full_name || '—',
      email: p?.email || '—',
      phone: p?.phone ?? null,
      role: m.role,
      roleLabel: roleLabel(m.role),
      relationship: rel === 'none' ? 'pending' : rel,
      permission: permissionLevel(m),
      requestedAt: m.requested_at ?? claim?.created_at ?? m.created_at,
      approvedAt: m.approved_at,
      approvedBy: m.approved_by,
      rejectedAt: m.rejected_at ?? null,
      rejectionReason: m.rejection_reason ?? null,
      revokedAt: m.revoked_at,
      revokedBy: m.revoked_by,
      claimMethod: claim?.verification_method ?? null,
      roleTypes,
      roleTypeLabel: roleTypes.length ? roleTypes.map((r) => ROLE_LABELS[r]).join(' + ') : null,
    }
  })
  return {
    // 0022 statuses (historical_pending_confirmation, no_longer_current, manual_review) read as 'pending' here
    // but are listed separately by the broker loader; keep them out of the approve queue.
    pendingRequests: rows.filter((r) => r.relationship === 'pending' && memberships.find((m) => m.id === r.membershipId)?.status === 'pending'),
    verifiedDispatchers: rows.filter((r) => r.relationship === 'verified'),
    pastRelationships: rows.filter((r) => r.relationship === 'rejected' || r.relationship === 'revoked'),
    audit,
    verificationRecords: (records.data ?? []) as CompanyVerificationRecord[],
    invitations: ((open.data ?? []) as CompanyInvitation[]).map((i) => ({ id: i.id, name: i.name, email: i.email, invitedAt: i.created_at })),
  }
}

/* ---------------- email events ---------------- */

/**
 * Raise a company email event. The copy is the admin-managed template of the
 * same key (DB override → shipped default); delivery is the email seam
 * (recorded when no provider is configured). Every raise lands in
 * email_events so support can see what went out.
 */
export async function emitCompanyEmail(params: {
  /** Template key — the admin-managed copy of the same name. */
  event: string
  to: string[]
  data: Record<string, string>
  companyId: string
  userId?: string | null
  actorUserId?: string | null
  /** Drop lines with missing variables (broker verification emails, §31). */
  clean?: boolean
}) {
  // One central sender (2026-09-22): stream, preferences, suppression, log — all decided there.
  const recipients = [...new Set(params.to.map((e) => e.trim().toLowerCase()).filter((e) => e.includes('@')))]
  const results: { to: string; status: string }[] = []
  for (const to of recipients) {
    const r = await sendPlatformEmail({ templateKey: params.event, to, data: params.data, userId: params.userId, actorUserId: params.actorUserId, clean: params.clean !== false, tags: { company: params.companyId } })
    results.push({ to, status: r.status })
  }
  return results
}

/** Who gets "a dispatcher is asking to join": verified Company Admins, else the official company email. */
export async function companyAdminRecipients(company: Company): Promise<string[]> {
  const { memberships, profiles } = await listCompanyMembers(company.id)
  const admins = memberships
    .filter((m) => m.status === 'approved' && permissionLevel(m) === 'company_admin')
    .map((m) => profiles.get(m.user_id)?.email)
    .filter((e): e is string => !!e)
  if (admins.length > 0) return admins
  return company.corporate_email ? [company.corporate_email] : []
}

/* ---------------- writes ---------------- */

interface Actor { id: string; label: string; email: string; isPlatformAdmin: boolean; ip: string | null }

/**
 * Company Admin invites a person who already has a HeavyHaul Agent account
 * (Nash, 2026-09-19: "all I need to put is his email and his name and hit
 * invite… a verified relation"). The membership is verified right now —
 * company_admin_invitation / COMPANY_ADMIN — and the invitation template
 * for this company type goes out. Someone without an account is reported
 * as `noAccount` so the caller can hold an invited contact (brokers) or say so.
 */
export async function inviteExistingAccount(params: {
  company: Company
  name: string
  email: string
  makeAdmin: boolean
  actor: { id: string; label: string; email: string; isPlatformAdmin: boolean; ip: string | null }
  /** Bulk invites use their own template. */
  templateKey?: string
  /** D15: the membership_roles title the person gets (carrier: dispatcher-power roles or Carrier Driver). Omitted → no role row is written, as before. */
  roleType?: RoleType
}): Promise<{ ok: true; outcome: 'verified'; name: string } | { ok: false; error: string; noAccount?: boolean }> {
  const { company, actor } = params
  if (params.roleType && !roleTypeAllowedFor(company, params.roleType)) return { ok: false, error: `${ROLE_LABELS[params.roleType]} is not a role this company hands out.` }
  const email = params.email.trim().toLowerCase()
  const name = params.name.trim()
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return { ok: false, error: 'Enter a valid email address.' }
  if (email === actor.email.trim().toLowerCase()) return { ok: false, error: 'That is you.' }
  const flow = flowFor(company)
  const admin = createAdminClient()
  const { data: profile } = await admin.from('profiles').select('id, full_name, email').ilike('email', emailPattern(email)).maybeSingle()
  if (!profile) return { ok: false, error: 'No HeavyHaul Agent account uses this email yet.', noAccount: true }
  const { data: existing } = await admin.from('company_memberships').select('id, status').eq('user_id', profile.id).eq('company_id', company.id).maybeSingle()
  if (existing?.status === 'approved') return { ok: false, error: `${profile.full_name || email} is already a verified member of ${company.display_name}.` }
  const now = new Date().toISOString()
  const permission = params.makeAdmin ? 'company_admin' : 'member'
  const base = { user_id: profile.id, company_id: company.id, role: flow.memberRole, status: 'approved', approved_by: actor.label, approved_at: now, revoked_by: null, revoked_at: null }
  const full = { ...base, permission_level: permission, approved_by_user_id: actor.id, confirmed_at: now, requested_at: now, verification_method: 'company_admin_invitation', verification_provider: 'COMPANY_ADMIN', updated_at: now, rejected_at: null, rejected_by: null, rejection_reason: null }
  let up = await admin.from('company_memberships').upsert(full, { onConflict: 'user_id,company_id' })
  // Columns from 0021 missing → write the 0013 shape. The role value itself needs 0037.
  if (up.error && /column|schema cache/i.test(up.error.message)) up = await admin.from('company_memberships').upsert(base, { onConflict: 'user_id,company_id' })
  if (up.error) return { ok: false, error: /constraint/i.test(up.error.message) ? 'Company relationships need database migration 0037.' : up.error.message }
  // D15: the chosen title is a membership_roles row on the (now verified) relationship.
  if (params.roleType) {
    const { data: membership } = await admin.from('company_memberships').select('id').eq('user_id', profile.id).eq('company_id', company.id).maybeSingle()
    if (membership) await addMembershipRole({ membershipId: (membership as { id: string }).id, userId: profile.id, companyId: company.id, roleType: params.roleType, actor: { id: actor.id, label: actor.label } })
  }
  await admin.from('company_claims').update({ claim_status: 'approved', approver_name: actor.label, approver_email: actor.email, approver_ip: actor.ip, decided_at: now, updated_at: now })
    .eq('user_id', profile.id).eq('company_id', company.id).not('claim_status', 'in', '("approved","rejected","revoked")')
  await markVerificationResult(profile.id, company.id, 'approved')
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: flow.audit.invited,
    oldValue: existing ? { membership_id: existing.id, status: existing.status } : null,
    newValue: { user_id: profile.id, email, status: 'approved', permission_level: permission, role_type: params.roleType ?? null, via: 'company_admin_invitation', by_platform_admin: actor.isPlatformAdmin },
  })
  const [first, ...rest] = name.split(/\s+/)
  await emitCompanyEmail({
    event: params.templateKey ?? flow.events.invitation, to: [email], companyId: company.id, userId: profile.id, actorUserId: actor.id, clean: true,
    data: companyData(company, { requester_first_name: first ?? '', requester_last_name: rest.join(' '), requester_full_name: name, requester_email: email, user_name: name || email, user_email: email, invited_by: actor.label, invited_by_email: actor.email, invited_by_phone: await actorPhone(actor.id), requested_role: params.roleType ? ROLE_LABELS[params.roleType] : flow.memberNoun, mc_number: company.mc_number ?? '', usdot_number: company.dot_number ?? '' }),
  })
  return { ok: true, outcome: 'verified', name: profile.full_name || name }
}

/** The inviter's phone from their profile — credibility for the recipient (Nash: "I know this guy, he's a real person"). */
export async function actorPhone(userId: string): Promise<string> {
  const admin = createAdminClient()
  const { data } = await admin.from('profiles').select('phone').eq('id', userId).maybeSingle()
  return data?.phone ?? ''
}

/**
 * Invite by email for ANY company type: account → verified right now; no
 * account → held in company_invitations (0024) and verified the first time
 * that email signs in. Either way the invitation email goes out.
 */
export async function inviteOrHold(params: {
  company: Company
  name: string
  email: string
  makeAdmin: boolean
  actor: { id: string; label: string; email: string; isPlatformAdmin: boolean; ip: string | null }
  templateKey?: string
  /**
   * D15: the title for the new member. Persisted only when the account exists;
   * company_invitations has no role column (and this task adds no schema), so a
   * held invitation is claimed with the company default.
   */
  roleType?: RoleType
}): Promise<{ ok: true; outcome: 'verified' | 'invited'; name: string } | { ok: false; error: string }> {
  const { company, actor } = params
  if (params.roleType && !roleTypeAllowedFor(company, params.roleType)) return { ok: false, error: `${ROLE_LABELS[params.roleType]} is not a role this company hands out.` }
  const viaAccount = await inviteExistingAccount(params)
  if (viaAccount.ok) return viaAccount
  if (!viaAccount.noAccount) return { ok: false, error: viaAccount.error }
  const email = params.email.trim().toLowerCase()
  const name = params.name.trim()
  const flow = flowFor(company)
  const admin = createAdminClient()
  const now = new Date().toISOString()
  const row = {
    company_id: company.id, company_type: company.company_type, email: params.email.trim(), email_normalized: email, name: name || null,
    permission_level: params.makeAdmin ? 'company_admin' : 'member', invited_by_user_id: actor.id, invited_by_label: actor.label,
    template_key: params.templateKey ?? flow.events.invitation, status: 'invited', updated_at: now,
  }
  const { data: open, error: findErr } = await admin.from('company_invitations').select('id').eq('company_id', company.id).eq('email_normalized', email).eq('status', 'invited').maybeSingle()
  if (findErr && /does not exist|schema cache/i.test(findErr.message)) {
    return { ok: false, error: 'Inviting someone without a HeavyHaul Agent account needs database migration 0024. People who already have an account can be invited now.' }
  }
  const w = open ? await admin.from('company_invitations').update(row).eq('id', open.id) : await admin.from('company_invitations').insert(row)
  if (w.error) return { ok: false, error: w.error.message }
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: flow.audit.invited,
    newValue: { email, name: name || null, status: 'invited_no_account', permission_level: row.permission_level, role_type: params.roleType ?? null, via: 'company_admin_invitation', resent: !!open, by_platform_admin: actor.isPlatformAdmin },
  })
  const [first, ...rest] = name.split(/\s+/)
  await emitCompanyEmail({
    event: row.template_key, to: [email], companyId: company.id, actorUserId: actor.id, clean: true,
    data: companyData(company, { requester_first_name: first ?? '', requester_last_name: rest.join(' '), requester_full_name: name, requester_email: email, user_name: name || email, user_email: email, invited_by: actor.label, invited_by_email: actor.email, invited_by_phone: await actorPhone(actor.id), requested_role: params.roleType ? ROLE_LABELS[params.roleType] : flow.memberNoun, mc_number: company.mc_number ?? '', usdot_number: company.dot_number ?? '' }),
  })
  return { ok: true, outcome: 'invited', name: name || email }
}

/** Bulk invite for any company type — one address at a time, same template, per-address report. */
export async function bulkInviteMembers(params: {
  company: Company
  emails: string[]
  makeAdmin: boolean
  actor: { id: string; label: string; email: string; isPlatformAdmin: boolean; ip: string | null }
  /** Per-address invite; defaults to inviteOrHold. Brokers pass their own (historical contacts). */
  inviteOne?: (email: string) => Promise<{ ok: true; outcome: 'verified' | 'invited'; name: string } | { ok: false; error: string }>
  /** D15: one title for the whole batch (carrier page). */
  roleType?: RoleType
}) {
  const flow = flowFor(params.company)
  const one = params.inviteOne ?? ((email: string) => inviteOrHold({ company: params.company, name: '', email, makeAdmin: params.makeAdmin, actor: params.actor, templateKey: flow.events.bulkInvitation, roleType: params.roleType }))
  const results: { email: string; outcome: 'verified' | 'invited' | 'skipped'; note?: string }[] = []
  for (const email of params.emails.slice(0, BULK_INVITE_LIMIT)) {
    const r = await one(email)
    results.push(r.ok ? { email, outcome: r.outcome } : { email, outcome: 'skipped', note: r.error })
  }
  await logCompanyEvent({
    companyId: params.company.id, actorUserId: params.actor.id, actorLabel: params.actor.label, ip: params.actor.ip, eventType: flow.audit.bulkInvited,
    newValue: { count: results.length, verified: results.filter((x) => x.outcome === 'verified').length, invited: results.filter((x) => x.outcome === 'invited').length, skipped: results.filter((x) => x.outcome === 'skipped').length, role_type: params.roleType ?? null, by_platform_admin: params.actor.isPlatformAdmin },
  })
  return { ok: true as const, results }
}

/** First sign-in of someone a Company Admin invited: the membership they were promised. Returns true when something was claimed. */
export async function claimCompanyInvitation(user: SessionUser, companyType: 'broker' | 'carrier'): Promise<boolean> {
  const admin = createAdminClient()
  const email = user.email.trim().toLowerCase()
  const { data: inv, error } = await admin.from('company_invitations').select('*').eq('email_normalized', email).eq('status', 'invited').eq('company_type', companyType).order('created_at', { ascending: false }).limit(1).maybeSingle()
  if (error || !inv) return false
  const invitation = inv as CompanyInvitation
  const { data: company } = await admin.from('companies').select('*').eq('id', invitation.company_id).maybeSingle()
  if (!company) return false
  const flow = flowFor(company as Company)
  const now = new Date().toISOString()
  const base = { user_id: user.id, company_id: invitation.company_id, role: flow.memberRole, status: 'approved', approved_by: invitation.invited_by_label, approved_at: now, revoked_by: null, revoked_at: null }
  const full = { ...base, permission_level: invitation.permission_level, approved_by_user_id: invitation.invited_by_user_id, confirmed_at: now, requested_at: invitation.created_at, verification_method: 'company_admin_invitation', verification_provider: 'COMPANY_ADMIN', updated_at: now }
  let up = await admin.from('company_memberships').upsert(full, { onConflict: 'user_id,company_id' })
  if (up.error && /column|schema cache/i.test(up.error.message)) up = await admin.from('company_memberships').upsert(base, { onConflict: 'user_id,company_id' })
  if (up.error) return false
  await admin.from('company_invitations').update({ status: 'claimed', claimed_user_id: user.id, claimed_at: now, updated_at: now }).eq('id', invitation.id)
  await admin.from('company_claims').update({ claim_status: 'approved', decided_at: now, updated_at: now, approver_name: invitation.invited_by_label })
    .eq('user_id', user.id).eq('company_id', invitation.company_id).not('claim_status', 'in', '("approved","rejected","revoked")')
  await logCompanyEvent({
    companyId: invitation.company_id, actorUserId: user.id, actorLabel: user.name || user.email, eventType: COMPANY_AUDIT.invitationClaimed,
    oldValue: { invitation_id: invitation.id, status: 'invited' }, newValue: { invitation_id: invitation.id, user_id: user.id, status: 'approved', permission_level: invitation.permission_level, invited_by: invitation.invited_by_label },
  })
  return true
}

/** Close out the verification communication log for this person + company (0022; tolerant when the table is missing). */
export async function markVerificationResult(userId: string, companyId: string, result: 'approved' | 'rejected' | 'cancelled' | 'expired' | 'reported') {
  const admin = createAdminClient()
  const { error } = await admin.from('verification_emails')
    .update({ final_result: result, decided_at: new Date().toISOString(), updated_at: new Date().toISOString() })
    .eq('requester_user_id', userId).eq('company_id', companyId).eq('final_result', 'pending')
  if (error && !/does not exist|schema cache/i.test(error.message)) console.error('verification_emails update failed', error.message)
}

export function fmtDate(iso: string): string {
  return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}

export async function personFor(userId: string) {
  const admin = createAdminClient()
  const { data } = await admin.from('profiles').select('full_name, email, phone').eq('id', userId).maybeSingle()
  return { name: data?.full_name || '—', email: data?.email || '', phone: data?.phone ?? '' }
}

async function membershipFor(membershipId: string, companyId: string): Promise<CompanyMembership | null> {
  const admin = createAdminClient()
  const { data } = await admin.from('company_memberships').select('*').eq('id', membershipId).eq('company_id', companyId).maybeSingle()
  return (data ?? null) as CompanyMembership | null
}

function companyData(company: Company, extra: Record<string, string>) {
  return { company_name: company.display_name, company_mc: company.mc_number ?? '—', company_dot: company.dot_number ?? '—', ...extra }
}

export async function approveDispatcher(params: { company: Company; membershipId: string; actor: Actor; /** D15: the title the approver gives (carrier); default Carrier Dispatcher / Freight Broker. */ roleType?: RoleType }) {
  const { company, actor } = params
  const roleType = params.roleType ?? defaultRoleTypeFor(company)
  if (!roleTypeAllowedFor(company, roleType)) return { ok: false as const, error: `${ROLE_LABELS[roleType]} is not a role this company hands out.` }
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Request not found.' }
  if (m.user_id === actor.id) return { ok: false as const, error: 'You cannot approve your own request.' }
  const t = membershipTransition(m.status, 'approve')
  if (!t.ok) return { ok: false as const, error: t.error }
  const now = new Date().toISOString()
  // A carrier relationship is always Carrier Dispatcher, whatever the legacy column says (0037 corrected the rows).
  const role = company.company_type === 'carrier' && m.role === 'freight_broker' ? 'carrier_dispatcher' : m.role
  const base = { status: 'approved', approved_by: actor.label, approved_at: now, revoked_by: null, revoked_at: null }
  const res = await tolerantUpdate('company_memberships', m.id, {
    ...base, role, approved_by_user_id: actor.id, verification_method: actor.isPlatformAdmin ? 'manual_review' : 'company_admin_approval',
    verification_provider: actor.isPlatformAdmin ? 'MANUAL_ADMIN' : 'COMPANY_ADMIN', updated_at: now,
  }, base)
  if (!res.ok) return { ok: false as const, error: res.error }
  const admin = createAdminClient()
  await admin.from('company_claims')
    .update({ claim_status: 'approved', approver_name: actor.label, approver_email: actor.email, approver_ip: actor.ip, decided_at: now, updated_at: now })
    .eq('user_id', m.user_id).eq('company_id', company.id)
    .not('claim_status', 'in', '("approved","rejected","revoked")')
  await raiseVerification(company.id, actor.isPlatformAdmin ? 'manual_verified' : 'company_admin_approved')
  const flow = flowFor(company)
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, eventType: flow.audit.approved, ip: actor.ip,
    oldValue: { membership_id: m.id, user_id: m.user_id, status: m.status },
    newValue: { membership_id: m.id, user_id: m.user_id, status: 'approved', role, role_type: roleType, permission_level: permissionLevel(m), approved_by: actor.label, by_platform_admin: actor.isPlatformAdmin },
  })
  await markVerificationResult(m.user_id, company.id, 'approved')
  // Multi-role accounts (2026-09-24): an approved relationship is what makes the
  // matching mode available. Nothing else changes for a single-role account.
  // D15: a carrier approver may give a title other than Carrier Dispatcher.
  await addMembershipRole({
    membershipId: m.id, userId: m.user_id, companyId: company.id,
    roleType,
    actor: { id: actor.id, label: actor.label },
  })
  const person = await personFor(m.user_id)
  await emitCompanyEmail({
    event: flow.events.approved, to: [person.email], companyId: company.id, userId: m.user_id, actorUserId: actor.id,
    data: { ...companyData(company, { user_name: person.name, user_email: person.email, requested_role: params.roleType ? ROLE_LABELS[roleType] : flow.roleLabel(role), decided_by: actor.label }) },
  })
  return { ok: true as const }
}

/**
 * D15 (Nash, 2026-09-30): change a verified carrier member's title — Dispatcher,
 * Safety Manager, Permit Manager, Accounting, Fleet Manager or Carrier Driver.
 * The relationship, its permission level and the legacy role column are
 * untouched; the chosen membership_roles row becomes active and the member's
 * other company-handed roles are retired (never deleted, §21). A dispatcher
 * who is also a driver keeps both only by being re-added as the other role.
 */
export async function setMemberRoleType(params: { company: Company; membershipId: string; roleType: RoleType; actor: Actor }) {
  const { company, actor, roleType } = params
  if (company.company_type !== 'carrier') return { ok: false as const, error: 'Member roles are chosen on carrier companies only.' }
  if (!roleTypeAllowedFor(company, roleType)) return { ok: false as const, error: `${ROLE_LABELS[roleType]} is not a role this company hands out.` }
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Member not found.' }
  if (m.status !== 'approved') return { ok: false as const, error: 'Only a verified member can be given a role.' }
  const admin = createAdminClient()
  const { data: current, error } = await admin.from('membership_roles').select('id, role_type').eq('membership_id', m.id).eq('status', 'active')
  if (error) return { ok: false as const, error: isMissingColumn(error) || /does not exist|schema cache/i.test(error.message) ? 'Roles need database migration 0034.' : error.message }
  const rows = (current ?? []) as Array<{ id: string; role_type: RoleType }>
  const previous = rows.map((r) => r.role_type)
  if (previous.length === 1 && previous[0] === roleType) return { ok: true as const, changed: false }
  const added = await addMembershipRole({ membershipId: m.id, userId: m.user_id, companyId: company.id, roleType, actor: { id: actor.id, label: actor.label } })
  if (!added.ok) return { ok: false as const, error: added.error }
  const now = new Date().toISOString()
  for (const r of rows) {
    if (r.role_type === roleType || !CARRIER_COMPANY_ROLES.includes(r.role_type)) continue
    await admin.from('membership_roles').update({ status: 'revoked', removed_at: now, removed_by: actor.label, removed_by_user_id: actor.id, updated_at: now }).eq('id', r.id)
    await logRoleAction({ userId: m.user_id, membershipRoleId: r.id, companyId: company.id, roleType: r.role_type, action: 'role_changed_by_company', previousStatus: 'active', newStatus: 'revoked', actor: { id: actor.id, label: actor.label }, detail: { replaced_by: roleType } })
  }
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: COMPANY_AUDIT.roleChanged,
    oldValue: { membership_id: m.id, user_id: m.user_id, role_types: previous },
    newValue: { membership_id: m.id, user_id: m.user_id, role_type: roleType, by_platform_admin: actor.isPlatformAdmin },
  })
  return { ok: true as const, changed: true }
}

export async function rejectDispatcher(params: { company: Company; membershipId: string; reason: RejectionReason; note: string | null; reasonLabel: string; actor: Actor }) {
  const { company, actor } = params
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Request not found.' }
  if (m.user_id === actor.id) return { ok: false as const, error: 'You cannot reject your own request.' }
  const t = membershipTransition(m.status, 'reject')
  if (!t.ok) return { ok: false as const, error: t.error }
  const now = new Date().toISOString()
  const base = { status: 'rejected' }
  const res = await tolerantUpdate('company_memberships', m.id, {
    ...base, rejected_at: now, rejected_by: actor.label, rejected_by_user_id: actor.id, rejection_reason: params.reason, rejection_note: params.note, updated_at: now,
  }, base)
  if (!res.ok) return { ok: false as const, error: res.error }
  await suspendCompanyRulesForMember({ userId: m.user_id, companyId: company.id, reason: 'membership_rejected', actor: { id: actor.id, label: actor.label } })
  const admin = createAdminClient()
  await admin.from('company_claims')
    .update({ claim_status: 'rejected', approver_name: actor.label, approver_email: actor.email, approver_ip: actor.ip, decided_at: now, updated_at: now, review_notes: params.note })
    .eq('user_id', m.user_id).eq('company_id', company.id)
    .not('claim_status', 'in', '("approved","rejected","revoked")')
  const flow = flowFor(company)
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, eventType: flow.audit.rejected, ip: actor.ip,
    oldValue: { membership_id: m.id, user_id: m.user_id, status: m.status },
    newValue: { membership_id: m.id, user_id: m.user_id, status: 'rejected', reason: params.reason, has_internal_note: !!params.note, by_platform_admin: actor.isPlatformAdmin },
  })
  await markVerificationResult(m.user_id, company.id, 'rejected')
  const person = await personFor(m.user_id)
  await emitCompanyEmail({
    event: flow.events.rejected, to: [person.email], companyId: company.id, userId: m.user_id, actorUserId: actor.id,
    data: { ...companyData(company, { user_name: person.name, user_email: person.email, requested_role: flow.roleLabel(m.role), decided_by: actor.label, rejection_reason: params.reasonLabel }) },
  })
  return { ok: true as const }
}

/** Revoke the relationship only — the user account stays intact. */
export async function revokeDispatcher(params: { company: Company; membershipId: string; actor: Actor; actorMembership: CompanyMembership | null }) {
  const { company, actor } = params
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Member not found.' }
  if (m.user_id === actor.id) return { ok: false as const, error: 'You cannot revoke your own relationship. Ask another admin.' }
  if (m.role === 'company_owner' && !actor.isPlatformAdmin && params.actorMembership?.role !== 'company_owner') {
    return { ok: false as const, error: 'Only the Company Owner or HeavyHaul Agent can remove the owner.' }
  }
  const t = membershipTransition(m.status, 'revoke')
  if (!t.ok) return { ok: false as const, error: t.error }
  const now = new Date().toISOString()
  const base = { status: 'revoked', revoked_by: actor.label, revoked_at: now }
  const res = await tolerantUpdate('company_memberships', m.id, { ...base, revoked_by_user_id: actor.id, updated_at: now }, base)
  if (!res.ok) return { ok: false as const, error: res.error }
  const admin = createAdminClient()
  await admin.from('company_claims').update({ claim_status: 'revoked', updated_at: now })
    .eq('user_id', m.user_id).eq('company_id', company.id).eq('claim_status', 'approved')
  const flow = flowFor(company)
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, eventType: flow.audit.revoked, ip: actor.ip,
    oldValue: { membership_id: m.id, user_id: m.user_id, status: 'approved', role: m.role, permission_level: permissionLevel(m) },
    newValue: { membership_id: m.id, user_id: m.user_id, status: 'revoked', by_platform_admin: actor.isPlatformAdmin },
  })
  // Keep the person's modes in step with the relationship (2026-09-24).
  await syncMembershipRoleStatus(m.id, 'revoked', { id: actor.id, label: actor.label })
  // §36: no more automatic company trips for someone who left (2026-09-30).
  await suspendCompanyRulesForMember({ userId: m.user_id, companyId: company.id, reason: 'membership_revoked', actor: { id: actor.id, label: actor.label } })
  const person = await personFor(m.user_id)
  await emitCompanyEmail({
    event: flow.events.revoked, to: [person.email], companyId: company.id, userId: m.user_id, actorUserId: actor.id,
    data: { ...companyData(company, { user_name: person.name, user_email: person.email, requested_role: flow.roleLabel(m.role), decided_by: actor.label }) },
  })
  return { ok: true as const }
}

/**
 * Reinstate a rejected or revoked member (Nash, 2026-09-22): "we should have
 * a way to reinstate any of the rejected members — only by the Company
 * Admin or the actual admin." The relationship becomes verified again; the
 * previous permission level stays as it was; the person is emailed.
 */
export async function reinstateMember(params: { company: Company; membershipId: string; actor: Actor }) {
  const { company, actor } = params
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Member not found.' }
  if (m.user_id === actor.id) return { ok: false as const, error: 'You cannot reinstate yourself. Ask another admin.' }
  const t = membershipTransition(m.status, 'reinstate')
  if (!t.ok) return { ok: false as const, error: t.error }
  const now = new Date().toISOString()
  const flow = flowFor(company)
  const base = { status: 'approved', approved_by: actor.label, approved_at: now, revoked_by: null, revoked_at: null }
  const res = await tolerantUpdate('company_memberships', m.id, {
    ...base, approved_by_user_id: actor.id, rejected_at: null, rejected_by: null, rejected_by_user_id: null, rejection_reason: null, rejection_note: null, revoked_by_user_id: null,
    verification_method: actor.isPlatformAdmin ? 'manual_review' : 'company_admin_approval', verification_provider: actor.isPlatformAdmin ? 'MANUAL_ADMIN' : 'COMPANY_ADMIN', updated_at: now,
  }, base)
  if (!res.ok) return { ok: false as const, error: res.error }
  const admin = createAdminClient()
  await admin.from('company_claims').update({ claim_status: 'approved', approver_name: actor.label, approver_email: actor.email, approver_ip: actor.ip, decided_at: now, updated_at: now })
    .eq('user_id', m.user_id).eq('company_id', company.id).in('claim_status', ['rejected', 'revoked'])
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, eventType: flow.audit.reinstated, ip: actor.ip,
    oldValue: { membership_id: m.id, user_id: m.user_id, status: m.status, rejection_reason: m.rejection_reason ?? null, revoked_by: m.revoked_by },
    newValue: { membership_id: m.id, user_id: m.user_id, status: 'approved', permission_level: permissionLevel(m), by_platform_admin: actor.isPlatformAdmin },
  })
  // Keep the person's modes in step with the relationship (2026-09-24).
  await syncMembershipRoleStatus(m.id, 'active', { id: actor.id, label: actor.label })
  const person = await personFor(m.user_id)
  await emitCompanyEmail({
    event: flow.events.approved, to: [person.email], companyId: company.id, userId: m.user_id, actorUserId: actor.id,
    data: companyData(company, { user_name: person.name, user_email: person.email, requested_role: flow.roleLabel(m.role), decided_by: actor.label }),
  })
  return { ok: true as const }
}

/** Member ⇄ Company Admin. Needs migration 0021 (the column is the permission). */
export async function setDispatcherPermission(params: { company: Company; membershipId: string; level: CompanyPermissionLevel; actor: Actor }) {
  const { company, actor } = params
  const m = await membershipFor(params.membershipId, company.id)
  if (!m) return { ok: false as const, error: 'Member not found.' }
  if (m.status !== 'approved') return { ok: false as const, error: 'Only a verified dispatcher can be given Company Admin permission.' }
  if (m.user_id === actor.id) return { ok: false as const, error: 'You cannot change your own permission. Ask another admin.' }
  if (m.role === 'company_owner') return { ok: false as const, error: 'The Company Owner is always a Company Admin.' }
  const before = permissionLevel(m)
  if (before === params.level) return { ok: true as const }
  const admin = createAdminClient()
  const { error } = await admin.from('company_memberships').update({ permission_level: params.level, updated_at: new Date().toISOString() }).eq('id', m.id)
  if (error) {
    if (isMissingColumn(error, 'permission_level')) return { ok: false as const, error: 'Permission levels need database migration 0021.' }
    return { ok: false as const, error: error.message }
  }
  await logCompanyEvent({
    companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip,
    eventType: params.level === 'company_admin' ? COMPANY_AUDIT.adminGranted : COMPANY_AUDIT.adminRemoved,
    oldValue: { membership_id: m.id, user_id: m.user_id, permission_level: before },
    newValue: { membership_id: m.id, user_id: m.user_id, permission_level: params.level, by_platform_admin: actor.isPlatformAdmin },
  })
  const flow = flowFor(company)
  if (params.level === 'company_admin' && flow.events.adminGranted) {
    const person = await personFor(m.user_id)
    await emitCompanyEmail({
      event: flow.events.adminGranted, to: [person.email], companyId: company.id, userId: m.user_id, actorUserId: actor.id,
      data: { ...companyData(company, { user_name: person.name, user_email: person.email, decided_by: actor.label, mc_number: company.mc_number ?? '' }) },
    })
  }
  return { ok: true as const }
}

/** Editable HeavyHaul Agent profile fields only — never the official identity. */
export async function updateCarrierProfile(params: { company: Company; patch: Partial<Record<EditableCompanyField, string | null>>; actor: Actor }) {
  const { company, actor } = params
  const old: Record<string, unknown> = {}
  const next: Record<string, unknown> = {}
  for (const [k, v] of Object.entries(params.patch)) {
    const current = (company as unknown as Record<string, unknown>)[k] ?? null
    const value = v === '' ? null : v
    if (current === value) continue
    old[k] = current
    next[k] = value
  }
  if (Object.keys(next).length === 0) return { ok: true as const, changed: [] as string[] }
  if (typeof next.display_name === 'string' && !next.display_name.trim()) return { ok: false as const, error: 'Display name cannot be empty.' }
  const now = new Date().toISOString()
  const base = Object.fromEntries(Object.entries(next).filter(([k]) => k === 'display_name' || k === 'logo_url'))
  const res = await tolerantUpdate('companies', company.id, { ...next, updated_at: now }, { ...base, updated_at: now })
  if (!res.ok) return { ok: false as const, error: res.error }
  if (res.degraded && Object.keys(next).some((k) => k !== 'display_name' && k !== 'logo_url')) {
    return { ok: false as const, error: 'Operational contact fields need database migration 0021. Display name was saved.' }
  }
  await logCompanyEvent({ companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: COMPANY_AUDIT.profileChanged, oldValue: old, newValue: next })
  return { ok: true as const, changed: Object.keys(next) }
}

/** HeavyHaul admin only: official email + its verification state, and the official-data source. */
export async function updateOfficialInfo(params: {
  company: Company
  patch: { official_company_email?: string | null; official_email_status?: OfficialEmailStatus; official_email_verification_source?: CompanyVerificationSource | null; verification_source?: CompanyVerificationSource | null }
  actor: Actor
}) {
  const { company, actor, patch } = params
  const now = new Date().toISOString()
  const admin = createAdminClient()
  if (patch.official_company_email !== undefined && (patch.official_company_email ?? null) !== (company.corporate_email ?? null)) {
    const { error } = await admin.from('companies').update({ corporate_email: patch.official_company_email || null, updated_at: now }).eq('id', company.id)
    if (error) return { ok: false as const, error: error.message }
    await logCompanyEvent({ companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: COMPANY_AUDIT.officialEmailChanged, oldValue: { official_company_email: company.corporate_email }, newValue: { official_company_email: patch.official_company_email || null } })
  }
  const emailState: Record<string, unknown> = {}
  if (patch.official_email_status !== undefined) {
    emailState.official_email_status = patch.official_email_status
    emailState.official_email_verified_at = patch.official_email_status === 'verified' ? now : null
  }
  if (patch.official_email_verification_source !== undefined) emailState.official_email_verification_source = patch.official_email_verification_source
  if (patch.verification_source !== undefined && patch.verification_source !== (company.verification_source ?? null)) {
    emailState.verification_source = patch.verification_source
    await logCompanyEvent({ companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: COMPANY_AUDIT.sourceChanged, oldValue: { verification_source: company.verification_source ?? null }, newValue: { verification_source: patch.verification_source } })
  }
  if (Object.keys(emailState).length > 0) {
    const { error } = await admin.from('companies').update({ ...emailState, updated_at: now }).eq('id', company.id)
    if (error) {
      if (isMissingColumn(error, ...MIGRATION_0021_COLUMNS)) return { ok: false as const, error: 'Official email verification fields need database migration 0021.' }
      return { ok: false as const, error: error.message }
    }
    if (patch.official_email_status !== undefined || patch.official_email_verification_source !== undefined) {
      await logCompanyEvent({ companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: COMPANY_AUDIT.officialEmailChanged, oldValue: { official_email_status: company.official_email_status ?? 'unverified', official_email_verification_source: company.official_email_verification_source ?? null }, newValue: emailState })
    }
  }
  return { ok: true as const }
}

/** A pending dispatcher nudges the company again (once per 24h). */
export async function remindCompany(params: { company: Company; membership: CompanyMembership; actor: Actor; extraData?: Record<string, string> }) {
  const { company, membership, actor } = params
  if (membership.status !== 'pending') return { ok: false as const, error: 'There is no pending request to resend.' }
  const recent = (await listCompanyAudit(company.id, 60)).find(
    (e) => e.event_type === COMPANY_AUDIT.reminderSent && e.actor_user_id === actor.id && Date.now() - new Date(e.created_at).getTime() < 86_400_000,
  )
  if (recent) return { ok: false as const, error: 'A reminder was already sent in the last 24 hours.' }
  const to = await companyAdminRecipients(company)
  if (to.length === 0) return { ok: false as const, error: 'This carrier has no verified admin or official email on file yet — contact support.' }
  const person = await personFor(actor.id)
  const flow = flowFor(company)
  const results = await emitCompanyEmail({
    event: flow.events.reminder, to, companyId: company.id, userId: actor.id, actorUserId: actor.id,
    data: { ...companyData(company, { user_name: person.name || actor.label, user_email: person.email || actor.email, user_phone: person.phone, requested_role: flow.roleLabel(membership.role), request_date: fmtDate(membership.requested_at ?? membership.created_at) }), ...(params.extraData ?? {}) },
    clean: company.company_type === 'broker',
  })
  await logCompanyEvent({ companyId: company.id, actorUserId: actor.id, actorLabel: actor.label, ip: actor.ip, eventType: flow.audit.reminderSent, newValue: { sent_to: to, results } })
  return { ok: true as const, sentTo: to, delivered: results.some((r) => r.status === 'sent') }
}

/** Raised when a dispatcher's request is created (claims route) — Company Admins hear about it. */
export async function notifyRelationshipRequested(params: { company: Company; userId: string; actorLabel: string; requestedAt: string; role: string }) {
  const flow = flowFor(params.company)
  const to = await companyAdminRecipients(params.company)
  await logCompanyEvent({ companyId: params.company.id, actorUserId: params.userId, actorLabel: params.actorLabel, eventType: flow.audit.requested, newValue: { user_id: params.userId, role: params.role, notified: to } })
  if (to.length === 0) return
  const person = await personFor(params.userId)
  await emitCompanyEmail({
    event: flow.events.requested, to, companyId: params.company.id, userId: params.userId, actorUserId: params.userId,
    data: { ...companyData(params.company, { user_name: person.name || params.actorLabel, user_email: person.email, user_phone: person.phone, requested_role: flow.roleLabel(params.role as CompanyMembership['role']), request_date: fmtDate(params.requestedAt) }) },
  })
}
