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

require_once __DIR__ . '/slug.php';
require_once __DIR__ . '/token_crypto.php';
require_once __DIR__ . '/google_calendar.php';

/**
 * FunctionDispatcher — ejecuta las funciones que Gemini decide llamar.
 * Cada handler escribe en la DB SIEMPRE scopeado por tenant_id y devuelve
 * un array (que se le manda de vuelta a Gemini como functionResponse).
 */
class FunctionDispatcher {
    public static function dispatch(PDO $db, int $tenantId, int $conversationId, int $contactId, string $name, array $args): array {
        switch ($name) {
            case 'get_availability':
                return self::getAvailability($db, $tenantId, $args);
            case 'escalate_to_owner':
                return self::escalateToOwner($db, $tenantId, $conversationId, $args);
            case 'crear_cita':
                return self::crearCita($db, $tenantId, $contactId, $args);
            case 'capturar_lead':
                return self::capturarLead($db, $tenantId, $conversationId, $contactId, $args);
            case 'actualizar_perfil_cliente':
                return self::actualizarPerfilCliente($db, $tenantId, $contactId, $args);
            default:
                return ['status' => 'error', 'mensaje' => "Función '$name' no implementada todavía."];
        }
    }

    /**
     * Dispatch del MODO OWNER (dueño editando su negocio desde el dashboard).
     * tenant_id siempre viene de la sesión autenticada, nunca del modelo.
     */
    public static function dispatchOwner(PDO $db, int $tenantId, string $name, array $args): array {
        switch ($name) {
            case 'agregar_servicio':
                return self::ownerAgregarServicio($db, $tenantId, $args);
            case 'editar_servicio':
                return self::ownerEditarServicio($db, $tenantId, $args);
            case 'eliminar_servicio':
                return self::ownerEliminarServicio($db, $tenantId, $args);
            case 'actualizar_horarios':
                return self::ownerActualizarHorarios($db, $tenantId, $args);
            case 'actualizar_info':
                return self::ownerActualizarInfo($db, $tenantId, $args);
            case 'agregar_staff':
                return self::ownerAgregarStaff($db, $tenantId, $args);
            case 'listar_servicios':
                return self::ownerListarServicios($db, $tenantId);
            default:
                return ['status' => 'error', 'mensaje' => "Función '$name' no reconocida."];
        }
    }

    private static function ownerAgregarServicio(PDO $db, int $tenantId, array $args): array {
        $nombre = mb_substr(trim((string)($args['nombre'] ?? '')), 0, 120);
        if ($nombre === '') return ['status' => 'error', 'mensaje' => 'Falta el nombre del servicio.'];
        $stmt = $db->prepare("SELECT id FROM services WHERE tenant_id = :t AND name = :n AND active = 1 LIMIT 1");
        $stmt->execute([':t' => $tenantId, ':n' => $nombre]);
        if ($stmt->fetch()) return ['status' => 'ya_existe', 'servicio' => $nombre];
        $precio = is_numeric($args['precio'] ?? null) ? (float)$args['precio'] : null;
        $dur    = is_numeric($args['duracion_min'] ?? null) ? (int)$args['duracion_min'] : null;
        $desc   = mb_substr(trim((string)($args['descripcion'] ?? '')), 0, 255) ?: null;
        $db->prepare("INSERT INTO services (tenant_id, name, duration_min, price_min, price_max, description, active) VALUES (:t,:n,:d,:p,:pmax,:desc,1)")
           ->execute([':t' => $tenantId, ':n' => $nombre, ':d' => $dur, ':p' => $precio, ':pmax' => null, ':desc' => $desc]);
        return ['status' => 'ok', 'servicio' => $nombre, 'mensaje' => "Servicio \"$nombre\" agregado."];
    }

    private static function ownerEditarServicio(PDO $db, int $tenantId, array $args): array {
        $id = (int)($args['id'] ?? 0);
        if (!$id) return ['status' => 'error', 'mensaje' => 'ID de servicio requerido. Usá listar_servicios para ver los IDs.'];
        $stmt = $db->prepare("SELECT id, name FROM services WHERE id = :id AND tenant_id = :t AND active = 1 LIMIT 1");
        $stmt->execute([':id' => $id, ':t' => $tenantId]);
        $svc = $stmt->fetch();
        if (!$svc) return ['status' => 'error', 'mensaje' => 'Servicio no encontrado.'];
        $sets = []; $params = [':id' => $id, ':t' => $tenantId];
        if (isset($args['nombre']) && trim($args['nombre']) !== '') {
            $sets[] = 'name = :name';
            $params[':name'] = mb_substr(trim($args['nombre']), 0, 120);
        }
        if (is_numeric($args['duracion_min'] ?? null)) {
            $sets[] = 'duration_min = :dur';
            $params[':dur'] = (int)$args['duracion_min'];
        }
        if (is_numeric($args['precio'] ?? null)) {
            $sets[] = 'price_min = :pmin'; $sets[] = 'price_max = :pmax';
            $params[':pmin'] = $params[':pmax'] = (float)$args['precio'];
        }
        if (isset($args['descripcion'])) {
            $sets[] = 'description = :desc';
            $params[':desc'] = mb_substr(trim($args['descripcion']), 0, 255) ?: null;
        }
        if (empty($sets)) return ['status' => 'error', 'mensaje' => 'No se especificaron cambios.'];
        try {
            $db->prepare("UPDATE services SET " . implode(', ', $sets) . " WHERE id = :id AND tenant_id = :t")
               ->execute($params);
        } catch (Throwable $e) {
            error_log("[ownerEditarServicio] tenant $tenantId svc $id: " . $e->getMessage());
            return ['status' => 'error', 'mensaje' => 'Error al actualizar el servicio.'];
        }
        return ['status' => 'ok', 'servicio' => $svc['name'], 'mensaje' => "Servicio actualizado."];
    }

