/*
|--------------------------------------------------------------------------
| Agência PSI — Edge Function: process-email-queue
|--------------------------------------------------------------------------
| Processa a notification_queue para channel = 'email'.
|
| schema-per-tenant: notification_queue/channels/logs e email_templates_tenant
| vivem no schema físico de cada tenant (SEM coluna tenant_id). O cron VARRE
| todos os tenants; se vier body.tenant_id, processa só aquele (modo single).
| email_templates_global é GLOBAL (admin/public).
|
| Fluxo por item:
|   1. Busca pendentes (channel='email', status='pendente', scheduled_at <= now)
|   2. Marca como 'processando' (lock otimista)
|   3. Busca canal SMTP em notification_channels (tdb)
|   4. Resolve template: COALESCE(tenant, global)
|   5. Renderiza variáveis e condicionais {{#if}}
|   6. Envia via SMTP (Deno raw TCP com STARTTLS)
|   7. Atualiza queue e insere em notification_logs (tdb)
|   8. Em erro: retry com backoff ou marca 'falhou'
|--------------------------------------------------------------------------
*/

import type { SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { SmtpClient } from 'https://deno.land/x/smtp@v0.7.0/mod.ts'
import { adminClient, listTenantSchemas, tenantDbForId, schemaForTenant } from '../_shared/tenant.ts'

const corsHeaders = {
  'Access-Control-Allow-Origin':  '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
}

// ── Template renderer ──────────────────────────────────────────

function resolveVariable(key: string, vars: Record<string, unknown>): unknown {
  if (!key.includes('.')) return vars[key]
  return key.split('.').reduce((obj: any, part) => obj?.[part], vars)
}

function renderTemplate(template: string, variables: Record<string, unknown>): string {
  if (!template) return ''
  let result = template

  // Blocos condicionais {{#if var}}...{{/if}}
  result = result.replace(
    /\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
    (_, key, content) => resolveVariable(key, variables) ? content : ''
  )

  // Substituições simples {{variavel}}
  result = result.replace(/\{\{([\w.]+)\}\}/g, (_, key) => {
    const value = resolveVariable(key, variables)
    return value !== undefined && value !== null ? String(value) : ''
  })

  return result
}

function stripHtml(html: string): string {
  return html
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>/gi, '\n')
    .replace(/<[^>]+>/g, '')
    .replace(/\n{3,}/g, '\n\n')
    .trim()
}

// ── SMTP sender ────────────────────────────────────────────────

interface SmtpCredentials {
  host: string
  port: number
  username: string
  password: string
  from_email: string
  from_name?: string
  tls?: boolean
}

