<?php
// Prevent direct access
if (count(get_included_files()) === 1) {
    http_response_code(403);
    exit('Forbidden');
}

require_once dirname(__DIR__) . '/services/gemini.php';
require_once dirname(__DIR__) . '/services/tools.php';
require_once dirname(__DIR__) . '/services/function_dispatcher.php';
require_once dirname(__DIR__) . '/services/playbooks.php';
require_once dirname(__DIR__) . '/services/vale_memory_service.php';

/**
 * AgentService — the channel-agnostic brain.
 *
 * Each channel (Twilio/WhatsApp, web widget) resolves its own tenant and
 * contact, then calls handleMessage(). With Function Calling, Vale can now
 * act (crear_cita, capturar_lead) and not just reply.
 */
class AgentService {

    /** Máximo de rondas de function calling por mensaje (evita loops infinitos). */
    private const MAX_FUNCTION_ROUNDS = 3;

    /** Find an open conversation for this contact/channel, or create one. */
    public static function findOrCreateConversation(PDO $db, int $tenantId, int $contactId, string $channel): int {
        $stmt = $db->prepare(
            "SELECT id FROM conversations
             WHERE tenant_id = :tenant_id AND contact_id = :contact_id AND status = 'open'
             ORDER BY id DESC LIMIT 1"
        );
        $stmt->execute([':tenant_id' => $tenantId, ':contact_id' => $contactId]);
        $row = $stmt->fetch();
        if ($row) {
            return (int) $row['id'];
        }

        $stmt = $db->prepare(
            "INSERT INTO conversations (tenant_id, contact_id, channel, status, handled_by)
             VALUES (:tenant_id, :contact_id, :channel, 'open', 'ai')"
        );
        $stmt->execute([
            ':tenant_id' => $tenantId,
            ':contact_id' => $contactId,
            ':channel' => $channel,
        ]);
        return (int) $db->lastInsertId();
    }

    /**
     * Renueva la ventana de 24h de WhatsApp para la conversación.
     * Llamar cada vez que llega un mensaje inbound del cliente.
     */
    public static function refreshWindow(PDO $db, int $conversationId): void {
        $db->prepare(
            "UPDATE conversations SET window_expires_at = DATE_ADD(NOW(), INTERVAL 24 HOUR) WHERE id = :id"
        )->execute([':id' => $conversationId]);
    }

    /**
     * Persist an inbound (contact -> Vale) message.
     * $metaMsgId: ID del mensaje de Meta — si no es null, INSERT IGNORE para
     * evitar doble inserción si el webhook llega duplicado.
     */
    public static function persistInbound(
        PDO $db, int $tenantId, int $conversationId,
        string $body, ?string $mediaUrl = null, ?string $metaMsgId = null
    ): void {
        $ignore = $metaMsgId !== null ? 'IGNORE' : '';
        $stmt = $db->prepare(
            "INSERT $ignore INTO messages
                (tenant_id, conversation_id, direction, sender, body, media_url, meta_message_id)
             VALUES
                (:tenant_id, :conversation_id, 'inbound', 'contact', :body, :media_url, :meta_msg_id)"
        );
        $stmt->execute([
            ':tenant_id'      => $tenantId,
            ':conversation_id'=> $conversationId,
            ':body'           => $body,
            ':media_url'      => $mediaUrl,
            ':meta_msg_id'    => $metaMsgId,
        ]);
    }