    private static function ownerEliminarServicio(PDO $db, int $tenantId, array $args): array {
        $id = (int)($args['id'] ?? 0);
        if (!$id) return ['status' => 'error', 'mensaje' => 'ID de servicio requerido.'];
        $stmt = $db->prepare("SELECT name FROM services WHERE id = :id AND tenant_id = :t AND active = 1 LIMIT 1");
        $stmt->execute([':id' => $id, ':t' => $tenantId]);
        $svc = $stmt->fetch();
        if (!$svc) return ['status' => 'error', 'mensaje' => 'Servicio no encontrado.'];
        $db->prepare("UPDATE services SET active = 0 WHERE id = :id AND tenant_id = :t")
           ->execute([':id' => $id, ':t' => $tenantId]);
        return ['status' => 'ok', 'servicio' => $svc['name'], 'mensaje' => "Servicio \"{$svc['name']}\" archivado."];
    }

    private static function ownerActualizarHorarios(PDO $db, int $tenantId, array $args): array {
        $horarios = mb_substr(trim((string)($args['horarios'] ?? '')), 0, 400);
        if ($horarios === '') return ['status' => 'error', 'mensaje' => 'Faltan los horarios.'];
        $db->prepare("INSERT INTO vale_config (tenant_id, hours_json) VALUES (:t,:h) ON DUPLICATE KEY UPDATE hours_json = VALUES(hours_json)")
           ->execute([':t' => $tenantId, ':h' => json_encode(['texto' => $horarios], JSON_UNESCAPED_UNICODE)]);
        return ['status' => 'ok', 'horarios' => $horarios, 'mensaje' => "Horarios actualizados: $horarios"];
    }

    private static function ownerActualizarInfo(PDO $db, int $tenantId, array $args): array {
        $sets = []; $params = [':t' => $tenantId];
        if (!empty($args['nombre'])) {
            $sets[] = 'name = :name';
            $params[':name'] = mb_substr(trim($args['nombre']), 0, 160);
        }
        if (array_key_exists('ciudad', $args)) {
            $sets[] = 'ciudad = :ciudad';
            $params[':ciudad'] = mb_substr(trim($args['ciudad']), 0, 80) ?: null;
        }
        if (array_key_exists('direccion', $args)) {
            $sets[] = 'direccion = :dir';
            $params[':dir'] = mb_substr(trim($args['direccion']), 0, 255) ?: null;
        }
        if (array_key_exists('telefono', $args)) {
            $sets[] = 'telefono = :tel';
            $params[':tel'] = mb_substr(trim($args['telefono']), 0, 32) ?: null;
        }
        if (array_key_exists('descripcion', $args)) {
            $sets[] = 'descripcion = :desc';
            $params[':desc'] = mb_substr(trim($args['descripcion']), 0, 500) ?: null;
        }
        if (empty($sets)) return ['status' => 'error', 'mensaje' => 'No se especificaron campos a actualizar.'];
        $db->prepare("UPDATE tenants SET " . implode(', ', $sets) . " WHERE id = :t")->execute($params);
        if (!empty($params[':name']) && session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION['tenant_name'] = $params[':name'];
        }
        return ['status' => 'ok', 'mensaje' => 'Información del negocio actualizada.'];
    }

    private static function ownerAgregarStaff(PDO $db, int $tenantId, array $args): array {
        $nombre = mb_substr(trim((string)($args['nombre'] ?? '')), 0, 120);
        $rol    = mb_substr(trim((string)($args['rol']    ?? '')), 0, 80);
        if ($nombre === '') return ['status' => 'error', 'mensaje' => 'Falta el nombre del integrante.'];
        try {
            $db->prepare("INSERT INTO staff (tenant_id, nombre, rol, activo) VALUES (:t,:n,:r,1)")
               ->execute([':t' => $tenantId, ':n' => $nombre, ':r' => $rol ?: null]);
            return ['status' => 'ok', 'nombre' => $nombre, 'mensaje' => "$nombre agregado al equipo."];
        } catch (Throwable $e) {
            error_log('[dispatchOwner] agregar_staff: ' . $e->getMessage());
            return ['status' => 'error', 'mensaje' => 'No se pudo agregar el integrante. La tabla de staff puede necesitar una migración.'];
        }
    }

    private static function ownerListarServicios(PDO $db, int $tenantId): array {
        $stmt = $db->prepare("SELECT id, name, duration_min, price_min, description FROM services WHERE tenant_id = :t AND active = 1 ORDER BY id");
        $stmt->execute([':t' => $tenantId]);
        $rows = $stmt->fetchAll();
        if (empty($rows)) return ['status' => 'ok', 'servicios' => [], 'mensaje' => 'No hay servicios activos todavía.'];
        $lista = array_map(fn($r) => [
            'id' => (int)$r['id'],
            'nombre' => $r['name'],
            'duracion_min' => $r['duration_min'],
            'precio' => $r['price_min'],
            'descripcion' => $r['description'],
        ], $rows);
        return ['status' => 'ok', 'servicios' => $lista];
    }

    /**
     * Dispatch del MODO ONBOARDING (Vale configurando con el dueño).
     * Set de funciones separado del de atención. tenant_id viene de la sesión
     * autenticada del dueño, nunca del input del modelo.
     */
    public static function dispatchOnboarding(PDO $db, int $tenantId, string $name, array $args): array {
        switch ($name) {
            case 'guardar_contacto_dueno':
                return self::guardarContactoDueno($db, $tenantId, $args);
            case 'configurar_negocio':
                return self::configurarNegocio($db, $tenantId, $args);
            case 'agregar_servicio':
                return self::agregarServicioOnboarding($db, $tenantId, $args);
            case 'set_horarios':
                return self::setHorarios($db, $tenantId, $args);
            case 'finalizar_onboarding':
                return self::finalizarOnboarding($db, $tenantId, $args);
            case 'inferir_rubro':
                return self::inferirRubro($args);
            case 'set_rubro':
                return self::setRubro($db, $tenantId, $args);
            case 'sugerir_servicios':
                return self::sugerirServicios($args);
            case 'guardar_memoria':
                return self::guardarMemoriaOnboarding($db, $tenantId, $args);
            case 'actualizar_info':
                return self::ownerActualizarInfo($db, $tenantId, $args);
            default:
                return ['status' => 'error', 'mensaje' => "Función '$name' no válida en onboarding."];
        }
    }

