/**
 * Marketing & Leads Pipeline query helpers — marketing-leads-pipeline.
 *
 * All helpers are tenant-scoped: every statement carries a tenant_id WHERE clause.
 * Routes MUST NOT import raw Drizzle tables — they call these helpers.
 *
 * Zod validation schemas are re-exported here so route files can import them
 * via @zync/db/queries (no-raw-drizzle-from-routes policy).
 */
import { and, eq, isNull, isNotNull, desc, asc, sql, inArray, lt, lte, ne, count } from 'drizzle-orm'
import type { Db, DbTx } from '../client'
import { ilikeSubstringPattern } from '../utils/escape-like'
import {
  leads,
  leadActivities,
  leadForms,
  leadFormSubmissions,
  leadWebhooks,
  pipelineStages,
} from '../schema/marketing'
import { tenantSettings } from '../schema/tenants'
import { contracts } from '../schema/contracts'
import { invoices } from '../schema/invoices'
import { tasks } from '../schema/tasks'
import { proposals } from '../schema/proposals'
import { positionBetween, needsRebalance } from '../lib/fractional-index'
import { assertTenantOwnsOrThrow, assertActiveTenantAssignee } from './tenant-guards'
import type {
  CreateLeadInput,
  UpdateLeadInput,
  MoveLeadInput,
  AddLeadActivityInput,
  CreateLeadFormInput,
  UpdateLeadFormInput,
  PublicFormSubmitInput,
  CreateLeadWebhookInput,
  UpdateLeadWebhookInput,
  CreatePipelineStageInput,
  UpdatePipelineStageInput,
  LeadFiltersInput,
} from '../validation/marketing'
import { auditLog } from './_audit-forward'

// ── Domain object types ───────────────────────────────────────────────────────

export interface LeadObject {
  id: string
  tenantId: string
  name: string
  email: string | null
  phone: string | null
  company: string | null
  notes: string | null
  stage: string
  stagePosition: string
  source: string
  sourceMetadata: unknown
  assignedTo: string | null
  customerId: string | null
  contractId: string | null
  lostReason: string | null
  estimatedValue: string | null
  utmSource: string | null
  utmMedium: string | null
  utmCampaign: string | null
  utmContent: string | null
  utmTerm: string | null
  archivedAt: string | null
  createdAt: string
  updatedAt: string
  // lead-qualification-scoring (wave-14)
  score: number
  scoreUpdatedAt: string | null
  // lead-lost-re-engagement (wave-14)
  reengagementAt: string | null
  reengagementNotifiedAt: string | null
}

export interface LeadActivityObject {
  id: string
  tenantId: string
  leadId: string
  userId: string | null
  type: string
  content: string | null
  metadata: unknown
  createdAt: string
}

export interface LeadFormObject {
  id: string
  tenantId: string
  name: string
  slug: string
  fields: unknown
  redirectUrl: string | null
  notifyEmail: string | null
  isActive: boolean
  style: unknown
  createdBy: string
  submissionCount: number
  createdAt: string
  updatedAt: string
}

export interface LeadFormSubmissionObject {
  id: string
  tenantId: string
  formId: string
  leadId: string
  payload: unknown
  ip: string | null
  userAgent: string | null
  referrer: string | null
  utmSource: string | null
  utmMedium: string | null
  utmCampaign: string | null
  utmContent: string | null
  utmTerm: string | null
  createdAt: string
}

export interface LeadWebhookObject {
  id: string
  tenantId: string
  name: string
  source: string
  // secret is never returned to clients — omitted from serialization
  fieldMapping: unknown
  isActive: boolean
  lastReceivedAt: string | null
  createdAt: string
}

export interface PipelineStageObject {
  id: string
  tenantId: string
  name: string
  slug: string
  color: string | null
  position: string
  isSystem: boolean
  createdAt: string
}

export interface OverviewData {
  leadsThisMonth: number
  leadsLastMonth: number
  conversionRate: number | null
  pipelineValue: string
  byStage: Array<{ stage: string; count: number }>
  bySource: Array<{ source: string; count: number }>
  recentActivities: LeadActivityObject[]
}

// ── Serializers ───────────────────────────────────────────────────────────────

function serializeLead(row: typeof leads.$inferSelect): LeadObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    name: row.name,
    email: row.email ?? null,
    phone: row.phone ?? null,
    company: row.company ?? null,
    notes: row.notes ?? null,
    stage: row.stage,
    stagePosition: String(row.stagePosition),
    source: row.source,
    sourceMetadata: row.sourceMetadata ?? null,
    assignedTo: row.assignedTo ?? null,
    customerId: row.customerId ?? null,
    contractId: row.contractId ?? null,
    lostReason: row.lostReason ?? null,
    estimatedValue: row.estimatedValue ? String(row.estimatedValue) : null,
    utmSource: row.utmSource ?? null,
    utmMedium: row.utmMedium ?? null,
    utmCampaign: row.utmCampaign ?? null,
    utmContent: row.utmContent ?? null,
    utmTerm: row.utmTerm ?? null,
    archivedAt: row.archivedAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    // lead-qualification-scoring (wave-14)
    score: row.score ?? 0,
    scoreUpdatedAt: row.scoreUpdatedAt?.toISOString() ?? null,
    // lead-lost-re-engagement (wave-14)
    reengagementAt: row.reengagementAt?.toISOString() ?? null,
    reengagementNotifiedAt: row.reengagementNotifiedAt?.toISOString() ?? null,
  }
}

function serializeActivity(row: typeof leadActivities.$inferSelect): LeadActivityObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    leadId: row.leadId,
    userId: row.userId ?? null,
    type: row.type,
    content: row.content ?? null,
    metadata: row.metadata ?? null,
    createdAt: row.createdAt.toISOString(),
  }
}

function serializeForm(
  row: typeof leadForms.$inferSelect & { submissionCount?: number },
): LeadFormObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    name: row.name,
    slug: row.slug,
    fields: row.fields,
    redirectUrl: row.redirectUrl ?? null,
    notifyEmail: row.notifyEmail ?? null,
    isActive: row.isActive,
    style: row.style ?? null,
    createdBy: row.createdBy,
    submissionCount: row.submissionCount ?? 0,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
  }
}