    /**
     * Build context, run the Function Calling loop, persist the reply + logs.
     * Assumes the inbound message is already persisted.
     *
     * @return array{text:string, tokens:int, locale:string}
     * @throws Exception if a Gemini call fails (caller handles the channel fallback).
     */
    public static function handleMessage(PDO $db, array $tenant, int $conversationId, int $contactId): array {
        $tenantId = (int) $tenant['id'];

        // Most recent window: current message + up to 10 prior, chronological order.
        // raw_candidate includes thoughtSignature parts needed for Gemini 2.5 multi-turn.
        $stmt = $db->prepare(
            "SELECT direction, sender, body, raw_candidate FROM messages
             WHERE conversation_id = :conversation_id ORDER BY id DESC LIMIT 11"
        );
        $stmt->execute([':conversation_id' => $conversationId]);
        $history = array_reverse($stmt->fetchAll());
        $userMessage = array_pop($history); // current (most recent) inbound message

        // Tenant's Vale config with sane defaults.
        $stmt = $db->prepare("SELECT * FROM vale_config WHERE tenant_id = :tenant_id LIMIT 1");
        $stmt->execute([':tenant_id' => $tenantId]);
        $valeConfig = $stmt->fetch() ?: [
            'greeting' => 'Hola, ¿en qué te puedo ayudar hoy?',
            'tone' => 'calido',
            'default_locale' => 'es',
        ];

        // Active services, injected into the prompt.
        $stmt = $db->prepare("SELECT * FROM services WHERE tenant_id = :tenant_id AND active = 1");
        $stmt->execute([':tenant_id' => $tenantId]);
        $services = $stmt->fetchAll();

        $isFirstTurn = count($history) === 0;
        $systemInstruction = self::buildSystemInstruction($tenant, $valeConfig, $services, $isFirstTurn, $contactId);
        $tools = ToolRegistry::declarations();

        // Build initial contents: prior history + current user message.
        $contents = self::historyToContents($history);
        $contents[] = ['role' => 'user', 'parts' => [['text' => $userMessage['body']]]];

        // ── Plan gate: bloquear si el tenant superó el límite de conversaciones ──
        require_once __DIR__ . '/plan_guard.php';
        if (!PlanGuard::dentroDelLimite($tenant, 'convos')) {
            $locale   = $valeConfig['default_locale'] ?? 'es';
            $limitMsg = ($locale === 'en')
                ? 'Hi! This business has reached its free plan limit for this month. Please contact them directly.'
                : 'Hola, este negocio alcanzó el límite de su plan gratuito este mes. Por favor contactalos directamente.';
            $db->prepare(
                "INSERT INTO messages (tenant_id, conversation_id, direction, sender, body, model_tokens, raw_candidate)
                 VALUES (:tenant_id, :conversation_id, 'outbound', 'ai', :body, 0, NULL)"
            )->execute([
                ':tenant_id'       => $tenantId,
                ':conversation_id' => $conversationId,
                ':body'            => $limitMsg,
            ]);
            return ['text' => $limitMsg, 'tokens' => 0, 'locale' => $locale];
        }

        $startTime = microtime(true);
        $totalTokens = 0;
        $finalText = '';
        $lastResult = null;
        $options = [];
        $accion = null;

        for ($round = 0; $round <= self::MAX_FUNCTION_ROUNDS; $round++) {
            $result = GeminiService::generate($systemInstruction, $contents, $tools);
            $lastResult = $result;
            $totalTokens += (int) ($result['tokens'] ?? 0);

            // If the model wants to call functions (and we still have budget), execute them.
            if (!empty($result['functionCalls']) && $round < self::MAX_FUNCTION_ROUNDS) {
                if (!empty($result['candidate'])) {
                    $contents[] = $result['candidate']; // model's functionCall turn
                }
                $responseParts = [];
                foreach ($result['functionCalls'] as $fc) {
                    if ($fc['name'] === 'ofrecer_opciones') {
                        $options = self::extractOptions($fc['args']);
                        $fnResult = ['status' => 'ok', 'mensaje' => 'Opciones mostradas al usuario.'];
                    } else {
                        $fnResult = FunctionDispatcher::dispatch($db, $tenantId, $conversationId, $contactId, $fc['name'], $fc['args']);
                        if ($fc['name'] === 'crear_cita' && empty($fnResult['error'])) {
                            $accion = 'iniciar_cita';
                        } elseif ($fc['name'] === 'escalate_to_owner') {
                            $accion = 'escalar_humano';
                        }
                    }
                    self::logFunctionCall($db, $tenantId, $fc, $fnResult);

                    $fr = ['name' => $fc['name'], 'response' => $fnResult];
                    if (!empty($fc['id'])) {
                        $fr['id'] = $fc['id']; // Gemini 3 matches calls by id
                    }
                    $responseParts[] = ['functionResponse' => $fr];
                }
                // Function results go back as a 'user' turn (current Gemini REST format).
                $contents[] = ['role' => 'user', 'parts' => $responseParts];
                continue;
            }

            $finalText = $result['text'] ?? '';
            break;
        }

        // Safety net: model only emitted function calls and never produced text.
        $safetyNetFired = ($finalText === '');
        if ($safetyNetFired) {
            $finalText = (($valeConfig['default_locale'] ?? 'es') === 'en')
                ? 'Done! Is there anything else I can help you with?'
                : '¡Listo! ¿Te puedo ayudar con algo más?';
        }

        $latencyMs = round((microtime(true) - $startTime) * 1000);

        // Persist outbound (Vale -> contact).
        // raw_candidate stores the full candidate (including thoughtSignature) for multi-turn round-trip.
        // When the safety net fires, $lastResult['candidate'] is a functionCall — storing it would
        // orphan a FC without its FunctionResponse in the next turn's history, causing hallucinations.
        $stmt = $db->prepare(
            "INSERT INTO messages (tenant_id, conversation_id, direction, sender, body, model_tokens, raw_candidate)
             VALUES (:tenant_id, :conversation_id, 'outbound', 'ai', :body, :model_tokens, :raw_candidate)"
        );
        $stmt->execute([
            ':tenant_id'      => $tenantId,
            ':conversation_id'=> $conversationId,
            ':body'           => $finalText,
            ':model_tokens'   => $totalTokens,
            ':raw_candidate'  => (!$safetyNetFired && $lastResult['candidate'] !== null)
                                     ? json_encode($lastResult['candidate'], JSON_UNESCAPED_UNICODE)
                                     : null,
        ]);

        // Incrementar contador mensual de conversaciones para gating de plan.
        require_once __DIR__ . '/plan_guard.php';
        PlanGuard::incrementarConvos($db, $tenantId);

        // Evidence log (the Gemini side of the turn).
        $stmt = $db->prepare(
            "INSERT INTO agent_logs (tenant_id, type, gemini_call, tokens, payload)
             VALUES (:tenant_id, 'agent_message', 1, :tokens, :payload)"
        );
        $stmt->execute([
            ':tenant_id' => $tenantId,
            ':tokens' => $totalTokens,
            ':payload' => json_encode([
                'channel' => $tenant['_channel'] ?? 'unknown',
                'request' => $lastResult['raw_request'] ?? null,
                'response' => $lastResult['raw_response'] ?? null,
                'latency_ms' => $latencyMs,
            ]),
        ]);

        return [
            'text'    => $finalText,
            'tokens'  => $totalTokens,
            'locale'  => $valeConfig['default_locale'] ?? 'es',
            'options' => $options,
            'accion'  => $accion,
        ];
    }