    private static function guardarMemoriaOnboarding(PDO $db, int $tenantId, array $args): array {
        $clave = mb_substr(trim((string)($args['clave'] ?? '')), 0, 120);
        $valor = $args['valor'] ?? null;
        if ($clave === '') {
            return ['status' => 'error', 'mensaje' => 'Falta la clave de memoria.'];
        }
        try {
            require_once __DIR__ . '/vale_memory_service.php';
            ValeMemoryService::set($db, $tenantId, $clave, $valor);
            return ['status' => 'ok', 'clave' => $clave];
        } catch (\Throwable $e) {
            error_log("[guardarMemoriaOnboarding] $clave: " . $e->getMessage());
            return ['status' => 'error', 'mensaje' => 'No se pudo guardar en memoria.'];
        }
    }

    private static function inferirRubro(array $args): array {
        require_once __DIR__ . '/rubro_service.php';
        $nombre = trim((string)($args['nombre'] ?? ''));
        try {
            $result = RubroService::inferirRubro($nombre);
            return [
                'status'       => 'ok',
                'rubro'        => $result['rubro'],
                'especialidad' => $result['especialidad'],
                'confianza'    => $result['confianza'],
                // Instrucción explícita para el modelo sobre qué hacer con este resultado
                'instruccion'  => $result['confianza'] >= 0.7
                    ? "Confianza alta ({$result['confianza']}). Confirmá el rubro con el dueño usando ofrecer_opciones: [\"Sí, exacto\", \"Es otra cosa\"]. Di algo como: \"Por el nombre parece una {$result['rubro']}, ¿voy bien?\""
                    : "Confianza baja ({$result['confianza']}). Mostrá chips con ofrecer_opciones: [\"Barbería\", \"Odontología\", \"Belleza / Spa\", \"Otro\"] para que el dueño elija el rubro.",
            ];
        } catch (\Throwable $e) {
            error_log('[RubroService] inferirRubro error: ' . $e->getMessage());
            return [
                'status'      => 'fallback',
                'rubro'       => 'general',
                'especialidad' => null,
                'confianza'   => 0.0,
                'instruccion' => 'No se pudo inferir el rubro. Mostrá chips con ofrecer_opciones: ["Barbería", "Odontología", "Belleza / Spa", "Otro"].',
            ];
        }
    }

    private static function setRubro(PDO $db, int $tenantId, array $args): array {
        $rubrosValidos = ['barberia', 'odontologia', 'belleza', 'general'];
        $rubro = in_array($args['rubro'] ?? '', $rubrosValidos, true)
            ? $args['rubro']
            : 'general';
        $especialidad = isset($args['especialidad']) && (string)$args['especialidad'] !== 'null'
            ? mb_substr(trim((string)$args['especialidad']), 0, 120)
            : null;

        $db->prepare(
            "UPDATE tenants SET rubro = :r, especialidad = :e WHERE id = :t"
        )->execute([':r' => $rubro, ':e' => $especialidad, ':t' => $tenantId]);

        return ['status' => 'ok', 'rubro' => $rubro, 'especialidad' => $especialidad];
    }

    private static function sugerirServicios(array $args): array {
        require_once __DIR__ . '/rubro_service.php';
        $rubro       = $args['rubro']        ?? 'general';
        $especialidad = $args['especialidad'] ?? null;
        try {
            $servicios = RubroService::sugerirServicios($rubro, $especialidad ?: null);
            return [
                'status'    => 'ok',
                'servicios' => $servicios,
                'instruccion' => 'Ofrecé estos servicios como chips con ofrecer_opciones (los nombres), más la opción "Escribir los míos". Pedí que confirmen o editen precio y duración.',
            ];
        } catch (\Throwable $e) {
            error_log('[RubroService] sugerirServicios error: ' . $e->getMessage());
            return ['status' => 'fallback', 'servicios' => [], 'instruccion' => 'No se pudieron sugerir servicios. Preguntá cuáles ofrecen.'];
        }
    }

    private static function guardarContactoDueno(PDO $db, int $tenantId, array $args): array {
        $nombre = mb_substr(trim((string) ($args['nombre_completo'] ?? '')), 0, 120);
        $telefono = mb_substr(trim((string) ($args['telefono'] ?? '')), 0, 32);
        if ($nombre === '') {
            return ['status' => 'error', 'mensaje' => 'Falta el nombre del dueño.'];
        }
        // El dueño es el user 'owner' del tenant (el email ya viene del login con Google).
        $db->prepare("UPDATE users SET name = :n, phone = :p WHERE tenant_id = :t AND role = 'owner'")
            ->execute([':n' => $nombre, ':p' => $telefono !== '' ? $telefono : null, ':t' => $tenantId]);
        return ['status' => 'ok', 'nombre' => $nombre];
    }

    private static function configurarNegocio(PDO $db, int $tenantId, array $args): array {
        // Recortar a los límites de cada columna (Gemini a veces manda frases largas).
        $nombre = mb_substr(trim((string) ($args['nombre'] ?? '')), 0, 160);
        $rubro = mb_substr(trim((string) ($args['rubro'] ?? 'otro')), 0, 80);
        $allowedKinds = ['cita', 'reserva', 'pedido', 'lead', 'generic'];
        $profileKind = in_array($args['profile_kind'] ?? '', $allowedKinds, true) ? $args['profile_kind'] : 'generic';
        if ($nombre === '') {
            return ['status' => 'error', 'mensaje' => 'Falta el nombre del negocio.'];
        }

        $db->prepare(
            "UPDATE tenants SET name = :n, business_type = :bt, profile_kind = :pk, onboarding_status = 'configuring' WHERE id = :t"
        )->execute([':n' => $nombre, ':bt' => $rubro, ':pk' => $profileKind, ':t' => $tenantId]);

        // Slug público (una sola vez, desde el nombre real; el placeholder 'negocio-id' se reemplaza).
        $stmt = $db->prepare("SELECT public_slug FROM tenants WHERE id = :t AND deleted_at IS NULL");
        $stmt->execute([':t' => $tenantId]);
        $curSlug = (string) $stmt->fetchColumn();
        if ($curSlug === '' || strpos($curSlug, 'negocio-') === 0) {
            $slug = Slug::uniqueFor($db, $nombre, $tenantId);
            $db->prepare("UPDATE tenants SET public_slug = :s WHERE id = :t")->execute([':s' => $slug, ':t' => $tenantId]);
        }

        $tone = mb_substr(trim((string) ($args['tono'] ?? '')) ?: 'calido', 0, 40); // columna VARCHAR(40)
        $saludo = trim((string) ($args['saludo'] ?? ''));
        $db->prepare(
            "INSERT INTO vale_config (tenant_id, greeting, tone)
             VALUES (:t, :g, :to)
             ON DUPLICATE KEY UPDATE greeting = VALUES(greeting), tone = VALUES(tone)"
        )->execute([':t' => $tenantId, ':g' => $saludo, ':to' => $tone]);

        return ['status' => 'ok', 'nombre' => $nombre, 'rubro' => $rubro, 'profile_kind' => $profileKind];
    }