function serializeWebhook(row: typeof leadWebhooks.$inferSelect): LeadWebhookObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    name: row.name,
    source: row.source,
    // secret intentionally omitted
    fieldMapping: row.fieldMapping ?? null,
    isActive: row.isActive,
    lastReceivedAt: row.lastReceivedAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
  }
}

function serializeSubmission(row: typeof leadFormSubmissions.$inferSelect): LeadFormSubmissionObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    formId: row.formId,
    leadId: row.leadId,
    payload: row.payload,
    ip: row.ip ?? null,
    userAgent: row.userAgent ?? null,
    referrer: row.referrer ?? null,
    utmSource: row.utmSource ?? null,
    utmMedium: row.utmMedium ?? null,
    utmCampaign: row.utmCampaign ?? null,
    utmContent: row.utmContent ?? null,
    utmTerm: row.utmTerm ?? null,
    createdAt: row.createdAt.toISOString(),
  }
}

function serializeStage(row: typeof pipelineStages.$inferSelect): PipelineStageObject {
  return {
    id: row.id,
    tenantId: row.tenantId,
    name: row.name,
    slug: row.slug,
    color: row.color ?? null,
    position: String(row.position),
    isSystem: row.isSystem,
    createdAt: row.createdAt.toISOString(),
  }
}

function toDateOrNull(value: Date | string | null | undefined): Date | null {
  if (value == null) return null
  return value instanceof Date ? value : new Date(value)
}

export type LeadRowRecord = typeof leads.$inferSelect

export interface UpsertLeadRowInput {
  id: string
  tenantId: string
  name: string
  email?: string | null
  stage?: string
  stagePosition?: string
  source?: string
  sourceMetadata?: unknown
  archivedAt?: Date | string | null
  createdAt?: Date | string
  updatedAt?: Date | string
}

export async function listLeadRowsForTenant(
  db: Db | DbTx,
  tenantId: string,
): Promise<LeadRowRecord[]> {
  return db.select().from(leads).where(eq(leads.tenantId, tenantId))
}

export async function upsertLeadRow(
  db: Db | DbTx,
  row: UpsertLeadRowInput,
): Promise<LeadRowRecord> {
  const [existing] = await db
    .select({ id: leads.id })
    .from(leads)
    .where(and(eq(leads.tenantId, row.tenantId), eq(leads.id, row.id)))
    .limit(1)

  if (existing) {
    const [updated] = await db
      .update(leads)
      .set({
        name: row.name,
        email: row.email ?? null,
        sourceMetadata: row.sourceMetadata ?? null,
        archivedAt: toDateOrNull(row.archivedAt),
        updatedAt: row.updatedAt ? new Date(row.updatedAt) : new Date(),
      })
      .where(and(eq(leads.tenantId, row.tenantId), eq(leads.id, row.id)))
      .returning()
    if (!updated) throw new Error('Lead not found after update')
    return updated
  }

  const [inserted] = await db
    .insert(leads)
    .values({
      id: row.id,
      tenantId: row.tenantId,
      name: row.name,
      email: row.email ?? null,
      stage: row.stage ?? 'NEW',
      stagePosition: row.stagePosition ?? '0',
      source: row.source ?? 'manual',
      sourceMetadata: row.sourceMetadata ?? null,
      archivedAt: toDateOrNull(row.archivedAt),
      createdAt: row.createdAt ? new Date(row.createdAt) : new Date(),
      updatedAt: row.updatedAt ? new Date(row.updatedAt) : new Date(),
    })
    .returning()
  if (!inserted) throw new Error('Lead not found after insert')
  return inserted
}

// ── Fractional position helpers ───────────────────────────────────────────────

/**
 * Rebalance all leads in a stage column to contiguous integers.
 * Called inside a transaction when gap < REBALANCE_THRESHOLD.
 */
export async function rebalanceLeadStage(
  tx: Db | DbTx,
  tenantId: string,
  stage: string,
): Promise<void> {
  const rows = await tx
    .select({ id: leads.id })
    .from(leads)
    .where(
      and(
        eq(leads.tenantId, tenantId),
        eq(leads.stage, stage),
        isNull(leads.archivedAt),
      ),
    )
    .orderBy(asc(leads.stagePosition))

  for (let i = 0; i < rows.length; i++) {
    const row = rows[i]
    if (!row) continue
    await tx
      .update(leads)
      .set({ stagePosition: String(i + 1), updatedAt: new Date() })
      .where(and(eq(leads.tenantId, tenantId), eq(leads.id, row.id)))
  }
}

// ── Pipeline stages ───────────────────────────────────────────────────────────

export async function listPipelineStages(
  db: Db,
  tenantId: string,
): Promise<PipelineStageObject[]> {
  const rows = await db
    .select()
    .from(pipelineStages)
    .where(eq(pipelineStages.tenantId, tenantId))
    .orderBy(asc(pipelineStages.position))
  return rows.map(serializeStage)
}

export async function createPipelineStage(
  db: Db,
  tenantId: string,
  input: CreatePipelineStageInput,
): Promise<PipelineStageObject> {
  const [row] = await db
    .insert(pipelineStages)
    .values({
      tenantId,
      name: input.name,
      slug: input.slug,
      color: input.color ?? null,
      position: String(input.position),
    })
    .returning()
  if (!row) throw new Error('Pipeline stage not found after insert')
  return serializeStage(row)
}

export async function updatePipelineStage(
  db: Db,
  tenantId: string,
  id: string,
  input: UpdatePipelineStageInput,
): Promise<PipelineStageObject> {
  const updates: Partial<typeof pipelineStages.$inferInsert> = {}
  if (input.name !== undefined) updates.name = input.name
  if (input.color !== undefined) updates.color = input.color ?? null
  if (input.position !== undefined) updates.position = String(input.position)

  const [row] = await db
    .update(pipelineStages)
    .set(updates)
    .where(and(eq(pipelineStages.tenantId, tenantId), eq(pipelineStages.id, id)))
    .returning()
  if (!row) throw new Error('Pipeline stage not found')
  return serializeStage(row)
}

