import 'server-only'

import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createAdminClient } from '@/lib/supabase/admin'
import { publicOrigin } from '@/lib/app-url'
import { DEFAULT_TEMPLATES, renderTemplateClean, sampleDataFor } from '@/lib/email-templates'
import { filtersFromParams, leadKindConfig, leadTemplateData, leadTemplateVariables, type LeadKind } from '@/lib/domain/broker-leads'
import {
  campaignProblems, createCampaign, deleteCampaign, deleteSegment, duplicateCampaign, enrollCampaign, filterLeads, inviteLeadsNow, leadsToCsv, loadAllLeads, loadCampaign, loadLead,
  loadLeadTemplates, previewAudience, resolveReview, runBrokerCampaigns, saveLeadsSettings, saveSegment, sendTestEmail, setCampaignStatus, suppressLead, tagLeads, unsuppressLead, updateCampaign, updateLead,
} from '@/lib/data/broker-leads'
import { requireInternal, requireLeadsTables } from './_guard'

/**
 * The lead manager's API, once, for both kinds (2026-09-25). Each route file
 * is one line: `export const { GET, POST } = leadsHandlers('carrier')`. The
 * kind decides the surface that guards the route, the settings row, the
 * campaign type, the template category and the join page copy.
 */

/* ---------------------------------------------------------------- leads */

const leadsSchema = z.discriminatedUnion('op', [
  z.object({ op: z.literal('update'), id: z.string().uuid(), first_name: z.string().max(80).optional(), last_name: z.string().max(80).optional(), phone: z.string().max(40).optional(), state: z.string().max(2).optional(), notes: z.string().max(4000).optional(), tags: z.array(z.string().max(40)).max(30).optional() }),
  z.object({ op: z.literal('tag'), ids: z.array(z.string().uuid()).min(1).max(2000), tag: z.string().min(1).max(40), action: z.enum(['add', 'remove']) }),
  z.object({ op: z.literal('suppress'), id: z.string().uuid(), reason: z.string().max(200).optional() }),
  z.object({ op: z.literal('unsuppress'), id: z.string().uuid() }),
  z.object({ op: z.literal('resolve_review'), id: z.string().uuid(), action: z.enum(['accept', 'drop']) }),
  z.object({ op: z.literal('invite_now'), ids: z.array(z.string().uuid()).min(1).max(2000), template_key: z.string().min(1).max(80), limit: z.number().int().min(1).max(500).optional() }),
])

export function leadsHandlers(kind: LeadKind) {
  const cfg = leadKindConfig(kind)
  async function GET(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const id = req.nextUrl.searchParams.get('id')
    if (id) {
      const d = await loadLead(id)
      return d && d.lead.lead_kind === kind ? NextResponse.json(d) : NextResponse.json({ error: 'Lead not found.' }, { status: 404 })
    }
    if (req.nextUrl.searchParams.get('export') === '1') {
      const rows = filterLeads(await loadAllLeads(undefined, kind), filtersFromParams((k) => req.nextUrl.searchParams.get(k)))
      return new NextResponse(leadsToCsv(rows, kind), { headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': `attachment; filename="${cfg.csvName}-${new Date().toISOString().slice(0, 10)}.csv"` } })
    }
    return NextResponse.json({ error: 'Pass ?id= or ?export=1.' }, { status: 400 })
  }
  async function POST(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const parsed = leadsSchema.safeParse(await req.json().catch(() => ({})))
    if (!parsed.success) return NextResponse.json({ error: 'Check the request and try again.' }, { status: 400 })
    const input = parsed.data
    const actor = guard.actor
    switch (input.op) {
      case 'update': { const r = await updateLead(input.id, input, actor); return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 }) }
      case 'tag': { const r = await tagLeads(input.ids, input.tag, input.action, actor); return r.ok ? NextResponse.json(r) : NextResponse.json({ error: r.error }, { status: 400 }) }
      case 'suppress': { const r = await suppressLead(input.id, input.reason ?? '', actor); return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 }) }
      case 'unsuppress': { const r = await unsuppressLead(input.id, actor); return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 }) }
      case 'resolve_review': { const r = await resolveReview(input.id, input.action, actor); return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 }) }
      case 'invite_now': { const r = await inviteLeadsNow({ leadIds: input.ids, templateKey: input.template_key, limit: input.limit, actor, origin: publicOrigin(req) }, undefined, kind); return r.ok ? NextResponse.json(r) : NextResponse.json({ error: r.error }, { status: 400 }) }
    }
  }
  return { GET, POST }
}