    private static function agregarServicioOnboarding(PDO $db, int $tenantId, array $args): array {
        $nombre = mb_substr(trim((string) ($args['nombre'] ?? '')), 0, 120); // columna VARCHAR(120)
        if ($nombre === '') {
            return ['status' => 'error', 'mensaje' => 'Falta el nombre del servicio.'];
        }
        // Dedup por nombre dentro del tenant.
        $stmt = $db->prepare("SELECT id FROM services WHERE tenant_id = :t AND name = :n LIMIT 1");
        $stmt->execute([':t' => $tenantId, ':n' => $nombre]);
        if ($stmt->fetch()) {
            return ['status' => 'ya_existe', 'servicio' => $nombre];
        }

        $intOrNull = static fn($v) => (isset($v) && is_numeric($v)) ? (int) $v : null;
        $floatOrNull = static fn($v) => (isset($v) && is_numeric($v)) ? (float) $v : null;

        $db->prepare(
            "INSERT INTO services (tenant_id, name, duration_min, price_min, price_max, description, active)
             VALUES (:t, :n, :d, :pmin, :pmax, :desc, 1)"
        )->execute([
            ':t' => $tenantId,
            ':n' => $nombre,
            ':d' => $intOrNull($args['duracion_min'] ?? null),
            ':pmin' => $floatOrNull($args['precio_min'] ?? null),
            ':pmax' => $floatOrNull($args['precio_max'] ?? null),
            ':desc' => mb_substr(trim((string) ($args['descripcion'] ?? '')), 0, 255) ?: null, // VARCHAR(255)
        ]);
        return ['status' => 'ok', 'servicio' => $nombre];
    }

    private static function setHorarios(PDO $db, int $tenantId, array $args): array {
        $horarios = trim((string) ($args['horarios'] ?? ''));
        if ($horarios === '') {
            return ['status' => 'error', 'mensaje' => 'Faltan los horarios.'];
        }
        $db->prepare(
            "INSERT INTO vale_config (tenant_id, hours_json)
             VALUES (:t, :h)
             ON DUPLICATE KEY UPDATE hours_json = VALUES(hours_json)"
        )->execute([':t' => $tenantId, ':h' => json_encode(['texto' => $horarios], JSON_UNESCAPED_UNICODE)]);
        return ['status' => 'ok', 'horarios' => $horarios];
    }