export async function deletePipelineStage(
  db: Db,
  tenantId: string,
  id: string,
): Promise<void> {
  // System stages cannot be deleted
  const [stage] = await db
    .select({ isSystem: pipelineStages.isSystem })
    .from(pipelineStages)
    .where(and(eq(pipelineStages.tenantId, tenantId), eq(pipelineStages.id, id)))
  if (!stage) throw new Error('Stage not found')
  if (stage.isSystem) throw new Error('System pipeline stages cannot be deleted')

  await db
    .delete(pipelineStages)
    .where(and(eq(pipelineStages.tenantId, tenantId), eq(pipelineStages.id, id)))
}

// ── Leads CRUD ────────────────────────────────────────────────────────────────

export async function listLeads(
  db: Db,
  tenantId: string,
  filters: LeadFiltersInput,
): Promise<{ items: LeadObject[]; nextCursor: string | null; total: number }> {
  const conditions = [eq(leads.tenantId, tenantId)]

  if (!filters.include_archived) {
    conditions.push(isNull(leads.archivedAt))
  }
  if (!filters.include_lost) {
    conditions.push(ne(leads.stage, 'LOST'))
  }
  if (filters.stage) {
    const stages = filters.stage.split(',').filter(Boolean)
    if (stages.length === 1) {
      conditions.push(eq(leads.stage, stages[0]!))
    } else if (stages.length > 1) {
      conditions.push(inArray(leads.stage, stages))
    }
  }
  if (filters.source) {
    conditions.push(eq(leads.source, filters.source))
  }
  if (filters.assigned_to) {
    conditions.push(eq(leads.assignedTo, filters.assigned_to))
  }
  if (filters.q) {
    const pattern = ilikeSubstringPattern(filters.q)
    conditions.push(
      sql`(${leads.name} ILIKE ${pattern} OR ${leads.email} ILIKE ${pattern} OR ${leads.company} ILIKE ${pattern})`,
    )
  }
  if (filters.cursor) {
    try {
      const decoded = JSON.parse(Buffer.from(filters.cursor, 'base64url').toString())
      conditions.push(lt(leads.createdAt, new Date(decoded.createdAt)))
    } catch {
      // invalid cursor — ignore
    }
  }

  // lead-qualification-scoring (wave-14): min_score filter
  if (filters.min_score != null) {
    conditions.push(sql`${leads.score} >= ${filters.min_score}`)
  }

  const limit = filters.limit ?? 50
  const orderDir = filters.sort_dir === 'asc' ? asc : desc

  // score sort uses a raw SQL column reference so TS doesn't need to know about it at the type level
  let orderByExpr
  if (filters.sort_by === 'score') {
    orderByExpr = filters.sort_dir === 'asc' ? asc(leads.score) : desc(leads.score)
  } else {
    const orderCol = filters.sort_by === 'name'
      ? leads.name
      : filters.sort_by === 'stage'
        ? leads.stage
        : filters.sort_by === 'estimated_value'
          ? leads.estimatedValue
          : leads.createdAt
    orderByExpr = orderDir(orderCol)
  }

  const [rows, countResult] = await Promise.all([
    db
      .select()
      .from(leads)
      .where(and(...conditions))
      .orderBy(orderByExpr)
      .limit(limit + 1),
    db
      .select({ count: sql<number>`count(*)::int` })
      .from(leads)
      .where(and(...conditions)),
  ])

  const hasMore = rows.length > limit
  const items = rows.slice(0, limit).map(serializeLead)
  const lastItem = items[items.length - 1]
  const nextCursor =
    hasMore && lastItem
      ? Buffer.from(JSON.stringify({ createdAt: lastItem.createdAt })).toString('base64url')
      : null

  return {
    items,
    nextCursor,
    total: countResult[0]?.count ?? 0,
  }
}

export async function getLead(
  db: Db,
  tenantId: string,
  id: string,
): Promise<LeadObject | null> {
  const [row] = await db
    .select()
    .from(leads)
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id)))
  return row ? serializeLead(row) : null
}

export async function createLead(
  db: Db,
  tenantId: string,
  input: CreateLeadInput,
): Promise<LeadObject> {
  assertTenantOwnsOrThrow(
    'assigned_to',
    await assertActiveTenantAssignee(db, tenantId, input.assigned_to),
  )

  // Append to end of target stage column
  const [lastInStage] = await db
    .select({ pos: leads.stagePosition })
    .from(leads)
    .where(
      and(
        eq(leads.tenantId, tenantId),
        eq(leads.stage, input.stage ?? 'NEW'),
        isNull(leads.archivedAt),
      ),
    )
    .orderBy(desc(leads.stagePosition))
    .limit(1)

  const position = positionBetween(
    lastInStage ? Number(lastInStage.pos) : null,
    null,
  )

  const [row] = await db
    .insert(leads)
    .values({
      tenantId,
      name: input.name,
      email: input.email ?? null,
      phone: input.phone ?? null,
      company: input.company ?? null,
      notes: input.notes ?? null,
      stage: input.stage ?? 'NEW',
      stagePosition: String(position),
      source: input.source ?? 'manual',
      assignedTo: input.assigned_to ?? null,
      estimatedValue: input.estimated_value ?? null,
      utmSource: input.utm_source ?? null,
      utmMedium: input.utm_medium ?? null,
      utmCampaign: input.utm_campaign ?? null,
      utmContent: input.utm_content ?? null,
      utmTerm: input.utm_term ?? null,
    })
    .returning()
  if (!row) throw new Error('Lead not found after insert')
  return serializeLead(row)
}

export async function updateLead(
  db: Db,
  tenantId: string,
  id: string,
  input: UpdateLeadInput,
): Promise<LeadObject> {
  if (input.assigned_to !== undefined) {
    assertTenantOwnsOrThrow(
      'assigned_to',
      await assertActiveTenantAssignee(db, tenantId, input.assigned_to),
    )
  }

  const updates: Partial<typeof leads.$inferInsert> = { updatedAt: new Date() }
  if (input.name !== undefined) updates.name = input.name
  if (input.email !== undefined) updates.email = input.email ?? null
  if (input.phone !== undefined) updates.phone = input.phone ?? null
  if (input.company !== undefined) updates.company = input.company ?? null
  if (input.notes !== undefined) updates.notes = input.notes ?? null
  if (input.stage !== undefined) updates.stage = input.stage
  if (input.assigned_to !== undefined) updates.assignedTo = input.assigned_to ?? null
  if (input.estimated_value !== undefined) updates.estimatedValue = input.estimated_value ?? null
  if (input.lost_reason !== undefined) updates.lostReason = input.lost_reason ?? null
  // lead-lost-re-engagement (wave-14)
  if (input.reengagement_at !== undefined) {
    updates.reengagementAt = input.reengagement_at ? new Date(input.reengagement_at) : null
    // Clear notification sentinel whenever the date is (re)set so cron fires again
    if (input.reengagement_at !== null) {
      updates.reengagementNotifiedAt = null
    }
  }

  const [row] = await db
    .update(leads)
    .set(updates)
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id)))
    .returning()
  if (!row) throw new Error('Lead not found')
  return serializeLead(row)
}