async function sendEmail(
  creds: SmtpCredentials,
  to: string,
  subject: string,
  bodyHtml: string,
  bodyText: string
): Promise<{ messageId?: string }> {
  const client = new SmtpClient()

  const connectConfig = {
    hostname: creds.host,
    port:     creds.port,
    username: creds.username,
    password: creds.password,
  }

  // Porta 465 = TLS direto, outras = STARTTLS
  if (creds.port === 465 || creds.tls === true) {
    await client.connectTLS(connectConfig)
  } else {
    await client.connect(connectConfig)
  }

  const fromHeader = creds.from_name
    ? `${creds.from_name} <${creds.from_email}>`
    : creds.from_email

  await client.send({
    from:    fromHeader,
    to,
    subject,
    content: bodyText,
    html:    bodyHtml,
  })

  await client.close()

  return { messageId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}` }
}

// ── Processa a fila de UM tenant ───────────────────────────────

type Result = { id: string; status: string; error?: string }

async function processTenantQueue(admin: SupabaseClient, tdb: SupabaseClient): Promise<Result[]> {
  const now = new Date().toISOString()

  // 1. Busca itens pendentes de email deste tenant
  const { data: items, error: fetchErr } = await tdb
    .from('notification_queue')
    .select('*')
    .eq('channel', 'email')
    .eq('status', 'pendente')
    .lte('scheduled_at', now)
    .lt('attempts', 5) // respeita max_attempts padrão
    .order('scheduled_at', { ascending: true })
    .limit(20)

  if (fetchErr) throw new Error(fetchErr.message)
  if (!items || items.length === 0) return []

  const results: Result[] = []

  for (const item of items) {
    // 2. Lock otimista — marca como processando
    const { error: lockErr } = await tdb
      .from('notification_queue')
      .update({ status: 'processando', attempts: item.attempts + 1 })
      .eq('id', item.id)
      .eq('status', 'pendente') // garante que outro worker não pegou

    if (lockErr) {
      results.push({ id: item.id, status: 'skip', error: 'lock failed' })
      continue
    }

    try {
      // 3. Busca canal SMTP (tdb)
      const { data: channel, error: chErr } = await tdb
        .from('notification_channels')
        .select('credentials, sender_address, provider')
        .eq('owner_id', item.owner_id)
        .eq('channel', 'email')
        .eq('is_active', true)
        .is('deleted_at', null)
        .single()

      if (chErr || !channel) {
        throw new Error('Canal SMTP não encontrado ou inativo para este owner')
      }

      // 4. Resolve template: tenant (tdb) → global (admin) fallback
      const { data: tenantTpl } = await tdb
        .from('email_templates_tenant')
        .select('subject, body_html, body_text, enabled')
        .eq('owner_id', item.owner_id)
        .eq('template_key', item.template_key)
        .maybeSingle()

      // Se tenant desabilitou o template → ignorar
      if (tenantTpl && tenantTpl.enabled === false) {
        await tdb
          .from('notification_queue')
          .update({ status: 'ignorado' })
          .eq('id', item.id)

        await tdb.from('notification_logs').insert({
          owner_id:          item.owner_id,
          queue_id:          item.id,
          agenda_evento_id:  item.agenda_evento_id,
          patient_id:        item.patient_id,
          channel:           'email',
          template_key:      item.template_key,
          schedule_key:      item.schedule_key,
          recipient_address: item.recipient_address,
          status:            'failed',
          failure_reason:    'Template desabilitado pelo tenant',
          created_at:        now,
        })

        results.push({ id: item.id, status: 'ignorado' })
        continue
      }

      // Busca global (admin/public)
      const { data: globalTpl } = await admin
        .from('email_templates_global')
        .select('subject, body_html, body_text')
        .eq('key', item.template_key)
        .eq('is_active', true)
        .single()

      if (!globalTpl) {
        throw new Error(`Template global não encontrado: ${item.template_key}`)
      }

      // COALESCE: tenant sobrescreve global quando não-null
      const resolvedSubject  = tenantTpl?.subject   ?? globalTpl.subject
      const resolvedBodyHtml = tenantTpl?.body_html  ?? globalTpl.body_html
      const resolvedBodyText = tenantTpl?.body_text  ?? globalTpl.body_text

      // 5. Renderiza variáveis
      const vars = item.resolved_vars || {}
      const finalSubject  = renderTemplate(resolvedSubject, vars)
      const finalBodyHtml = renderTemplate(resolvedBodyHtml, vars)
      const finalBodyText = resolvedBodyText
        ? renderTemplate(resolvedBodyText, vars)
        : stripHtml(finalBodyHtml)

      // 6. Envia via SMTP
      const creds = channel.credentials as SmtpCredentials
      if (!creds.from_email && channel.sender_address) {
        creds.from_email = channel.sender_address
      }

      const sendResult = await sendEmail(
        creds,
        item.recipient_address,
        finalSubject,
        finalBodyHtml,
        finalBodyText
      )

      // 7. Sucesso — atualiza queue
      await tdb
        .from('notification_queue')
        .update({
          status:              'enviado',
          sent_at:             new Date().toISOString(),
          provider_message_id: sendResult.messageId || null,
        })
        .eq('id', item.id)

      // Insere log de sucesso
      await tdb.from('notification_logs').insert({
        owner_id:           item.owner_id,
        queue_id:           item.id,
        agenda_evento_id:   item.agenda_evento_id,
        patient_id:         item.patient_id,
        channel:            'email',
        template_key:       item.template_key,
        schedule_key:       item.schedule_key,
        recipient_address:  item.recipient_address,
        resolved_message:   finalBodyHtml,
        resolved_vars:      vars,
        status:             'sent',
        provider:           channel.provider || 'smtp',
        provider_message_id: sendResult.messageId || null,
        sent_at:            new Date().toISOString(),
      })

      results.push({ id: item.id, status: 'enviado' })

    } catch (err) {
      // 8. Erro — retry com backoff ou falha definitiva
      const attempts = item.attempts + 1
      const maxAttempts = item.max_attempts || 5
      const isExhausted = attempts >= maxAttempts

      const retryDelay = attempts * 2 * 60 * 1000 // backoff: attempts * 2 min
      const nextRetryAt = isExhausted
        ? null
        : new Date(Date.now() + retryDelay).toISOString()

      await tdb
        .from('notification_queue')
        .update({
          status:       isExhausted ? 'falhou' : 'pendente',
          last_error:   err.message,
          next_retry_at: nextRetryAt,
        })
        .eq('id', item.id)

      // Log de falha
      await tdb.from('notification_logs').insert({
        owner_id:          item.owner_id,
        queue_id:          item.id,
        agenda_evento_id:  item.agenda_evento_id,
        patient_id:        item.patient_id,
        channel:           'email',
        template_key:      item.template_key,
        schedule_key:      item.schedule_key,
        recipient_address: item.recipient_address,
        status:            'failed',
        failure_reason:    err.message,
        failed_at:         new Date().toISOString(),
      })

      results.push({ id: item.id, status: isExhausted ? 'falhou' : 'retry', error: err.message })
    }
  }

  return results
}

// ── Main handler ───────────────────────────────────────────────

Deno.serve(async (req: Request) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  const admin = adminClient()

  // Modo single-tenant se body.tenant_id; senão varre todos.
  let bodyTenantId: string | null = null
  try {
    const body = await req.json()
    bodyTenantId = body?.tenant_id ?? null
  } catch {
    // sem body / body inválido → modo varredura
  }

  const results: Result[] = []
  const errors: Array<{ tenantId: string; error: string }> = []

  try {
    if (bodyTenantId) {
      const schema = await schemaForTenant(admin, bodyTenantId)
      if (!schema) {
        return new Response(
          JSON.stringify({ error: `schema indisponível para tenant ${bodyTenantId}` }),
          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
        )
      }
      const tdb = await tenantDbForId(admin, bodyTenantId)
      results.push(...await processTenantQueue(admin, tdb))
    } else {
      const tenants = await listTenantSchemas(admin)
      for (const t of tenants) {
        try {
          const tdb = admin.schema(t.schema)
          results.push(...await processTenantQueue(admin, tdb))
        } catch (e) {
          console.error(`[process-email-queue] tenant ${t.tenantId} falhou:`, e.message)
          errors.push({ tenantId: t.tenantId, error: e.message })
        }
      }
    }
  } catch (err) {
    return new Response(
      JSON.stringify({ error: err.message }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  if (!results.length && !errors.length) {
    return new Response(
      JSON.stringify({ message: 'Nenhum email na fila', processed: 0 }),
      { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  const sent    = results.filter(r => r.status === 'enviado').length
  const failed  = results.filter(r => r.status === 'falhou').length
  const retried = results.filter(r => r.status === 'retry').length
  const ignored = results.filter(r => r.status === 'ignorado').length

  return new Response(
    JSON.stringify({
      processed: results.length,
      sent,
      failed,
      retried,
      ignored,
      details: results,
      tenantErrors: errors,
    }),
    { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
})