    /** Normaliza las opciones de ofrecer_opciones a una lista de strings cortos. */
    private static function extractOptions(array $args): array {
        $opts = $args['opciones'] ?? [];
        if (!is_array($opts)) {
            return [];
        }
        $out = [];
        foreach ($opts as $o) {
            $s = trim((string) $o);
            if ($s !== '') {
                $out[] = mb_substr($s, 0, 60);
            }
        }
        return array_slice($out, 0, 8);
    }

    /** Record an executed function call (the agent acting) as evidence. */
    private static function logFunctionCall(PDO $db, int $tenantId, array $functionCall, array $result): void {
        $stmt = $db->prepare(
            "INSERT INTO agent_logs (tenant_id, type, gemini_call, payload)
             VALUES (:tenant_id, 'function_call', 0, :payload)"
        );
        $stmt->execute([
            ':tenant_id' => $tenantId,
            ':payload' => json_encode([
                'function' => $functionCall['name'],
                'args' => $functionCall['args'],
                'result' => $result,
            ]),
        ]);
    }

    /** Record an error in agent_logs (no Gemini reply produced). */
    public static function logError(PDO $db, ?int $tenantId, string $type, Throwable $e): void {
        $stmt = $db->prepare(
            "INSERT INTO agent_logs (tenant_id, type, gemini_call, payload)
             VALUES (:tenant_id, :type, 1, :payload)"
        );
        $stmt->execute([
            ':tenant_id' => $tenantId,
            ':type' => $type,
            ':payload' => json_encode([
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]),
        ]);
    }