export async function moveLead(
  db: Db | DbTx,
  tenantId: string,
  id: string,
  input: MoveLeadInput,
): Promise<LeadObject> {
  const position = positionBetween(
    input.before_position ?? null,
    input.after_position ?? null,
  )

  const [row] = await db
    .update(leads)
    .set({
      stage: input.stage,
      stagePosition: String(position),
      updatedAt: new Date(),
    })
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id)))
    .returning()
  if (!row) throw new Error('Lead not found')

  // Rebalance if needed
  if (
    needsRebalance(input.before_position ?? null, input.after_position ?? null)
  ) {
    await rebalanceLeadStage(db, tenantId, input.stage)
    const [rebalanced] = await db
      .select()
      .from(leads)
      .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id)))
    if (rebalanced) return serializeLead(rebalanced)
  }

  return serializeLead(row)
}

export async function archiveLead(
  db: Db,
  tenantId: string,
  id: string,
): Promise<void> {
  await db
    .update(leads)
    .set({ archivedAt: new Date(), updatedAt: new Date() })
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id), isNull(leads.archivedAt)))
}

/**
 * Convert a WON lead to customer. Sets customer_id on the lead record.
 * Actual customer creation is handled by the route (calls customers query fn).
 */
export async function linkLeadToCustomer(
  db: Db | DbTx,
  tenantId: string,
  leadId: string,
  customerId: string,
): Promise<LeadObject> {
  const [row] = await db
    .update(leads)
    .set({ customerId, updatedAt: new Date() })
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, leadId)))
    .returning()
  if (!row) throw new Error('Lead not found')
  return serializeLead(row)
}

// ── Lead Activities ───────────────────────────────────────────────────────────

export async function listLeadActivities(
  db: Db,
  tenantId: string,
  leadId: string,
  limit = 50,
  cursor?: string,
): Promise<{ items: LeadActivityObject[]; nextCursor: string | null }> {
  const conditions = [
    eq(leadActivities.tenantId, tenantId),
    eq(leadActivities.leadId, leadId),
  ]

  if (cursor) {
    try {
      const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString())
      conditions.push(lt(leadActivities.createdAt, new Date(decoded.createdAt)))
    } catch {
      // invalid cursor — ignore
    }
  }

  const rows = await db
    .select()
    .from(leadActivities)
    .where(and(...conditions))
    .orderBy(desc(leadActivities.createdAt))
    .limit(limit + 1)

  const hasMore = rows.length > limit
  const items = rows.slice(0, limit).map(serializeActivity)
  const lastItem = items[items.length - 1]
  const nextCursor =
    hasMore && lastItem
      ? Buffer.from(JSON.stringify({ createdAt: lastItem.createdAt })).toString('base64url')
      : null

  return { items, nextCursor }
}

export async function addLeadActivity(
  db: Db | DbTx,
  tenantId: string,
  leadId: string,
  userId: string | null,
  input: AddLeadActivityInput,
): Promise<LeadActivityObject> {
  const [row] = await db
    .insert(leadActivities)
    .values({
      tenantId,
      leadId,
      userId: userId ?? null,
      type: input.type,
      content: input.content ?? null,
      metadata: input.metadata ?? null,
    })
    .returning()
  if (!row) throw new Error('Activity not found after insert')
  return serializeActivity(row)
}

// ── Lead Forms ────────────────────────────────────────────────────────────────

export async function listLeadForms(
  db: Db,
  tenantId: string,
): Promise<LeadFormObject[]> {
  const rows = await db
    .select()
    .from(leadForms)
    .where(eq(leadForms.tenantId, tenantId))
    .orderBy(desc(leadForms.createdAt))

  // Get submission counts in a separate query
  const formIds = rows.map((r) => r.id)
  if (formIds.length === 0) return []

  const counts = await db
    .select({
      formId: leadFormSubmissions.formId,
      count: sql<number>`count(*)::int`,
    })
    .from(leadFormSubmissions)
    .where(inArray(leadFormSubmissions.formId, formIds))
    .groupBy(leadFormSubmissions.formId)

  const countMap = new Map(counts.map((c) => [c.formId, c.count]))

  return rows.map((r) => serializeForm({ ...r, submissionCount: countMap.get(r.id) ?? 0 }))
}

export async function getLeadForm(
  db: Db,
  tenantId: string,
  id: string,
): Promise<LeadFormObject | null> {
  const [row] = await db
    .select()
    .from(leadForms)
    .where(and(eq(leadForms.tenantId, tenantId), eq(leadForms.id, id)))
  if (!row) return null

  const [countResult] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(leadFormSubmissions)
    .where(eq(leadFormSubmissions.formId, id))

  return serializeForm({ ...row, submissionCount: countResult?.count ?? 0 })
}

export async function getLeadFormBySlug(
  db: Db,
  tenantId: string,
  slug: string,
): Promise<typeof leadForms.$inferSelect | null> {
  const [row] = await db
    .select()
    .from(leadForms)
    .where(
      and(
        eq(leadForms.tenantId, tenantId),
        eq(leadForms.slug, slug),
        eq(leadForms.isActive, true),
      ),
    )
  return row ?? null
}

export async function getLeadFormSubmissionCount(
  db: Db,
  formId: string,
): Promise<number> {
  const [result] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(leadFormSubmissions)
    .where(eq(leadFormSubmissions.formId, formId))
  return result?.count ?? 0
}