    private static function finalizarOnboarding(PDO $db, int $tenantId, array $args): array {
        if (empty($args['confirmado'])) {
            return ['status' => 'pendiente', 'mensaje' => 'Necesito que el dueño confirme el resumen antes de finalizar.'];
        }
        $db->prepare("UPDATE tenants SET onboarding_status = 'live' WHERE id = :t")->execute([':t' => $tenantId]);

        // Nombre del negocio para los hooks post-onboarding
        $negocioName = '';
        try {
            $r = $db->prepare("SELECT name FROM tenants WHERE id = :t AND deleted_at IS NULL LIMIT 1");
            $r->execute([':t' => $tenantId]);
            $negocioName = (string)($r->fetchColumn() ?: '');
        } catch (\Throwable $_e) {}

        // Generar descripción del directorio
        try {
            require_once __DIR__ . '/directorio_service.php';
            DirectorioService::generarDescripcion($db, $tenantId);
        } catch (\Throwable $_e) {
            error_log("[DirectorioService] hook onboarding tenant=$tenantId: " . $_e->getMessage());
        }

        // Capa A: bienvenida por WhatsApp al dueño
        try {
            require_once __DIR__ . '/platform_whatsapp.php';
            PlatformWhatsApp::sendBienvenida($db, $tenantId, $negocioName);
        } catch (\Throwable $_e) {
            error_log("[PlatformWhatsApp] bienvenida tenant=$tenantId: " . $_e->getMessage());
        }

        return ['status' => 'ok', 'done' => true];
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    /**
     * Obtiene el refresh token del dueño del tenant.
     * Prioridad: google_tokens cifrado → tenants.google_refresh_token (legacy).
     */
    private static function getTenantRefreshToken(PDO $db, int $tenantId): ?string {
        // Intento 1: token cifrado en google_tokens del owner
        $stmt = $db->prepare(
            "SELECT gt.refresh_token
             FROM google_tokens gt
             JOIN users u ON u.id = gt.user_id
             WHERE u.tenant_id = :t AND u.role = 'owner' AND gt.revoked_at IS NULL
             LIMIT 1"
        );
        $stmt->execute([':t' => $tenantId]);
        $enc = $stmt->fetchColumn();
        if ($enc) {
            try {
                return TokenCrypto::decrypt((string) $enc);
            } catch (\Throwable $e) {
                error_log("getTenantRefreshToken: decrypt failed for tenant $tenantId — " . $e->getMessage());
            }
        }

        // Intento 2: columna legacy en tenants (raw, sin cifrar)
        $stmt = $db->prepare(
            "SELECT google_refresh_token FROM tenants
             WHERE id = :t AND google_calendar_connected = 1 AND deleted_at IS NULL LIMIT 1"
        );
        $stmt->execute([':t' => $tenantId]);
        $raw = $stmt->fetchColumn();
        return $raw ?: null;
    }

    private static function getTenantTimezone(PDO $db, int $tenantId): string {
        $stmt = $db->prepare("SELECT timezone FROM tenants WHERE id = :t AND deleted_at IS NULL LIMIT 1");
        $stmt->execute([':t' => $tenantId]);
        return (string) ($stmt->fetchColumn() ?: 'America/New_York');
    }

    // ── Herramientas de atención ───────────────────────────────────────────────

    private static function escalateToOwner(PDO $db, int $tenantId, int $conversationId, array $args): array {
        $motivo  = mb_substr(trim((string) ($args['motivo']  ?? 'sin especificar')), 0, 80);
        $resumen = mb_substr(trim((string) ($args['resumen'] ?? '')), 0, 500);

        // Marcar conversación como pendiente de atención humana
        $db->prepare(
            "UPDATE conversations SET status = 'needs_owner' WHERE id = :id AND tenant_id = :t"
        )->execute([':id' => $conversationId, ':t' => $tenantId]);

        // Registro para el dashboard del dueño
        $db->prepare(
            "INSERT INTO agent_logs (tenant_id, type, gemini_call, payload)
             VALUES (:t, 'escalation', 0, :p)"
        )->execute([
            ':t' => $tenantId,
            ':p' => json_encode([
                'conversation_id' => $conversationId,
                'motivo'  => $motivo,
                'resumen' => $resumen,
            ], JSON_UNESCAPED_UNICODE),
        ]);

        return [
            'status'               => 'ok',
            'mensaje_para_cliente' => 'Dejame confirmarlo con el equipo y te aviso a la brevedad.',
        ];
    }

    private static function getAvailability(PDO $db, int $tenantId, array $args): array {
        $fecha = trim((string) ($args['fecha'] ?? ''));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
            return ['status' => 'error', 'mensaje' => 'Formato de fecha inválido. Usar YYYY-MM-DD.'];
        }

        // Rechazar fechas en el pasado
        if ($fecha < date('Y-m-d')) {
            return ['status' => 'error', 'mensaje' => 'La fecha ya pasó. Pedí una fecha futura.'];
        }

        // Config del agente: duracion_slot y calendar_id
        $stmt = $db->prepare("SELECT duracion_slot, calendar_id FROM vale_config WHERE tenant_id = :t LIMIT 1");
        $stmt->execute([':t' => $tenantId]);
        $cfg = $stmt->fetch();
        $durationMin = max(15, (int) ($cfg['duracion_slot'] ?? 30));
        $calendarId  = !empty($cfg['calendar_id']) ? $cfg['calendar_id'] : null;

        $refreshToken = self::getTenantRefreshToken($db, $tenantId);

        if (!$refreshToken || !$calendarId) {
            // Sin Calendar conectado o sin calendar_id: solo appointments locales
            return self::availabilityFromAppointments($db, $tenantId, $fecha, $durationMin);
        }

        $timezone = self::getTenantTimezone($db, $tenantId);
        [$openH, $closeH] = self::parseHoursForDate($db, $tenantId, $fecha);

        if ($openH === 0 && $closeH === 0) {
            return ['status' => 'ok', 'fecha' => $fecha, 'slots_libres' => [], 'mensaje' => 'El negocio está cerrado ese día.'];
        }

        $slots = GoogleCalendar::getAvailableSlots($refreshToken, $calendarId, $fecha, $durationMin, $timezone, $openH, $closeH);

        if ($slots === null) {
            // Falló la consulta a Google — fallback a la tabla local
            return self::availabilityFromAppointments($db, $tenantId, $fecha, $durationMin);
        }

        if (empty($slots)) {
            return [
                'status' => 'ok',
                'fecha'  => $fecha,
                'slots_libres' => [],
                'mensaje' => 'No hay horarios disponibles para ese día.',
            ];
        }

        return ['status' => 'ok', 'fecha' => $fecha, 'slots_libres' => $slots];
    }

    /** Fallback cuando no hay Calendar conectado: genera slots y descuenta appointments. */
    private static function availabilityFromAppointments(PDO $db, int $tenantId, string $fecha, int $durationMin): array {
        $stmt = $db->prepare(
            "SELECT TIME_FORMAT(scheduled_at, '%H:%i') AS hora
             FROM appointments
             WHERE tenant_id = :t
               AND DATE(scheduled_at) = :f
               AND status IN ('booked','confirmed')"
        );
        $stmt->execute([':t' => $tenantId, ':f' => $fecha]);
        $booked = array_column($stmt->fetchAll(), 'hora');

        // Read configured hours for this tenant
        [$openH, $closeH] = self::parseHoursForDate($db, $tenantId, $fecha);

        $slots = [];
        $start = strtotime($fecha . ' ' . sprintf('%02d:00:00', $openH));
        $end   = strtotime($fecha . ' ' . sprintf('%02d:00:00', $closeH));
        $step  = $durationMin * 60;

        for ($ts = $start; $ts + $step <= $end; $ts += $step) {
            $hm = date('H:i', $ts);
            if (!in_array($hm, $booked, true)) {
                $slots[] = $hm;
            }
        }

        if (empty($slots)) {
            $msg = ($openH === 0 && $closeH === 0)
                ? 'El negocio está cerrado ese día.'
                : 'No hay horarios disponibles para ese día.';
            return ['status' => 'ok', 'fecha' => $fecha, 'slots_libres' => [], 'mensaje' => $msg];
        }

        return ['status' => 'ok', 'fecha' => $fecha, 'slots_libres' => $slots];
    }