    /** Map stored messages to Gemini `contents`, merging consecutive same-role turns. */
    private static function historyToContents(array $history): array {
        $contents = [];
        $lastRole = null;
        foreach ($history as $msg) {
            $role = ($msg['direction'] === 'inbound') ? 'user' : 'model';

            // For model turns: restore the full candidate (with thoughtSignature) when stored.
            // Gemini 2.5-flash emits a thoughtSignature even with thinkingBudget:0; omitting it
            // on the next request causes the model to produce content:[] (zero tokens).
            if ($role === 'model' && !empty($msg['raw_candidate'])) {
                $candidate = json_decode($msg['raw_candidate'], true);
                if ($candidate && !empty($candidate['parts'])) {
                    if ($role === $lastRole && !empty($contents)) {
                        $lastIdx = count($contents) - 1;
                        foreach ($candidate['parts'] as $part) {
                            $contents[$lastIdx]['parts'][] = $part;
                        }
                    } else {
                        $contents[] = $candidate; // already has 'role' => 'model'
                        $lastRole = $role;
                    }
                    continue;
                }
            }

            // Fallback: plain text reconstruction (user turns + legacy model turns without raw_candidate).
            $text = $msg['body'];
            if ($role === $lastRole && !empty($contents)) {
                $lastIdx = count($contents) - 1;
                $contents[$lastIdx]['parts'][0]['text'] .= "\n" . $text;
            } else {
                $contents[] = ['role' => $role, 'parts' => [['text' => $text]]];
                $lastRole = $role;
            }
        }
        return $contents;
    }

    /**
     * Trims $contents to at most $maxTurns entries, always starting on a plain
     * user-text turn — never splits an FC pair or orphans a functionResponse.
     */
    private static function trimHistory(array $contents, int $maxTurns): array {
        if (count($contents) <= $maxTurns) {
            return $contents;
        }
        $slice = array_slice($contents, -$maxTurns);
        foreach ($slice as $i => $turn) {
            if (($turn['role'] ?? '') === 'user'
                && isset($turn['parts'][0]['text'])
                && !isset($turn['parts'][0]['functionResponse'])) {
                return array_slice($slice, $i);
            }
        }
        return $slice; // fallback: no plain user turn found (shouldn't occur)
    }

    /**
     * Modo ONBOARDING: Vale configura el negocio conversando con el dueño.
     * Historial en sesión server-side keyed por tenant_id — preserva
     * thoughtSignature entre requests. El parámetro $history ya no se usa.
     *
     * @param array $history  Ignorado — conservado para backward-compat del caller.
     * @return array{text:string, done:bool, tokens:int}
     */
    public static function handleOnboarding(PDO $db, array $tenant, array $history, string $userMessage): array {
        $tenantId = (int) $tenant['id'];

        $stmt = $db->prepare("SELECT name FROM services WHERE tenant_id = :t AND active = 1");
        $stmt->execute([':t' => $tenantId]);
        $svcNames = array_column($stmt->fetchAll(), 'name');

        $systemInstruction = self::buildOnboardingPrompt($tenant, $svcNames);
        $tools = ToolRegistry::declarations('onboarding');

        // Server-side session history keyed by tenant — preserves thoughtSignature across requests.
        // The $history param (client-side text-only) is intentionally ignored.
        $sessionKey = 'onboarding_history_' . $tenantId;
        $contents   = is_array($_SESSION[$sessionKey] ?? null) ? $_SESSION[$sessionKey] : [];
        $contents[] = ['role' => 'user', 'parts' => [['text' => $userMessage]]];

        $totalTokens = 0;
        $finalText = '';
        $done = false;
        $lastResult = null;
        $options = [];
        $multiSelect = false; // true when options come from sugerir_servicios (service picker)

        for ($round = 0; $round <= self::MAX_FUNCTION_ROUNDS; $round++) {
            $result = GeminiService::generate($systemInstruction, $contents, $tools);
            $lastResult = $result;
            $totalTokens += (int) ($result['tokens'] ?? 0);

            if (!empty($result['functionCalls']) && $round < self::MAX_FUNCTION_ROUNDS) {
                if (!empty($result['candidate'])) {
                    $contents[] = $result['candidate'];
                }
                $responseParts = [];
                foreach ($result['functionCalls'] as $fc) {
                    if ($fc['name'] === 'sugerir_servicios') {
                        $multiSelect = true; // next ofrecer_opciones will be a service picker
                    }
                    if ($fc['name'] === 'ofrecer_opciones') {
                        $options = self::extractOptions($fc['args']);
                        $fnResult = ['status' => 'ok', 'mensaje' => 'Opciones mostradas al usuario.'];
                    } else {
                        $fnResult = FunctionDispatcher::dispatchOnboarding($db, $tenantId, $fc['name'], $fc['args']);
                    }
                    self::logAction($db, $tenantId, 'onboarding_config', $fc, $fnResult);
                    if (!empty($fnResult['done'])) {
                        $done = true;
                    }
                    $fr = ['name' => $fc['name'], 'response' => $fnResult];
                    if (!empty($fc['id'])) {
                        $fr['id'] = $fc['id'];
                    }
                    $responseParts[] = ['functionResponse' => $fr];
                }
                $contents[] = ['role' => 'user', 'parts' => $responseParts];
                continue;
            }

            $finalText = $result['text'] ?? '';
            break;
        }

        if ($finalText === '') {
            // Context-aware fallback: infer next question from current state instead of generic prompt.
            $nbOk  = !empty($tenant['name']) && $tenant['name'] !== 'Mi Negocio';
            $rbOk  = !empty($tenant['rubro']);
            $nbStr = $nbOk ? $tenant['name'] : 'tu negocio';
            if (!$nbOk) {
                $finalText = '¿Cómo se llama tu negocio?';
            } elseif (!$rbOk) {
                $finalText = '¿A qué se dedica ' . $nbStr . '?';
            } elseif (empty($svcNames)) {
                $finalText = '¿Qué servicios ofrecés? Podés dictarme los principales con su precio y duración aproximada.';
            } elseif (empty($tenant['ciudad'])) {
                $finalText = '¿En qué ciudad está ' . $nbStr . '?';
            } else {
                $finalText = 'Quedamos en eso. ¿Hay algo más que quieras ajustar?';
            }
        }

        // Always persist history — even when fallback fired — so the next turn is not orphaned.
        // Use a synthetic text candidate when the model produced no real text candidate.
        $candidate = !empty($lastResult['candidate'])
            ? $lastResult['candidate']
            : ['role' => 'model', 'parts' => [['text' => $finalText]]];
        $contents[] = $candidate;
        $contents = self::trimHistory($contents, 40);
        if ($done) {
            unset($_SESSION[$sessionKey]);
        } else {
            $_SESSION[$sessionKey] = $contents;
        }

        // Evidencia del turno de Gemini — en try/catch: un fallo de log nunca mata el onboarding.
        try {
            $db->prepare(
                "INSERT INTO agent_logs (tenant_id, type, gemini_call, tokens, payload)
                 VALUES (:t, 'onboarding_message', 1, :tok, :p)"
            )->execute([
                ':t'   => $tenantId,
                ':tok' => $totalTokens,
                ':p'   => json_encode(['tokens' => $totalTokens, 'done' => $done], JSON_UNESCAPED_UNICODE),
            ]);
        } catch (Throwable $_logEx) {
            error_log('[AgentService/handleOnboarding] log insert failed: ' . $_logEx->getMessage());
        }

        return ['text' => $finalText, 'done' => $done, 'tokens' => $totalTokens, 'options' => $options, 'multi_select' => $multiSelect && !empty($options)];
    }