export async function listLeadFormSubmissions(
  db: Db,
  tenantId: string,
  formId: string,
  limit = 50,
  cursor?: string,
): Promise<{ items: LeadFormSubmissionObject[]; nextCursor: string | null; total: number }> {
  const conditions = [
    eq(leadFormSubmissions.tenantId, tenantId),
    eq(leadFormSubmissions.formId, formId),
  ]

  if (cursor) {
    try {
      const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString())
      conditions.push(lt(leadFormSubmissions.createdAt, new Date(decoded.createdAt)))
    } catch {
      // ignore invalid cursor
    }
  }

  const clampedLimit = Math.min(Math.max(limit, 1), 100)
  const [rows, countResult] = await Promise.all([
    db
      .select()
      .from(leadFormSubmissions)
      .where(and(...conditions))
      .orderBy(desc(leadFormSubmissions.createdAt))
      .limit(clampedLimit + 1),
    db
      .select({ count: sql<number>`count(*)::int` })
      .from(leadFormSubmissions)
      .where(and(eq(leadFormSubmissions.tenantId, tenantId), eq(leadFormSubmissions.formId, formId))),
  ])

  const hasMore = rows.length > clampedLimit
  const items = rows.slice(0, clampedLimit).map(serializeSubmission)
  const lastItem = items[items.length - 1]
  const nextCursor =
    hasMore && lastItem
      ? Buffer.from(JSON.stringify({ createdAt: lastItem.createdAt })).toString('base64url')
      : null

  return {
    items,
    nextCursor,
    total: countResult[0]?.count ?? 0,
  }
}

export async function createLeadForm(
  db: Db,
  tenantId: string,
  createdBy: string,
  input: CreateLeadFormInput,
): Promise<LeadFormObject> {
  const [row] = await db
    .insert(leadForms)
    .values({
      tenantId,
      createdBy,
      name: input.name,
      slug: input.slug,
      fields: input.fields,
      redirectUrl: input.redirect_url ?? null,
      notifyEmail: input.notify_email ?? null,
      style: input.style ?? null,
    })
    .returning()
  if (!row) throw new Error('Lead form not found after insert')
  return serializeForm({ ...row, submissionCount: 0 })
}

export async function updateLeadForm(
  db: Db,
  tenantId: string,
  id: string,
  input: UpdateLeadFormInput,
): Promise<LeadFormObject> {
  const updates: Partial<typeof leadForms.$inferInsert> = { updatedAt: new Date() }
  if (input.name !== undefined) updates.name = input.name
  if (input.slug !== undefined) updates.slug = input.slug
  if (input.fields !== undefined) updates.fields = input.fields
  if (input.redirect_url !== undefined) updates.redirectUrl = input.redirect_url ?? null
  if (input.notify_email !== undefined) updates.notifyEmail = input.notify_email ?? null
  if (input.style !== undefined) updates.style = input.style ?? null
  if (input.is_active !== undefined) updates.isActive = input.is_active

  const [row] = await db
    .update(leadForms)
    .set(updates)
    .where(and(eq(leadForms.tenantId, tenantId), eq(leadForms.id, id)))
    .returning()
  if (!row) throw new Error('Lead form not found')

  const [countResult] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(leadFormSubmissions)
    .where(eq(leadFormSubmissions.formId, id))

  return serializeForm({ ...row, submissionCount: countResult?.count ?? 0 })
}

export async function deleteLeadForm(
  db: Db,
  tenantId: string,
  id: string,
): Promise<void> {
  await db
    .delete(leadForms)
    .where(and(eq(leadForms.tenantId, tenantId), eq(leadForms.id, id)))
}

/**
 * Create a form submission and linked lead record atomically.
 * Returns both the submission and the new lead.
 */
export async function createFormSubmissionAndLead(
  db: Db,
  tenantId: string,
  form: typeof leadForms.$inferSelect,
  input: PublicFormSubmitInput,
  context: { ip?: string; userAgent?: string; referrer?: string },
): Promise<{ lead: LeadObject; submissionId: string }> {
  return db.transaction(async (tx) => {
    // Extract name/email/phone from payload using field config
    const fields = form.fields as Array<{
      id: string
      type: string
      label: string
      required: boolean
    }>

    let extractedName = '(unknown)'
    let extractedEmail: string | null = null
    let extractedPhone: string | null = null
    let extractedCompany: string | null = null

    for (const field of fields) {
      const val = input.payload[field.id]
      if (typeof val !== 'string' || !val.trim()) continue
      const lower = field.label.toLowerCase()
      if (lower.includes('name') && extractedName === '(unknown)') extractedName = val.trim()
      if (field.type === 'email' && !extractedEmail) extractedEmail = val.trim()
      if (field.type === 'phone' && !extractedPhone) extractedPhone = val.trim()
      if (lower.includes('company') && !extractedCompany) extractedCompany = val.trim()
    }

    // Append to end of NEW column
    const [lastInNew] = await tx
      .select({ pos: leads.stagePosition })
      .from(leads)
      .where(and(eq(leads.tenantId, tenantId), eq(leads.stage, 'NEW'), isNull(leads.archivedAt)))
      .orderBy(desc(leads.stagePosition))
      .limit(1)

    const position = positionBetween(lastInNew ? Number(lastInNew.pos) : null, null)

    const [lead] = await tx
      .insert(leads)
      .values({
        tenantId,
        name: extractedName,
        email: extractedEmail,
        phone: extractedPhone,
        company: extractedCompany,
        stage: 'NEW',
        stagePosition: String(position),
        source: 'form',
        utmSource: input.utm_source ?? null,
        utmMedium: input.utm_medium ?? null,
        utmCampaign: input.utm_campaign ?? null,
        utmContent: input.utm_content ?? null,
        utmTerm: input.utm_term ?? null,
      })
      .returning()
    if (!lead) throw new Error('Lead insert failed')

    const [submission] = await tx
      .insert(leadFormSubmissions)
      .values({
        tenantId,
        formId: form.id,
        leadId: lead.id,
        payload: input.payload,
        ip: context.ip ?? null,
        userAgent: context.userAgent ?? null,
        referrer: context.referrer ?? null,
        utmSource: input.utm_source ?? null,
        utmMedium: input.utm_medium ?? null,
        utmCampaign: input.utm_campaign ?? null,
        utmContent: input.utm_content ?? null,
        utmTerm: input.utm_term ?? null,
      })
      .returning()
    if (!submission) throw new Error('Submission insert failed')

    await tx.insert(leadActivities).values({
      tenantId,
      leadId: lead.id,
      userId: null,
      type: 'form_submitted',
      content: null,
      metadata: { formId: form.id, submissionId: submission.id },
    })

    await tx.insert(auditLog).values({
      tenantId,
      actorId: null,
      actorType: 'system',
      entityType: 'lead',
      entityId: lead.id,
      action: 'lead.created_via_form',
    })

    return { lead: serializeLead(lead), submissionId: submission.id }
  })
}