    /**
     * Returns [openHour, closeHour] integers for $fecha based on configured hours_json.
     * Falls back to [9, 18] if hours are not configured or cannot be parsed.
     * Returns [0, 0] if the business is explicitly closed that day.
     */
    private static function parseHoursForDate(PDO $db, int $tenantId, string $fecha): array {
        try {
            $stmt = $db->prepare("SELECT hours_json FROM vale_config WHERE tenant_id = :t LIMIT 1");
            $stmt->execute([':t' => $tenantId]);
            $row = $stmt->fetchColumn();
        } catch (\Throwable $_) {
            return [9, 18];
        }

        if (!$row) return [9, 18];
        $hd = json_decode($row, true);
        $texto = is_array($hd) ? trim($hd['texto'] ?? '') : '';
        if ($texto === '') return [9, 18];

        // Day-of-week number (1=Mon ... 7=Sun) for the requested date
        $dayNum = (int) date('N', strtotime($fecha));
        $esDay  = ['lun'=>1,'mar'=>2,'mié'=>3,'mie'=>3,'jue'=>4,'vie'=>5,'sáb'=>6,'sab'=>6,'dom'=>7];

        foreach (explode(',', strtolower($texto)) as $seg) {
            $seg = trim($seg);
            // "cerrado" segment
            if (str_contains($seg, 'cerrado')) {
                if (preg_match('/([a-záéíóúü]+)/', $seg, $dm)) {
                    $d = $esDay[$dm[1]] ?? null;
                    if ($d && $d === $dayNum) return [0, 0];
                }
                continue;
            }
            // Day range: "lun-vie 9-18" or "lun a vie 9-18"
            if (preg_match('/([a-záéíóúü]+)[- a]+([a-záéíóúü]+)\s+(\d+)[:\.]?(\d*)\s*[-a]\s*(\d+)[:\.]?(\d*)/', $seg, $m)) {
                $d1 = $esDay[$m[1]] ?? null;
                $d2 = $esDay[$m[2]] ?? null;
                if ($d1 && $d2 && $dayNum >= $d1 && $dayNum <= $d2) {
                    return [(int)$m[3], (int)$m[5]];
                }
            }
            // Single day: "sáb 9-13"
            if (preg_match('/^([a-záéíóúü]+)\s+(\d+)[:\.]?(\d*)\s*[-a]\s*(\d+)[:\.]?(\d*)/', $seg, $m)) {
                $d = $esDay[$m[1]] ?? null;
                if ($d && $d === $dayNum) {
                    return [(int)$m[2], (int)$m[4]];
                }
            }
        }

        // No day-specific match — try to extract any generic time range as fallback
        if (preg_match('/(\d+)[:\.]?(\d*)\s*[-a]\s*(\d+)[:\.]?(\d*)/', strtolower($texto), $m)) {
            $open  = (int)$m[1];
            $close = (int)$m[3];
            if ($open < $close && $close <= 24) {
                return [$open, $close];
            }
        }

        return [9, 18];
    }