/* ------------------------------------------------------------ campaigns */

const campaignInput = z.object({
  name: z.string().min(1).max(120),
  description: z.string().max(1000).nullable().optional(),
  steps: z.array(z.object({ template_key: z.string().min(1).max(80), delay_days: z.number().int().min(0).max(120) })).min(1).max(6),
  audience_kind: z.enum(['segment', 'leads']),
  audience: z.record(z.string(), z.unknown()),
  send_window: z.object({ days: z.array(z.number().int().min(0).max(6)).min(1), start_hour: z.number().int().min(0).max(23), end_hour: z.number().int().min(1).max(24), timezone: z.string().max(60) }),
  batch_size: z.number().int().min(1).max(500),
  ramp: z.object({ start: z.number().int().min(1), step: z.number().int().min(0), max: z.number().int().min(1) }).nullable(),
  frequency_cap_days: z.number().int().min(0).max(90),
  daily_send_limit: z.number().int().min(1).max(5000),
  start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable(),
  end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable(),
})
const campaignSchema = z.discriminatedUnion('op', [
  z.object({ op: z.literal('create') }).merge(campaignInput),
  z.object({ op: z.literal('update'), id: z.string().uuid() }).merge(campaignInput.partial()),
  z.object({ op: z.literal('duplicate'), id: z.string().uuid() }),
  z.object({ op: z.literal('delete'), id: z.string().uuid() }),
  z.object({ op: z.literal('preview'), id: z.string().uuid().optional(), audience_kind: z.enum(['segment', 'leads']).optional(), audience: z.record(z.string(), z.unknown()).optional() }),
  z.object({ op: z.literal('enroll'), id: z.string().uuid() }),
  z.object({ op: z.literal('approve'), id: z.string().uuid() }),
  z.object({ op: z.literal('pause'), id: z.string().uuid() }),
  z.object({ op: z.literal('resume'), id: z.string().uuid() }),
  z.object({ op: z.literal('stop'), id: z.string().uuid() }),
  z.object({ op: z.literal('test_send'), id: z.string().uuid().optional(), template_key: z.string().min(1).max(80), to: z.string().trim().email() }),
  z.object({ op: z.literal('run_now') }),
])

export function campaignsHandlers(kind: LeadKind) {
  async function GET(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const id = req.nextUrl.searchParams.get('id')
    if (!id) return NextResponse.json({ error: 'Pass ?id=.' }, { status: 400 })
    const c = await loadCampaign(id)
    if (!c || c.kind !== kind) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 })
    return NextResponse.json({ campaign: c, problems: await campaignProblems(c) })
  }
  async function POST(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const parsed = campaignSchema.safeParse(await req.json().catch(() => ({})))
    if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0]?.message ? `Check the campaign: ${parsed.error.issues[0].path.join('.')} ${parsed.error.issues[0].message}` : 'Check the campaign and try again.' }, { status: 400 })
    const i = parsed.data
    const actor = guard.actor
    const out = (r: { ok: boolean; error?: string } & Record<string, unknown>) => (r.ok ? NextResponse.json(r) : NextResponse.json({ error: r.error }, { status: 400 }))
    const own = async (id: string) => { const c = await loadCampaign(id); return c && c.kind === kind ? c : null }
    switch (i.op) {
      case 'create': { const { op: _o, ...rest } = i; void _o; return out(await createCampaign(rest as Parameters<typeof createCampaign>[0], actor, undefined, kind)) }
      case 'update': { const { op: _o, id, ...rest } = i; void _o; if (!(await own(id))) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 }); return out(await updateCampaign(id, rest as Parameters<typeof updateCampaign>[1], actor)) }
      case 'duplicate': if (!(await own(i.id))) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 }); return out(await duplicateCampaign(i.id, actor))
      case 'delete': if (!(await own(i.id))) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 }); return out(await deleteCampaign(i.id))
      case 'preview': {
        const base = i.id ? await own(i.id) : null
        const audienceKind = i.audience_kind ?? base?.audience_kind ?? 'segment'
        const audience = (i.audience as Parameters<typeof previewAudience>[0]['audience'] | undefined) ?? base?.audience ?? {}
        const p = await previewAudience({ id: i.id ?? '', audience_kind: audienceKind, audience, kind })
        const { eligibleRows: _rows, ...rest } = p; void _rows
        return NextResponse.json(rest)
      }
      case 'enroll': if (!(await own(i.id))) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 }); return out(await enrollCampaign(i.id, actor))
      case 'approve': case 'pause': case 'resume': case 'stop': if (!(await own(i.id))) return NextResponse.json({ error: 'Campaign not found.' }, { status: 404 }); return out(await setCampaignStatus(i.id, i.op, actor))
      case 'test_send': return out(await sendTestEmail({ templateKey: i.template_key, to: i.to, campaignId: i.id ?? null, actor, origin: publicOrigin(req) }, undefined, kind))
      case 'run_now': return NextResponse.json({ ok: true, summary: await runBrokerCampaigns({ origin: publicOrigin(req), actor, manual: true }, undefined, kind) })
    }
  }
  return { GET, POST }
}