// ── Inbound Webhooks ──────────────────────────────────────────────────────────

export async function listLeadWebhooks(
  db: Db,
  tenantId: string,
): Promise<LeadWebhookObject[]> {
  const rows = await db
    .select()
    .from(leadWebhooks)
    .where(eq(leadWebhooks.tenantId, tenantId))
    .orderBy(desc(leadWebhooks.createdAt))
  return rows.map(serializeWebhook)
}

export async function getLeadWebhook(
  db: Db,
  tenantId: string,
  id: string,
): Promise<typeof leadWebhooks.$inferSelect | null> {
  const [row] = await db
    .select()
    .from(leadWebhooks)
    .where(and(eq(leadWebhooks.tenantId, tenantId), eq(leadWebhooks.id, id)))
  return row ?? null
}

export async function getLeadWebhookById(
  db: Db,
  id: string,
): Promise<typeof leadWebhooks.$inferSelect | null> {
  const [row] = await db
    .select()
    .from(leadWebhooks)
    .where(eq(leadWebhooks.id, id))
  return row ?? null
}

export async function createLeadWebhook(
  db: Db,
  tenantId: string,
  input: CreateLeadWebhookInput,
  encryptedSecret: string,
): Promise<LeadWebhookObject> {
  const [row] = await db
    .insert(leadWebhooks)
    .values({
      tenantId,
      name: input.name,
      source: input.source,
      secret: encryptedSecret,
      fieldMapping: input.field_mapping ?? null,
    })
    .returning()
  if (!row) throw new Error('Webhook not found after insert')
  return serializeWebhook(row)
}

export async function updateLeadWebhook(
  db: Db,
  tenantId: string,
  id: string,
  input: UpdateLeadWebhookInput,
  updatedSecret?: string,
): Promise<LeadWebhookObject> {
  const updates: Partial<typeof leadWebhooks.$inferInsert> = {}
  if (input.name !== undefined) updates.name = input.name
  if (updatedSecret) updates.secret = updatedSecret
  if (input.field_mapping !== undefined) updates.fieldMapping = input.field_mapping ?? null
  if (input.is_active !== undefined) updates.isActive = input.is_active

  const [row] = await db
    .update(leadWebhooks)
    .set(updates)
    .where(and(eq(leadWebhooks.tenantId, tenantId), eq(leadWebhooks.id, id)))
    .returning()
  if (!row) throw new Error('Webhook not found')
  return serializeWebhook(row)
}

export async function deleteLeadWebhook(
  db: Db,
  tenantId: string,
  id: string,
): Promise<void> {
  await db
    .delete(leadWebhooks)
    .where(and(eq(leadWebhooks.tenantId, tenantId), eq(leadWebhooks.id, id)))
}

export async function touchWebhookLastReceived(
  db: Db | DbTx,
  tenantId: string,
  id: string,
): Promise<void> {
  await db
    .update(leadWebhooks)
    .set({ lastReceivedAt: new Date() })
    .where(and(eq(leadWebhooks.tenantId, tenantId), eq(leadWebhooks.id, id)))
}

/**
 * Create a webhook-sourced lead.
 * field_mapping maps JSON paths → lead columns (name, email, phone, company, notes).
 */
export async function createWebhookLead(
  db: Db | DbTx,
  tenantId: string,
  webhook: typeof leadWebhooks.$inferSelect,
  rawPayload: Record<string, unknown>,
): Promise<LeadObject> {
  const fieldMapping = (webhook.fieldMapping ?? {}) as Record<string, string>

  // Simple dot-path resolver for nested JSON
  function resolvePath(obj: Record<string, unknown>, path: string): string | undefined {
    const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.')
    let current: unknown = obj
    for (const part of parts) {
      if (current == null || typeof current !== 'object') return undefined
      current = (current as Record<string, unknown>)[part]
    }
    return typeof current === 'string' ? current : undefined
  }

  const mapped: Record<string, string> = {}
  for (const [path, col] of Object.entries(fieldMapping)) {
    const val = resolvePath(rawPayload, path)
    if (val && col) mapped[col] = val
  }

  const [lastInNew] = await (db as Db)
    .select({ pos: leads.stagePosition })
    .from(leads)
    .where(and(eq(leads.tenantId, tenantId), eq(leads.stage, 'NEW'), isNull(leads.archivedAt)))
    .orderBy(desc(leads.stagePosition))
    .limit(1)

  const position = positionBetween(lastInNew ? Number(lastInNew.pos) : null, null)

  const [row] = await (db as Db)
    .insert(leads)
    .values({
      tenantId,
      name: mapped['name'] || '(unknown)',
      email: mapped['email'] ?? null,
      phone: mapped['phone'] ?? null,
      company: mapped['company'] ?? null,
      notes: mapped['notes'] ?? null,
      stage: 'NEW',
      stagePosition: String(position),
      source: webhook.source,
      sourceMetadata: rawPayload,
    })
    .returning()
  if (!row) throw new Error('Lead insert failed')
  return serializeLead(row)
}

// ── Overview / Analytics ──────────────────────────────────────────────────────