    private static function crearCita(PDO $db, int $tenantId, int $contactId, array $args): array {
        $servicio = trim((string) ($args['servicio'] ?? ''));
        $fechaRaw = trim((string) ($args['fecha_hora'] ?? ''));
        if ($servicio === '' || $fechaRaw === '') {
            return ['status' => 'error', 'mensaje' => 'Falta el servicio o la fecha/hora.'];
        }
        $ts = strtotime($fechaRaw);
        if ($ts === false) {
            return ['status' => 'error', 'mensaje' => 'No se entendió la fecha/hora; pedile al cliente que la aclare.'];
        }
        $scheduledAt = date('Y-m-d H:i:s', $ts);

        // 1) Dedup: este mismo cliente ya tiene esa cita activa -> no duplicar.
        $stmt = $db->prepare(
            "SELECT id FROM appointments
             WHERE tenant_id = :t AND contact_id = :c AND scheduled_at = :sa
               AND status IN ('booked','confirmed') LIMIT 1"
        );
        $stmt->execute([':t' => $tenantId, ':c' => $contactId, ':sa' => $scheduledAt]);
        if ($row = $stmt->fetch()) {
            return [
                'status' => 'ya_agendada',
                'mensaje' => 'El cliente ya tiene esta cita agendada. Confirmásela sin crear otra.',
                'cita_id' => (int) $row['id'],
                'servicio' => $servicio,
                'fecha_hora' => date('Y-m-d H:i', $ts),
            ];
        }

        // 2) Disponibilidad: ese horario ya está ocupado por otro cliente del negocio.
        $stmt = $db->prepare(
            "SELECT id FROM appointments
             WHERE tenant_id = :t AND scheduled_at = :sa
               AND status IN ('booked','confirmed') LIMIT 1"
        );
        $stmt->execute([':t' => $tenantId, ':sa' => $scheduledAt]);
        if ($stmt->fetch()) {
            return [
                'status' => 'ocupado',
                'mensaje' => 'Ese horario ya está ocupado. Ofrecé al cliente otro horario distinto.',
            ];
        }

        // Config del agente: duracion_slot y calendar_id del tenant
        $stmtCfg = $db->prepare("SELECT duracion_slot, calendar_id FROM vale_config WHERE tenant_id = :t LIMIT 1");
        $stmtCfg->execute([':t' => $tenantId]);
        $agentCfg = $stmtCfg->fetch();
        $defaultDuration = max(15, (int) ($agentCfg['duracion_slot'] ?? 30));
        $calendarId      = !empty($agentCfg['calendar_id']) ? $agentCfg['calendar_id'] : null;

        // ── GUARD: verificar disponibilidad real en Google Calendar ──────────
        // Independientemente de si Gemini llamó o no a get_availability antes,
        // siempre verificamos que el slot esté libre en Calendar.
        $refreshToken = self::getTenantRefreshToken($db, $tenantId);
        if ($refreshToken && $calendarId) {
            $timezone  = self::getTenantTimezone($db, $tenantId);
            $fechaSolo = date('Y-m-d', $ts);
            $horaSolo  = date('H:i',   $ts);
            $slots = GoogleCalendar::getAvailableSlots(
                $refreshToken, $calendarId, $fechaSolo, $defaultDuration, $timezone
            );
            if ($slots !== null && !in_array($horaSolo, $slots, true)) {
                return [
                    'status'  => 'ocupado',
                    'mensaje' => 'El Calendar del negocio ya tiene ese horario ocupado. Ofrecé al cliente otro horario disponible.',
                ];
            }
        } elseif ($refreshToken && !$calendarId) {
            error_log("[crearCita] tenant $tenantId: Google Calendar conectado pero calendar_id NULL — disponibilidad no verificada.");
        }
        // ── FIN GUARD ────────────────────────────────────────────────────────

        // Best-effort match de un servicio del tenant por nombre.
        $serviceId   = null;
        $durationMin = $defaultDuration;
        $stmt = $db->prepare("SELECT id, duration_min FROM services WHERE tenant_id = :t AND active = 1 AND name LIKE :n ORDER BY id LIMIT 1");
        $stmt->execute([':t' => $tenantId, ':n' => '%' . $servicio . '%']);
        $row = $stmt->fetch();
        if ($row) {
            $serviceId = (int) $row['id'];
            if (!empty($row['duration_min'])) {
                $durationMin = (int) $row['duration_min'];
            }
        }

        self::maybeUpdateContactName($db, $tenantId, $contactId, $args['nombre_cliente'] ?? null);

        try {
            $stmt = $db->prepare(
                "INSERT INTO appointments (tenant_id, contact_id, service_id, scheduled_at, status, source)
                 VALUES (:t, :c, :s, :sa, 'booked', 'ai')"
            );
            $stmt->execute([':t' => $tenantId, ':c' => $contactId, ':s' => $serviceId, ':sa' => $scheduledAt]);
            $citaId = (int) $db->lastInsertId();
        } catch (Throwable $e) {
            error_log("[crearCita] INSERT appointments failed tenant $tenantId: " . $e->getMessage());
            return ['status' => 'error', 'mensaje' => 'No se pudo guardar la cita. Intentá de nuevo.'];
        }
        if (!$citaId) {
            error_log("[crearCita] tenant $tenantId: INSERT no retornó ID.");
            return ['status' => 'error', 'mensaje' => 'No se pudo guardar la cita. Intentá de nuevo.'];
        }

        // Lead "en caliente": todo cliente que reserva queda registrado como lead (booked).
        self::ensureLead($db, $tenantId, $contactId, 'booked');

        // Confirmación de cita por email si el cliente tiene email registrado
        try {
            $stmtEmail = $db->prepare(
                "SELECT cp.email, c.name FROM client_profiles cp
                 JOIN contacts c ON c.id = cp.contact_id AND c.tenant_id = cp.tenant_id
                 WHERE cp.tenant_id = :t AND cp.contact_id = :c LIMIT 1"
            );
            $stmtEmail->execute([':t' => $tenantId, ':c' => $contactId]);
            $emailRow = $stmtEmail->fetch();
            if ($emailRow && !empty($emailRow['email'])) {
                if (!class_exists('Mailer', false)) {
                    require_once __DIR__ . '/mailer.php';
                }
                $stmtTenant = $db->prepare("SELECT name, timezone FROM tenants WHERE id = :t AND deleted_at IS NULL LIMIT 1");
                $stmtTenant->execute([':t' => $tenantId]);
                $tenantRow  = $stmtTenant->fetch();
                $tenantName = $tenantRow['name'] ?? '';
                $tz         = $tenantRow['timezone'] ?? 'America/Managua';
                Mailer::sendAppointmentConfirmation(
                    $db,
                    $emailRow['email'],
                    $emailRow['name'] ?: ($args['nombre_cliente'] ?? ''),
                    $tenantName,
                    $servicio,
                    $scheduledAt,
                    $tz,
                    $tenantId
                );
            }
        } catch (Throwable $_e) {
            error_log('[crearCita] mailer error: ' . $_e->getMessage());
        }

        // Sincronizar con el Google Calendar REAL del dueño (si está conectado y tiene calendar_id).
        $refreshToken = self::getTenantRefreshToken($db, $tenantId);
        if ($refreshToken && $calendarId) {
            $timezone = self::getTenantTimezone($db, $tenantId);

            // Datos del cliente para la descripción del evento
            $stmtC = $db->prepare("SELECT name, phone FROM contacts WHERE id = :id AND tenant_id = :t LIMIT 1");
            $stmtC->execute([':id' => $contactId, ':t' => $tenantId]);
            $contact     = $stmtC->fetch();
            $clientName  = $contact['name']  ?? ($args['nombre_cliente'] ?? '');
            $clientPhone = $contact['phone'] ?? '';
            $notas       = trim((string) ($args['notas'] ?? ''));

            $descParts = [];
            if ($clientName)  $descParts[] = 'Cliente: ' . $clientName;
            if ($clientPhone) $descParts[] = 'Teléfono: ' . $clientPhone;
            if ($notas)       $descParts[] = 'Notas: ' . $notas;
            $descParts[] = 'Agendado por Vale AI';
            $description = implode("\n", $descParts);

            $eventId = GoogleCalendar::createEvent(
                $refreshToken,
                'Cita: ' . $servicio . ($clientName ? " — $clientName" : ''),
                $scheduledAt,
                $durationMin,
                $timezone,
                $calendarId,
                $description
            );
            if ($eventId) {
                $db->prepare("UPDATE appointments SET gcal_event_id = :e WHERE id = :id")->execute([':e' => $eventId, ':id' => $citaId]);
            }
        } elseif ($refreshToken && !$calendarId) {
            error_log("[crearCita] tenant $tenantId: cita $citaId guardada en DB pero NO en Google Calendar — calendar_id es NULL. Reconectar Google Calendar en el panel.");
        }

        return [
            'status' => 'ok',
            'cita_id' => $citaId,
            'servicio' => $servicio,
            'fecha_hora' => date('Y-m-d H:i', $ts),
        ];
    }