/* ------------------------------------------------------------------ run */

/** The scheduler's entry point: hourly from the host cron with `x-campaign-cron-secret`, or as a signed-in internal user. */
export function runHandlers(kind: LeadKind) {
  async function POST(req: NextRequest) {
    const secret = process.env.CAMPAIGN_CRON_SECRET || process.env.PILOT_CRON_SECRET
    const header = req.headers.get('x-campaign-cron-secret')
    let actor = { id: null as string | null, label: 'cron' }
    if (!(secret && header && header === secret)) {
      const guard = await requireInternal(kind)
      if (!guard.ok) return guard.response
      actor = guard.actor
    }
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const summary = await runBrokerCampaigns({ origin: publicOrigin(req), actor, manual: false }, undefined, kind)
    return NextResponse.json({ ok: true, summary })
  }
  return { POST }
}

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

const segmentFilters = z.record(z.string(), z.unknown())
const segmentSchema = z.discriminatedUnion('op', [
  z.object({ op: z.literal('save'), id: z.string().uuid().nullable().optional(), name: z.string().min(1).max(80), description: z.string().max(400).nullable().optional(), filters: segmentFilters }),
  z.object({ op: z.literal('delete'), id: z.string().uuid() }),
  z.object({ op: z.literal('preview'), filters: segmentFilters }),
])

export function segmentsHandlers(kind: LeadKind) {
  async function POST(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const parsed = segmentSchema.safeParse(await req.json().catch(() => ({})))
    if (!parsed.success) return NextResponse.json({ error: 'Check the request and try again.' }, { status: 400 })
    const input = parsed.data
    if (input.op === 'delete') { const r = await deleteSegment(input.id); return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 }) }
    if (input.op === 'preview') { const rows = filterLeads(await loadAllLeads(undefined, kind), input.filters); return NextResponse.json({ count: rows.length }) }
    const r = await saveSegment({ id: input.id ?? null, name: input.name, description: input.description ?? null, filters: input.filters }, guard.actor, undefined, kind)
    return r.ok ? NextResponse.json({ ok: true, id: r.id }) : NextResponse.json({ error: r.error }, { status: 400 })
  }
  return { POST }
}

/* ------------------------------------------------------------- settings */

const settingsSchema = z.object({
  cron_enabled: z.boolean().optional(),
  default_daily_limit: z.number().int().min(1).max(5000).optional(),
  default_batch_size: z.number().int().min(1).max(500).optional(),
  default_send_window: z.object({ days: z.array(z.number().int().min(0).max(6)).min(1), start_hour: z.number().int().min(0).max(23), end_hour: z.number().int().min(1).max(24), timezone: z.string().max(60) }).optional(),
  default_ramp: z.object({ start: z.number().int().min(1), step: z.number().int().min(0), max: z.number().int().min(1) }).nullable().optional(),
  default_frequency_cap_days: z.number().int().min(0).max(90).optional(),
})

export function settingsHandlers(kind: LeadKind) {
  async function POST(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const parsed = settingsSchema.safeParse(await req.json().catch(() => ({})))
    if (!parsed.success) return NextResponse.json({ error: 'Check the settings and try again.' }, { status: 400 })
    const r = await saveLeadsSettings(parsed.data, guard.actor, undefined, kind)
    return r.ok ? NextResponse.json({ ok: true }) : NextResponse.json({ error: r.error }, { status: 400 })
  }
  return { POST }
}

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