export async function getMarketingOverview(
  db: Db,
  tenantId: string,
): Promise<OverviewData> {
  const now = new Date()
  const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1)
  const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
  const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59)

  const [
    thisMonthCount,
    lastMonthCount,
    closedThisMonth,
    pipelineValueResult,
    byStageResult,
    bySourceResult,
    recentActivitiesResult,
  ] = await Promise.all([
    // Leads this month
    db
      .select({ count: sql<number>`count(*)::int` })
      .from(leads)
      .where(
        and(
          eq(leads.tenantId, tenantId),
          isNull(leads.archivedAt),
          sql`${leads.createdAt} >= ${thisMonthStart.toISOString()}::timestamptz`,
        ),
      ),
    // Leads last month
    db
      .select({ count: sql<number>`count(*)::int` })
      .from(leads)
      .where(
        and(
          eq(leads.tenantId, tenantId),
          isNull(leads.archivedAt),
          sql`${leads.createdAt} >= ${lastMonthStart.toISOString()}::timestamptz`,
          sql`${leads.createdAt} <= ${lastMonthEnd.toISOString()}::timestamptz`,
        ),
      ),
    // WON + LOST this month (for conversion rate)
    db
      .select({
        stage: leads.stage,
        count: sql<number>`count(*)::int`,
      })
      .from(leads)
      .where(
        and(
          eq(leads.tenantId, tenantId),
          sql`${leads.stage} IN ('WON','LOST')`,
          sql`${leads.updatedAt} >= ${thisMonthStart.toISOString()}::timestamptz`,
        ),
      )
      .groupBy(leads.stage),
    // Pipeline value (active, non-lost, non-archived)
    db
      .select({ total: sql<string>`COALESCE(SUM(estimated_value), 0)::text` })
      .from(leads)
      .where(
        and(
          eq(leads.tenantId, tenantId),
          isNull(leads.archivedAt),
          isNotNull(leads.estimatedValue),
          sql`${leads.stage} NOT IN ('LOST')`,
        ),
      ),
    // By stage
    db
      .select({ stage: leads.stage, count: sql<number>`count(*)::int` })
      .from(leads)
      .where(and(eq(leads.tenantId, tenantId), isNull(leads.archivedAt)))
      .groupBy(leads.stage),
    // By source
    db
      .select({ source: leads.source, count: sql<number>`count(*)::int` })
      .from(leads)
      .where(and(eq(leads.tenantId, tenantId), isNull(leads.archivedAt)))
      .groupBy(leads.source),
    // Recent activities (last 10 across all leads)
    db
      .select()
      .from(leadActivities)
      .where(eq(leadActivities.tenantId, tenantId))
      .orderBy(desc(leadActivities.createdAt))
      .limit(10),
  ])

  const wonCount = closedThisMonth.find((r) => r.stage === 'WON')?.count ?? 0
  const lostCount = closedThisMonth.find((r) => r.stage === 'LOST')?.count ?? 0
  const totalClosed = wonCount + lostCount
  const conversionRate = totalClosed > 0 ? wonCount / totalClosed : null

  return {
    leadsThisMonth: thisMonthCount[0]?.count ?? 0,
    leadsLastMonth: lastMonthCount[0]?.count ?? 0,
    conversionRate,
    pipelineValue: pipelineValueResult[0]?.total ?? '0',
    byStage: byStageResult.map((r) => ({ stage: r.stage, count: r.count })),
    bySource: bySourceResult.map((r) => ({ source: r.source, count: r.count })),
    recentActivities: recentActivitiesResult.map(serializeActivity),
  }
}

// ── Lead Detail (linked entity counts) ───────────────────────────────────────

export interface LeadLinkedCounts {
  proposalCount: number
  contractCount: number
  invoiceCount: number
  taskCount: number
}

export interface LeadDetailObject extends LeadObject {
  linkedCounts: LeadLinkedCounts
}

/**
 * Fetch a lead with linked entity counts for the detail page.
 * Returns null if not found or belongs to a different tenant.
 */
export async function getLeadDetail(
  db: Db,
  tenantId: string,
  id: string,
): Promise<LeadDetailObject | null> {
  const lead = await getLead(db, tenantId, id)
  if (!lead) return null

  const [proposalCountResult, contractCountResult, invoiceCountResult, taskCountResult] =
    await Promise.all([
      db
        .select({ n: count() })
        .from(proposals)
        .where(and(eq(proposals.tenantId, tenantId), eq(proposals.leadId, id))),
      db
        .select({ n: count() })
        .from(contracts)
        .where(and(eq(contracts.tenantId, tenantId), eq(contracts.leadId, id))),
      db
        .select({ n: count() })
        .from(invoices)
        .where(and(eq(invoices.tenantId, tenantId), eq(invoices.leadId, id))),
      db
        .select({ n: count() })
        .from(tasks)
        .where(and(eq(tasks.tenantId, tenantId), eq(tasks.leadId, id))),
    ])

  return {
    ...lead,
    linkedCounts: {
      proposalCount: Number(proposalCountResult[0]?.n ?? 0),
      contractCount: Number(contractCountResult[0]?.n ?? 0),
      invoiceCount: Number(invoiceCountResult[0]?.n ?? 0),
      taskCount: Number(taskCountResult[0]?.n ?? 0),
    },
  }
}

// ── Lead Linked Entities (list for tabs) ─────────────────────────────────────

export interface LinkedProposal {
  id: string
  title: string
  status: string
  total: string | null
  createdAt: string
}

export interface LinkedContract {
  id: string
  title: string
  status: string
  createdAt: string
}

export interface LinkedInvoice {
  id: string
  invoiceNumber: string | null
  proformaNumber: string | null
  status: string
  total: string
  createdAt: string
}

export interface LinkedTask {
  id: string
  title: string
  priority: string
  dueDate: string | null
  createdAt: string
}

export async function listLinkedProposals(
  db: Db,
  tenantId: string,
  leadId: string,
): Promise<LinkedProposal[]> {
  const rows = await db
    .select({
      id: proposals.id,
      title: proposals.title,
      status: proposals.status,
      total: proposals.total,
      createdAt: proposals.createdAt,
    })
    .from(proposals)
    .where(and(eq(proposals.tenantId, tenantId), eq(proposals.leadId, leadId)))
    .orderBy(desc(proposals.createdAt))
  return rows.map((r) => ({
    id: r.id,
    title: r.title,
    status: r.status,
    total: r.total,
    createdAt: r.createdAt.toISOString(),
  }))
}

export async function listLinkedContracts(
  db: Db,
  tenantId: string,
  leadId: string,
): Promise<LinkedContract[]> {
  const rows = await db
    .select({
      id: contracts.id,
      title: contracts.title,
      status: contracts.status,
      createdAt: contracts.createdAt,
    })
    .from(contracts)
    .where(and(eq(contracts.tenantId, tenantId), eq(contracts.leadId, leadId)))
    .orderBy(desc(contracts.createdAt))
  return rows.map((r) => ({
    id: r.id,
    title: r.title,
    status: r.status,
    createdAt: r.createdAt.toISOString(),
  }))
}