    /** Log de una acción del agente (no-Gemini) con un type custom. */
    private static function logAction(PDO $db, int $tenantId, string $type, array $functionCall, array $result): void {
        $db->prepare(
            "INSERT INTO agent_logs (tenant_id, type, gemini_call, payload) VALUES (:t, :ty, 0, :p)"
        )->execute([
            ':t' => $tenantId,
            ':ty' => $type,
            ':p' => json_encode([
                'function' => $functionCall['name'],
                'args' => $functionCall['args'],
                'result' => $result,
            ], JSON_UNESCAPED_UNICODE),
        ]);
    }

    private static function buildOnboardingPrompt(array $tenant, array $svcNames): string {
        require_once __DIR__ . '/rubro_service.php';

        $nombreActual = $tenant['name'] ?: '';
        $rubroCanon   = $tenant['rubro']       ?? null;
        $especialidad = $tenant['especialidad'] ?? null;
        $ciudad       = $tenant['ciudad']       ?? null;
        $servicios    = $svcNames ? implode(', ', $svcNames) : 'ninguno todavía';
        $calendarOk   = !empty($tenant['google_calendar_connected']);

        // ── Estado actual (evitar re-preguntar lo que ya sabe) ────────────────
        $nombreOk  = $nombreActual && $nombreActual !== 'Mi Negocio';
        $rubroOk   = (bool)$rubroCanon;
        $serviciosOk = !empty($svcNames);

        // ── Bloque del rubro (cuando ya está confirmado) ──────────────────────
        $bloqueRubro = '';
        if ($rubroCanon) {
            $bloqueRubro = "\nCONOCIMIENTO DEL RUBRO (aplicalo en TODA la conversación):\n"
                . RubroService::bloquePrompt($rubroCanon);
            if ($especialidad) {
                $bloqueRubro .= "\nESPECIALIDAD: $especialidad";
            }
            $vocab = RubroService::vocab($rubroCanon);
            $bloqueRubro .= "\nVOCABULARIO: llamá al usuario '{$vocab['usuario']}', al prestador '{$vocab['prestador']}', al servicio '{$vocab['servicio']}' y a la reserva '{$vocab['reserva']}'.";
        }

        // ── Estado resumido para que Vale sepa desde dónde arrancar ──────────
        $estadoLineas = [];
        if ($nombreOk)    $estadoLineas[] = "Nombre: $nombreActual (YA GUARDADO — no volver a preguntar)";
        else              $estadoLineas[] = "Nombre: sin confirmar todavía";
        if ($rubroOk)     $estadoLineas[] = "Rubro: $rubroCanon" . ($especialidad ? " ($especialidad)" : '') . " (YA GUARDADO)";
        else              $estadoLineas[] = "Rubro: sin confirmar";
        if ($serviciosOk) $estadoLineas[] = "Servicios cargados: $servicios (YA GUARDADOS — confirmar al dueño y preguntar precios si faltan)";
        else              $estadoLineas[] = "Servicios: ninguno todavía";
        if ($ciudad)      $estadoLineas[] = "Ciudad: $ciudad (YA GUARDADA)";
        else              $estadoLineas[] = "Ciudad: sin registrar";
        if ($calendarOk)  $estadoLineas[] = "Google Calendar: CONECTADO (YA GUARDADO — saltá el PASO 7 completamente)";
        else              $estadoLineas[] = "Google Calendar: no conectado";

        $estadoBloque = implode("\n- ", $estadoLineas);

        return <<<PROMPT
Sos Vale, una empleada con IA que está configurando el negocio del DUEÑO — no un cliente final.
Tu misión: entrevistarlo en UNA conversación fluida y natural, completar su perfil y dejar Vale lista para trabajar.
Copiá el registro del dueño (voseo centroamericano o tuteo según cómo escriba). Default: voseo suave.
Una pregunta a la vez. Tono cálido, directo, sin rodeos. Nunca uses markdown ni listas.
Identificate siempre como asistente de IA.

{$bloqueRubro}

ESTADO ACTUAL — arrancá desde donde corresponde, NO repreguntés lo que ya está guardado:
- {$estadoBloque}

FLUJO ADAPTATIVO — completá solo lo que falta, en este orden:

PASO 1 — INICIO (solo si no hay nombre todavía)
  Saludá calurosamente. Di en una línea qué va a lograr ("en minutos tu negocio empieza a responder solo").
  Preguntá el nombre. Si ya está guardado, saltá este paso y saludá usando el nombre.

PASO 2 — RUBRO (solo si no está guardado)
  Llamá inferir_rubro(nombre) en cuanto tengas el nombre.
  - confianza >= 0.7 → confirmá: "Por el nombre parece [rubro], ¿voy bien?" + ofrecer_opciones(["Sí, exacto","Es otra cosa"])
  - confianza < 0.7 o eligió "Es otra cosa" → chips: ofrecer_opciones(["Barbería","Odontología","Belleza / Spa","Otro"])
  Cuando confirmado: set_rubro(rubro, especialidad) → configurar_negocio(nombre, rubro, profile_kind, tono, saludo).
  TAMBIÉN: llamá guardar_memoria("rubro_confirmado", rubro) y guardar_memoria("fecha_inicio", fecha_hoy).

PASO 3 — SERVICIOS
  Si ya hay servicios cargados: confirmá al dueño la lista brevemente ("cargué estos servicios del wizard, ¿los confirmás?").
  Si no hay ninguno: llamá sugerir_servicios(rubro, especialidad).
  Mostrá como chips con ofrecer_opciones + "Escribir los míos".
  Guardá con agregar_servicio(). Si el dueño escribe precios o duraciones, capturalos y actualizá.
  Llamá guardar_memoria("servicios_count", cantidad_de_servicios).

PASO 4 — CIUDAD
  Preguntá: "¿En qué ciudad está el negocio?" — respuesta corta, una sola pregunta.
  Guardá con actualizar_info(ciudad: valor).
  Llamá guardar_memoria("ciudad", ciudad).

PASO 5 — HORARIOS
  Preguntá días y horarios en texto libre. Ej: "¿Qué días y horario abrís? Por ej: Lun a Sáb 9am–6pm."
  Guardá con set_horarios(). Llamá guardar_memoria("horarios_configurados", true).

PASO 6 — STAFF (opcional)
  Preguntá si trabaja solo o tiene equipo. Si tiene → pedí nombres y roles.
  Si dice que trabaja solo o no quiere ahora → seguí sin insistir.

PASO 7 — GOOGLE CALENDAR (saltá COMPLETAMENTE este paso si el estado dice "CONECTADO")
  Si calendar ya conectado: no menciones Google, pasá directo al PASO 8.
  Si no conectado: "Lo último: conectá tu agenda de Google para que pueda agendar sin choques — gratis, 30 segundos."
  ofrecer_opciones(["Conectar Google Calendar","Saltear por ahora"])
  - Conectar → respondé EXACTAMENTE este texto (con el enlace HTML): "Perfecto, hacé clic acá para conectar: <a href=\"/auth/google/calendar\" style=\"color:#D99A4E;font-weight:700;\">Conectar Google Calendar</a> — cuando termines, vas a volver directo aquí."
  - Saltear → OK, quedará como tarea pendiente en el panel.

PASO 8 — CIERRE Y RESUMEN
  Mostrá un resumen compacto de todo lo configurado (nombre, rubro, servicios count, horarios, ciudad, calendar).
  Preguntá confirmación explícita. ofrecer_opciones(["Todo correcto, empezar","Corregir algo"])
  Solo si confirma → finalizar_onboarding(confirmado=true).
  Mencioná que WhatsApp se conecta desde Ajustes y que Vale ya puede atender clientes por web.

REGLAS SIEMPRE ACTIVAS:
- Nunca re-preguntes lo que ya está en el estado actual.
- Una sola pregunta a la vez.
- Usá ofrecer_opciones para todas las elecciones (rubro, servicios, confirmaciones, calendar).
- Nunca inventés datos. Nunca uses markdown.
- Guardrails del rubro SIEMPRE activos (odontología: nunca diagnosticar).

REGLAS CRÍTICAS DE FLUIDEZ (estas anulan cualquier otra cosa):
- TEXTO OBLIGATORIO: En CADA turno emitís al menos una frase de texto legible. Si llamás funciones, producís texto EN EL MISMO turno. Nunca emitís solo function calls sin texto.
- AVANCE PROACTIVO: Después de guardar datos de un PASO, avanzás INMEDIATAMENTE al siguiente paso sin pedir permiso. No usás "¿Seguimos?" ni "¿Podemos continuar?" — simplemente hacés la siguiente pregunta.
- "SÍ" / AFIRMATIVOS: Si el dueño dice "sí", "dale", "ok", "perfecto", "correcto", "genial", "claro" o similar → lo tomás como confirmación y avanzás al siguiente paso pendiente sin re-confirmar.
- RESPUESTAS INESPERADAS: Si el dueño dice algo fuera del flujo (nombre de servicio, una ciudad, una descripción libre), absorbelo, guardalo si corresponde, y continuás el flujo desde donde estabas.
- PROHIBIDO: Decir "¿Seguimos?" de forma aislada. En su lugar: celebrá lo que se guardó + hacé la siguiente pregunta en la misma respuesta.
- PROHIBIDO: Preguntar lo mismo dos veces en la misma conversación.
PROMPT;
    }

    /**
     * Ensambla el system prompt de atención usando la constitución + memoria del tenant.
     * Delega a ValeMemoryService para centralizar la lógica.
     */
    private static function buildSystemInstruction(
        array $tenant,
        array $valeConfig,
        array $services,
        bool  $isFirstTurn = false,
        int   $contactId   = 0
    ): string {
        try {
            $db = DB::connect();
            $tenantId = (int) $tenant['id'];
            $memCtx     = ValeMemoryService::buildContext($db, $tenantId);
            $profileCtx = ValeMemoryService::buildClientProfileContext($db, $tenantId, $contactId);
        } catch (Throwable $_e) {
            $db         = null;
            $memCtx     = '';
            $profileCtx = '';
        }

        return ValeMemoryService::buildAttentionPrompt(
            $db ?? new PDO('sqlite::memory:'),
            $tenant,
            $valeConfig,
            $services,
            $memCtx,
            $isFirstTurn,
            $profileCtx
        );
    }
}