    /** Crea o actualiza el lead del contacto (un lead por contacto y tenant). */
    private static function ensureLead(PDO $db, int $tenantId, int $contactId, string $status): void {
        try {
            $stmt = $db->prepare("SELECT id FROM leads WHERE tenant_id = :t AND contact_id = :c LIMIT 1");
            $stmt->execute([':t' => $tenantId, ':c' => $contactId]);
            $row = $stmt->fetch();
            if ($row) {
                $db->prepare("UPDATE leads SET status = :s WHERE id = :id")->execute([':s' => $status, ':id' => $row['id']]);
            } else {
                $db->prepare("INSERT INTO leads (tenant_id, contact_id, status, source) VALUES (:t, :c, :s, 'ai')")
                    ->execute([':t' => $tenantId, ':c' => $contactId, ':s' => $status]);
            }
        } catch (Throwable $e) {
            error_log("[ensureLead] tenant $tenantId contact $contactId: " . $e->getMessage());
        }
    }

    private static function capturarLead(PDO $db, int $tenantId, int $conversationId, int $contactId, array $args): array {
        $interes = trim((string) ($args['interes'] ?? ''));
        $valor = (isset($args['valor_estimado']) && is_numeric($args['valor_estimado']))
            ? (float) $args['valor_estimado'] : null;

        self::maybeUpdateContactName($db, $tenantId, $contactId, $args['nombre'] ?? null);

        $stmt = $db->prepare(
            "INSERT INTO leads (tenant_id, contact_id, conversation_id, status, value_estimate, source)
             VALUES (:t, :c, :cv, 'new', :v, 'ai')"
        );
        $stmt->execute([':t' => $tenantId, ':c' => $contactId, ':cv' => $conversationId, ':v' => $valor]);

        return ['status' => 'ok', 'lead_id' => (int) $db->lastInsertId(), 'interes' => $interes];
    }

    /** Completa el nombre del contacto si todavía es un placeholder. */
    private static function maybeUpdateContactName(PDO $db, int $tenantId, int $contactId, ?string $name): void {
        $name = $name !== null ? mb_substr(trim($name), 0, 120) : '';
        if ($name === '') {
            return;
        }
        $stmt = $db->prepare(
            "UPDATE contacts SET name = :n
             WHERE id = :id AND tenant_id = :t AND (name IS NULL OR name IN ('Cliente', 'Visitante web'))"
        );
        $stmt->execute([':n' => $name, ':id' => $contactId, ':t' => $tenantId]);
    }

    private static function actualizarPerfilCliente(PDO $db, int $tenantId, int $contactId, array $args): array {
        try {
            // Garantizar que existe la fila del perfil
            $db->prepare(
                "INSERT INTO client_profiles (tenant_id, contact_id) VALUES (:t, :c)
                 ON DUPLICATE KEY UPDATE updated_at = NOW()"
            )->execute([':t' => $tenantId, ':c' => $contactId]);

            $stmt = $db->prepare(
                "SELECT email, birthday, notes, preferences FROM client_profiles
                 WHERE tenant_id = :t AND contact_id = :c LIMIT 1"
            );
            $stmt->execute([':t' => $tenantId, ':c' => $contactId]);
            $profile = $stmt->fetch();

            $sets   = [];
            $params = [':t' => $tenantId, ':c' => $contactId];

            if (!empty($args['telefono'])) {
                $phone = mb_substr(trim($args['telefono']), 0, 40);
                $db->prepare("UPDATE contacts SET phone = :p WHERE id = :c AND tenant_id = :t")
                   ->execute([':p' => $phone, ':c' => $contactId, ':t' => $tenantId]);
            }

            if (!empty($args['email'])) {
                $sets[] = 'email = :email';
                $params[':email'] = mb_substr(trim($args['email']), 0, 160);
            }

            if (!empty($args['birthday'])) {
                $sets[] = 'birthday = :birthday';
                $params[':birthday'] = $args['birthday'];
            }

            if (!empty($args['notas'])) {
                $existing = $profile['notes'] ? trim($profile['notes']) . "\n" : '';
                $sets[] = 'notes = :notes';
                $params[':notes'] = $existing . trim($args['notas']);
            }

            if (!empty($args['clave']) && isset($args['valor']) && $args['valor'] !== '') {
                $prefs = json_decode($profile['preferences'] ?: '{}', true) ?: [];
                $prefs[mb_substr(trim($args['clave']), 0, 60)] = mb_substr(trim($args['valor']), 0, 255);
                $sets[] = 'preferences = :prefs';
                $params[':prefs'] = json_encode($prefs, JSON_UNESCAPED_UNICODE);
            }

            if ($sets) {
                $db->prepare(
                    "UPDATE client_profiles SET " . implode(', ', $sets) .
                    " WHERE tenant_id = :t AND contact_id = :c"
                )->execute($params);
            }

            // Email de bienvenida + alerta interna la primera vez que se captura el email del cliente
            $isFirstEmail = !empty($args['email']) && empty($profile['email']);
            if ($isFirstEmail) {
                try {
                    if (!class_exists('Mailer', false)) {
                        require_once __DIR__ . '/mailer.php';
                    }
                    $stmtC = $db->prepare("SELECT name FROM contacts WHERE id = :c AND tenant_id = :t LIMIT 1");
                    $stmtC->execute([':c' => $contactId, ':t' => $tenantId]);
                    $clientName = $stmtC->fetchColumn() ?: '';

                    $stmtT = $db->prepare("SELECT name FROM tenants WHERE id = :t AND deleted_at IS NULL LIMIT 1");
                    $stmtT->execute([':t' => $tenantId]);
                    $tenantName = $stmtT->fetchColumn() ?: 'el negocio';

                    $clientEmail = mb_substr(trim($args['email']), 0, 160);
                    Mailer::sendClientWelcome($db, $clientEmail, $clientName, $tenantName, $tenantId);
                    Mailer::sendNewClientAlert($db, $clientName, $clientEmail, $tenantName, $tenantId);
                } catch (Throwable $_e) {
                    error_log('[actualizarPerfilCliente] mailer error: ' . $_e->getMessage());
                }
            }

            return ['status' => 'ok'];
        } catch (Throwable $e) {
            error_log("[actualizarPerfilCliente] tenant $tenantId contact $contactId: " . $e->getMessage());
            return ['status' => 'error', 'mensaje' => 'No se pudo actualizar el perfil.'];
        }
    }
}
