/*
|--------------------------------------------------------------------------
| Agência PSI — Edge Function: conversation-sla-check
|--------------------------------------------------------------------------
| Cron a cada 5 minutos. Detecta conversas com mensagem INBOUND mais
| antiga que o threshold de SLA do tenant sem resposta OUTBOUND depois,
| abre breach (idempotente), e notifica:
|   - Terapeuta atribuído (assigned_to) sempre
|   - clinic_admin/tenant_admin se rule.notify_admin_on_breach = true
|
| Se rule.respect_business_hours = true, o tempo decorrido conta apenas
| minutos DENTRO da janela comercial (pausa fora). Isso evita falsos
| positivos quando o paciente manda mensagem às 23:00 — o SLA só começa
| a contar a partir do próximo horário comercial.
|
| Escopo (rule.alert_scope):
|   - 'assigned_only' (default): só conversas com assigned_to preenchido
|   - 'all': todas, inclusive não-atribuídas (aí o alerta vai só pros admins)
|
| Resolução: trigger trg_sla_resolve_on_outbound marca breach como
| resolved_at quando chega nova mensagem outbound na thread.
|--------------------------------------------------------------------------
*/

import { type SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { adminClient, listTenantSchemas } 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, GET, OPTIONS',
}

function json(body: unknown, status = 200) {
    return new Response(JSON.stringify(body), {
        status,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })
}

type Rule = {
    enabled: boolean
    threshold_minutes: number
    respect_business_hours: boolean
    business_hours_start: string   // 'HH:MM:SS'
    business_hours_end: string
    business_days: number[]        // 1=seg ... 7=dom
    alert_scope: 'assigned_only' | 'all'
    notify_admin_on_breach: boolean
}

type ThreadCandidate = {
    tenant_id: string
    thread_key: string
    patient_id: string | null
    patient_name: string | null
    contact_number: string | null
    assigned_to: string | null
    last_inbound_at: string   // ISO
}

// ────────────────────────────────────────────────────────────────
// Business-hours math
// ────────────────────────────────────────────────────────────────

function parseHHMMSS(s: string): { h: number; m: number } {
    const [h, m] = s.split(':').map((x) => parseInt(x, 10))
    return { h: h || 0, m: m || 0 }
}

// Cria um Date em UTC que representa "YYYY-MM-DD HH:MM em horário de São Paulo".
// Usa Intl.DateTimeFormat pra descobrir o offset do timezone naquela data.
function saoPauloDate(year: number, month: number, day: number, hour: number, minute: number): Date {
    // Cria uma instância UTC aproximada e ajusta pelo offset de SP na data
    const approxUtc = Date.UTC(year, month - 1, day, hour, minute)
    // Descobre o offset atual de SP
    const fmt = new Intl.DateTimeFormat('en-US', {
        timeZone: 'America/Sao_Paulo',
        year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', hour12: false
    })
    const parts = fmt.formatToParts(new Date(approxUtc))
    const getP = (t: string) => parseInt(parts.find((p) => p.type === t)?.value || '0', 10)
    const spRenderedUtcMs = Date.UTC(getP('year'), getP('month') - 1, getP('day'), getP('hour'), getP('minute'))
    const offsetMs = approxUtc - spRenderedUtcMs
    return new Date(approxUtc + offsetMs)
}

// ISO day of week in São Paulo (1=Monday .. 7=Sunday)
function isoWeekdaySp(d: Date): number {
    const fmt = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Sao_Paulo', weekday: 'short' })
    const map: Record<string, number> = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 }
    return map[fmt.format(d)] || 1
}

// Retorna {year, month, day} em timezone de São Paulo
function ymdSp(d: Date): { year: number, month: number, day: number } {
    const fmt = new Intl.DateTimeFormat('en-US', {
        timeZone: 'America/Sao_Paulo',
        year: 'numeric', month: '2-digit', day: '2-digit'
    })
    const parts = fmt.formatToParts(d)
    return {
        year: parseInt(parts.find((p) => p.type === 'year')?.value || '0', 10),
        month: parseInt(parts.find((p) => p.type === 'month')?.value || '0', 10),
        day: parseInt(parts.find((p) => p.type === 'day')?.value || '0', 10)
    }
}