function sampleLeadData(kind: LeadKind, joinLink: string): Record<string, string> {
  return kind === 'carrier'
    ? leadTemplateData({ first_name: 'Sam', last_name: 'Rivera', broker_company_name: null, mc_number: null, email: 'sam@example.com', company_name: 'ABC TRANSPORT LLC', lead_type: 'both' }, joinLink)
    : leadTemplateData({ first_name: 'Sam', last_name: 'Rivera', broker_company_name: 'ABC FREIGHT BROKERAGE LLC', mc_number: '654321', email: 'sam@example.com' }, joinLink)
}

const KEY = /^[a-z0-9_]{3,80}$/
const templateSave = z.object({
  op: z.literal('save'),
  key: z.string().trim().regex(KEY, 'Use lowercase letters, digits and underscores (3–80 characters).'),
  name: z.string().trim().min(1).max(160),
  subject: z.string().trim().min(1).max(300),
  body: z.string().trim().min(1).max(20_000),
  footer: z.string().trim().max(2000).optional(),
  support_contact: z.string().trim().max(200).optional(),
  active: z.boolean(),
  reason: z.string().trim().max(300).optional(),
})
const templateSchema = z.discriminatedUnion('op', [
  templateSave,
  z.object({ op: z.literal('preview'), key: z.string().trim().max(80), subject: z.string().max(300).optional(), body: z.string().max(20_000).optional(), lead_id: z.string().uuid().optional() }),
  z.object({ op: z.literal('test'), key: z.string().trim().max(80), to: z.string().trim().email() }),
  z.object({ op: z.literal('duplicate'), key: z.string().trim().max(80), new_key: z.string().trim().regex(KEY, 'Use lowercase letters, digits and underscores (3–80 characters).'), name: z.string().trim().min(1).max(160) }),
])