export async function listLinkedInvoices(
  db: Db,
  tenantId: string,
  leadId: string,
): Promise<LinkedInvoice[]> {
  const rows = await db
    .select({
      id: invoices.id,
      invoiceNumber: invoices.invoiceNumber,
      proformaNumber: invoices.proformaNumber,
      status: invoices.status,
      total: invoices.total,
      createdAt: invoices.createdAt,
    })
    .from(invoices)
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.leadId, leadId)))
    .orderBy(desc(invoices.createdAt))
  return rows.map((r) => ({
    id: r.id,
    invoiceNumber: r.invoiceNumber,
    proformaNumber: r.proformaNumber,
    status: r.status,
    total: r.total,
    createdAt: r.createdAt.toISOString(),
  }))
}

export async function listLinkedTasks(
  db: Db,
  tenantId: string,
  leadId: string,
): Promise<LinkedTask[]> {
  const rows = await db
    .select({
      id: tasks.id,
      title: tasks.title,
      priority: tasks.priority,
      dueDate: tasks.dueDate,
      createdAt: tasks.createdAt,
    })
    .from(tasks)
    .where(and(eq(tasks.tenantId, tenantId), eq(tasks.leadId, leadId)))
    .orderBy(asc(tasks.dueDate), desc(tasks.createdAt))
  return rows.map((r) => ({
    id: r.id,
    title: r.title,
    priority: r.priority,
    dueDate: r.dueDate ?? null,
    createdAt: r.createdAt.toISOString(),
  }))
}

// ── Lead lost re-engagement helpers (wave-14) ─────────────────────────────────

/**
 * Reopen a LOST lead: move it back to NEW (or a specified stage), clear lost fields.
 */
export async function reopenLead(
  db: Db,
  tenantId: string,
  id: string,
  targetStage: string = 'NEW',
): Promise<LeadObject> {
  const [row] = await db
    .update(leads)
    .set({
      stage: targetStage,
      lostReason: null,
      reengagementAt: null,
      reengagementNotifiedAt: null,
      updatedAt: new Date(),
    })
    .where(and(eq(leads.tenantId, tenantId), eq(leads.id, id)))
    .returning()
  if (!row) throw new Error('Lead not found')
  return serializeLead(row)
}

export interface DueReengagementLead {
  id: string
  tenantId: string
  name: string
  email: string | null
  assignedTo: string | null
  reengagementAt: string
}

/**
 * Returns LOST leads where reengagement_at <= now and reengagement_notified_at IS NULL.
 * Used by the daily cron. Max 500 rows per call for safety.
 */
export async function selectDueReengagementLeads(
  db: Db,
  now: Date,
  limit = 500,
): Promise<DueReengagementLead[]> {
  const rows = await db
    .select({
      id: leads.id,
      tenantId: leads.tenantId,
      name: leads.name,
      email: leads.email,
      assignedTo: leads.assignedTo,
      reengagementAt: leads.reengagementAt,
    })
    .from(leads)
    .where(
      and(
        eq(leads.stage, 'LOST'),
        isNull(leads.reengagementNotifiedAt),
        isNotNull(leads.reengagementAt),
        lte(leads.reengagementAt, now),
        isNull(leads.archivedAt),
      ),
    )
    .limit(limit)

  return rows.map((r) => ({
    id: r.id,
    tenantId: r.tenantId,
    name: r.name,
    email: r.email ?? null,
    assignedTo: r.assignedTo ?? null,
    reengagementAt: r.reengagementAt!.toISOString(),
  }))
}

/**
 * Stamp reengagement_notified_at = now to prevent re-firing the cron.
 */
export async function markReengagementNotified(
  db: Db,
  leadId: string,
  tenantId: string,
  now: Date,
): Promise<void> {
  await db
    .update(leads)
    .set({ reengagementNotifiedAt: now, updatedAt: now })
    .where(and(eq(leads.id, leadId), eq(leads.tenantId, tenantId)))
}

const DEFAULT_LOST_REASONS = [
  'Price too high',
  'Chose competitor',
  'Not the right time',
  'No budget',
  'Unresponsive',
]

/**
 * Get the tenant's lost reason labels. Falls back to defaults if not configured.
 */
export async function getLostReasons(
  db: Db,
  tenantId: string,
): Promise<string[]> {
  const [row] = await db
    .select({ reasons: tenantSettings.leadLostReasons })
    .from(tenantSettings)
    .where(eq(tenantSettings.tenantId, tenantId))
    .limit(1)

  if (!row || !Array.isArray(row.reasons) || row.reasons.length === 0) {
    return DEFAULT_LOST_REASONS
  }
  return row.reasons as string[]
}

/**
 * Upsert the tenant's lost reason labels.
 * Validates: 1–20 items, each max 200 chars.
 */
export async function updateLostReasons(
  db: Db,
  tenantId: string,
  reasons: string[],
): Promise<string[]> {
  await db
    .update(tenantSettings)
    .set({
      leadLostReasons: reasons,
      updatedAt: new Date(),
    })
    .where(eq(tenantSettings.tenantId, tenantId))

  return reasons
}

// ── Re-export validation schemas (required by no-raw-drizzle-from-routes) ─────

export {
  createLeadSchema,
  updateLeadSchema,
  moveLeadSchema,
  convertLeadSchema,
  leadFiltersSchema,
  addLeadActivitySchema,
  createLeadFormSchema,
  updateLeadFormSchema,
  publicFormSubmitSchema,
  createLeadWebhookSchema,
  updateLeadWebhookSchema,
  createPipelineStageSchema,
  updatePipelineStageSchema,
  leadStageSchema,
  leadSourceSchema,
  webhookSourceSchema,
} from '../validation/marketing'

export type {
  CreateLeadInput,
  UpdateLeadInput,
  MoveLeadInput,
  ConvertLeadInput,
  AddLeadActivityInput,
  CreateLeadFormInput,
  UpdateLeadFormInput,
  PublicFormSubmitInput,
  CreateLeadWebhookInput,
  UpdateLeadWebhookInput,
  CreatePipelineStageInput,
  UpdatePipelineStageInput,
  LeadFiltersInput,
  FieldConfig,
  FormStyle,
} from '../validation/marketing'