/**
 * Retorna quantos minutos transcorreram DENTRO da janela comercial entre from e to.
 * Business days são ISO 1-7 (1=seg). business_hours_start/end são 'HH:MM:SS' em SP.
 */
function businessMinutesElapsed(
    fromISO: string,
    toDate: Date,
    rule: Rule
): number {
    const from = new Date(fromISO)
    if (toDate <= from) return 0

    const { h: startH, m: startM } = parseHHMMSS(rule.business_hours_start)
    const { h: endH, m: endM } = parseHHMMSS(rule.business_hours_end)
    const daysSet = new Set(rule.business_days)

    let total = 0
    // Itera dia a dia, de from.date até to.date (inclusive)
    const fromYmd = ymdSp(from)
    const toYmd = ymdSp(toDate)

    let cursor = new Date(from)
    let cursorYmd = fromYmd
    let safety = 0   // evita loop infinito em caso de bug
    while (safety < 400) {
        safety++
        // dia atual em SP
        const dayStartSp = saoPauloDate(cursorYmd.year, cursorYmd.month, cursorYmd.day, startH, startM)
        const dayEndSp = saoPauloDate(cursorYmd.year, cursorYmd.month, cursorYmd.day, endH, endM)

        // Se é dia de trabalho, soma interseção
        if (daysSet.has(isoWeekdaySp(dayStartSp))) {
            const intervalStart = Math.max(from.getTime(), dayStartSp.getTime())
            const intervalEnd = Math.min(toDate.getTime(), dayEndSp.getTime())
            if (intervalEnd > intervalStart) {
                total += Math.floor((intervalEnd - intervalStart) / 60000)
            }
        }

        // Avança pro próximo dia
        if (cursorYmd.year === toYmd.year && cursorYmd.month === toYmd.month && cursorYmd.day === toYmd.day) break
        // Adiciona 1 dia em UTC (suficiente mesmo com DST pq estamos só iterando data local)
        cursor = new Date(cursor.getTime() + 24 * 3600 * 1000)
        cursorYmd = ymdSp(cursor)
    }

    return total
}

// ────────────────────────────────────────────────────────────────
// Processamento por tenant
// ────────────────────────────────────────────────────────────────

async function processRule(tdb: SupabaseClient, admin: SupabaseClient, tenantId: string, rule: Rule, now: Date): Promise<{
    tenant_id: string
    candidates: number
    opened: number
    still_pending: number
    notified: number
}> {
    // Query candidatas: threads onde:
    //   - última mensagem é INBOUND
    //   - (se assigned_only) assigned_to IS NOT NULL
    // Vou usar a view conversation_threads (tenant) + filtro direction='inbound'.
    let query = tdb
        .from('conversation_threads')
        .select('thread_key, patient_id, patient_name, contact_number, assigned_to, last_message_at, last_message_direction')
        .eq('last_message_direction', 'inbound')

    if (rule.alert_scope === 'assigned_only') {
        query = query.not('assigned_to', 'is', null)
    }

    const { data: candidates, error } = await query
    if (error) {
        return { tenant_id: tenantId, candidates: 0, opened: 0, still_pending: 0, notified: 0, /* @ts-ignore */ error: error.message }
    }

    let opened = 0
    let stillPending = 0
    let notified = 0

    for (const row of candidates || []) {
        const last = row.last_message_at as string | null
        if (!last) continue

        const elapsed = rule.respect_business_hours
            ? businessMinutesElapsed(last, now, rule)
            : Math.floor((now.getTime() - new Date(last).getTime()) / 60000)

        if (elapsed < rule.threshold_minutes) {
            stillPending++
            continue
        }

        // Abre breach (idempotente). RPC mantém p_tenant_id (F6 reescreve depois).
        const { data: breachId, error: openErr } = await admin.rpc('sla_open_breach', {
            p_tenant_id: tenantId,
            p_thread_key: row.thread_key,
            p_assigned_to: row.assigned_to,
            p_last_inbound_at: last,
            p_threshold_minutes: rule.threshold_minutes
        })
        if (openErr || !breachId) continue

        opened++

        // Notificação (só se ainda não notificou esse breach)
        const didNotify = await notifyBreach(tdb, admin, tenantId, {
            breach_id: breachId as unknown as string,
            thread_key: row.thread_key,
            patient_name: row.patient_name || row.contact_number || 'Paciente desconhecido',
            assigned_to: row.assigned_to as string | null,
            notify_admin: rule.notify_admin_on_breach,
            elapsed_minutes: elapsed,
            threshold_minutes: rule.threshold_minutes
        })
        if (didNotify) notified++
    }

    return { tenant_id: tenantId, candidates: (candidates || []).length, opened, still_pending: stillPending, notified }
}