export function templatesHandlers(kind: LeadKind) {
  const cfg = leadKindConfig(kind)
  async function GET() {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const admin = createAdminClient()
    const templates = await loadLeadTemplates(admin, kind)
    const keys = templates.map((t) => t.key)
    const { data: saved } = keys.length ? await admin.from('email_templates').select('id, key, body, footer, support_contact, version, updated_by, updated_at').in('key', keys) : { data: [] }
    const savedByKey = new Map(((saved ?? []) as Array<Record<string, unknown>>).map((r) => [r.key as string, r]))
    const full = templates.map((t) => {
      const d = DEFAULT_TEMPLATES.find((x) => x.key === t.key)
      const o = savedByKey.get(t.key)
      return { ...t, body: (o?.body as string) ?? d?.body ?? '', footer: (o?.footer as string) ?? '', support_contact: (o?.support_contact as string) ?? '', updated_by: (o?.updated_by as string | null) ?? null, updated_at: (o?.updated_at as string | null) ?? null, shipped: !!d }
    })
    let versions: unknown[] = []
    if (keys.length) {
      const byKey = await admin.from('email_template_versions').select('*').in('template_key', keys).order('created_at', { ascending: false }).limit(40)
      if (byKey.error) {
        const ids = ((saved ?? []) as Array<{ id?: string }>).map((r) => r.id).filter(Boolean) as string[]
        const byId = ids.length ? await admin.from('email_template_versions').select('*').in('template_id', ids).order('created_at', { ascending: false }).limit(40) : { data: [] }
        versions = (byId.data ?? []) as unknown[]
      } else versions = (byKey.data ?? []) as unknown[]
    }
    return NextResponse.json({ templates: full, versions, variables: leadTemplateVariables(kind) })
  }
  async function POST(req: NextRequest) {
    const guard = await requireInternal(kind)
    if (!guard.ok) return guard.response
    const missing = await requireLeadsTables(kind)
    if (missing) return missing
    const parsed = templateSchema.safeParse(await req.json().catch(() => ({})))
    if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0]?.message ?? 'Check the template and try again.' }, { status: 400 })
    const input = parsed.data
    const admin = createAdminClient()
    const actor = guard.actor

    if (input.op === 'preview' || input.op === 'test') {
      const templates = await loadLeadTemplates(admin, kind)
      const t = templates.find((x) => x.key === input.key)
      if (!t && input.op === 'test') return NextResponse.json({ error: 'Save the template first, then send a test.' }, { status: 400 })
      if (input.op === 'test') {
        const r = await sendTestEmail({ templateKey: input.key, to: input.to, actor, origin: publicOrigin(req) }, admin, kind)
        return r.ok ? NextResponse.json({ ok: true, status: r.status, subject: r.subject }) : NextResponse.json({ error: r.error }, { status: 400 })
      }
      // Preview against a real lead when one is given, otherwise the sample.
      let data: Record<string, string> = { ...sampleDataFor(input.key), ...sampleLeadData(kind, `${publicOrigin(req)}/join/preview-token`) }
      if (input.lead_id) {
        const lead = (await loadAllLeads(admin, kind)).find((l) => l.id === input.lead_id)
        if (lead) data = { ...data, ...leadTemplateData({ first_name: lead.first_name, last_name: lead.last_name, broker_company_name: lead.brokerage_name ?? lead.broker_company_name, mc_number: lead.mc_number, email: lead.email, company_name: lead.company_name, lead_type: lead.lead_type }, `${publicOrigin(req)}/join/preview-token`) }
      }
      const shipped = DEFAULT_TEMPLATES.find((x) => x.key === input.key)
      const subject = input.subject ?? t?.subject ?? shipped?.subject ?? ''
      const body = input.body ?? shipped?.body ?? ''
      return NextResponse.json({ subject: renderTemplateClean(subject, data), body: renderTemplateClean(body, data) })
    }

    const notes = kind === 'carrier'
      ? { recipients_note: 'A carrier lead (dispatcher or driver) in a campaign.', trigger_note: 'Carrier lead campaigns.', cc_rules: 'None.' }
      : { recipients_note: 'A freight broker lead in a campaign.', trigger_note: 'Freight broker lead campaigns.', cc_rules: 'None.' }

    if (input.op === 'duplicate') {
      const templates = await loadLeadTemplates(admin, kind)
      if (templates.some((t) => t.key === input.new_key)) return NextResponse.json({ error: 'A template with that key already exists.' }, { status: 409 })
      const { data: src } = await admin.from('email_templates').select('*').eq('key', input.key).maybeSingle()
      const shipped = DEFAULT_TEMPLATES.find((x) => x.key === input.key)
      const from = (src ?? shipped ?? null) as Record<string, unknown> | null
      if (!from) return NextResponse.json({ error: 'That template no longer exists.' }, { status: 404 })
      const { error } = await admin.from('email_templates').insert({
        key: input.new_key, name: input.name, subject: from.subject, body: from.body, footer: from.footer ?? null, support_contact: from.support_contact ?? null, ...notes,
        active: false, email_stream: 'updates', category_key: cfg.category, required: false, unsubscribe_required: true, version: 1, updated_by: actor.label,
      })
      if (error) return NextResponse.json({ error: error.message }, { status: 400 })
      return NextResponse.json({ ok: true, key: input.new_key })
    }

    // save
    const t = input
    const { data: previous } = await admin.from('email_templates').select('id, subject, body, version').eq('key', t.key).maybeSingle()
    const nextVersion = (((previous as { version?: number } | null)?.version ?? 0) as number) + 1
    const base = { key: t.key, name: t.name, subject: t.subject, body: t.body, active: t.active, ...notes, updated_by: actor.label, updated_at: new Date().toISOString() }
    // The stream and category are fixed: a lead campaign email must always be unsubscribable.
    const full = { ...base, footer: t.footer || null, support_contact: t.support_contact || null, version: nextVersion, email_stream: 'updates', category_key: cfg.category, required: false, unsubscribe_required: true }
    let up = await admin.from('email_templates').upsert(full, { onConflict: 'key' }).select('*').single()
    if (up.error && /column|schema cache/i.test(up.error.message)) up = await admin.from('email_templates').upsert(base, { onConflict: 'key' }).select('*').single()
    if (up.error) return NextResponse.json({ error: up.error.message }, { status: 400 })
    // Version history: template_id + edited_by (0008); 0022 added template_key, version and the previous copy.
    const savedRow = up.data as { id: string }
    const versionBase = { template_id: savedRow.id, subject: t.subject, body: t.body, edited_by: actor.label, reason: t.reason || null }
    let v = await admin.from('email_template_versions').insert({ ...versionBase, template_key: t.key, version: nextVersion, previous_subject: (previous as { subject?: string } | null)?.subject ?? null, previous_body: (previous as { body?: string } | null)?.body ?? null })
    if (v.error && /column|schema cache/i.test(v.error.message)) v = await admin.from('email_template_versions').insert(versionBase)
    if (v.error) console.error('template version not recorded', v.error.message)
    return NextResponse.json({ ok: true, template: up.data, version: nextVersion, version_recorded: !v.error })
  }
  return { GET, POST }
}