async function notifyBreach(tdb: SupabaseClient, admin: SupabaseClient, tenantId: string, params: {
    breach_id: string
    thread_key: string
    patient_name: string
    assigned_to: string | null
    notify_admin: boolean
    elapsed_minutes: number
    threshold_minutes: number
}): Promise<boolean> {
    // Anti-spam: não renotifica se já notificou (breach é tenant → tdb)
    const { data: breach } = await tdb
        .from('conversation_sla_breaches')
        .select('notified_at')
        .eq('id', params.breach_id)
        .maybeSingle()
    if (breach?.notified_at) return false

    // Monta set de user_ids (assigned_to + admins, se configurado)
    const userIds = new Set<string>()
    if (params.assigned_to) userIds.add(params.assigned_to)

    if (params.notify_admin) {
        // tenant_members é GLOBAL → admin, mantém filtro por tenant_id
        const { data: admins } = await admin
            .from('tenant_members')
            .select('user_id')
            .eq('tenant_id', tenantId)
            .in('role', ['clinic_admin', 'tenant_admin'])
            .eq('status', 'active')
        for (const a of admins || []) userIds.add(a.user_id)
    }

    if (userIds.size === 0) return false

    const title = `SLA estourado: ${params.patient_name}`
    const detail = `Conversa sem resposta há ${params.elapsed_minutes} min (limite: ${params.threshold_minutes}). Responda o quanto antes.`

    const rows = Array.from(userIds).map((uid) => ({
        owner_id: uid,
        type: 'system_alert',
        ref_id: params.breach_id,
        ref_table: 'conversation_sla_breaches',
        payload: {
            title,
            detail,
            severity: 'error',
            deeplink: '/conversas',
            actionLabel: 'Abrir CRM',
            thread_key: params.thread_key
        }
    }))

    // notifications é tenant → tdb (sem tenant_id no payload)
    const { error: insertErr } = await tdb.from('notifications').insert(rows)
    if (insertErr) return false

    await admin.rpc('sla_mark_notified', { p_tenant_id: tenantId, p_breach_id: params.breach_id })
    return true
}

// ────────────────────────────────────────────────────────────────
// Handler
// ────────────────────────────────────────────────────────────────

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

    const admin = adminClient()

    try {
        const now = new Date()

        // Varre todos os tenants; cada schema tem suas próprias sla_rules (tenant)
        const tasks: Array<Promise<{ tenant_id: string; candidates: number; opened: number; still_pending: number; notified: number }>> = []

        for (const t of await listTenantSchemas(admin)) {
            const tdb = admin.schema(t.schema)

            // Regras habilitadas do tenant (tabela tenant → tdb, sem tenant_id)
            const { data: rules, error: rulesErr } = await tdb
                .from('conversation_sla_rules')
                .select('enabled, threshold_minutes, respect_business_hours, business_hours_start, business_hours_end, business_days, alert_scope, notify_admin_on_breach')
                .eq('enabled', true)

            if (rulesErr) {
                console.error(`[sla] rules query error (tenant ${t.tenantId}):`, rulesErr.message)
                continue
            }
            for (const r of rules || []) {
                tasks.push(
                    processRule(tdb, admin, t.tenantId, r as Rule, now).catch((e) => ({
                        tenant_id: t.tenantId,
                        candidates: 0, opened: 0, still_pending: 0, notified: 0,
                        error: (e as Error).message
                    }))
                )
            }
        }

        if (tasks.length === 0) return json({ checked: 0, results: [] })

        const results = await Promise.all(tasks)

        const summary = {
            checked: results.length,
            opened: results.reduce((s, r) => s + r.opened, 0),
            notified: results.reduce((s, r) => s + r.notified, 0),
            still_pending: results.reduce((s, r) => s + r.still_pending, 0)
        }

        return json({ ...summary, results })
    } catch (e) {
        return json({ error: (e as Error).message || 'unexpected_error' }, 500)
    }
})
