#!/bin/bash
# ================================================================
# OKRFEDEF — Sprint 06: Claude API + ElevenLabs Voice
# Motor de IA central + consultor de voz en sala
# Ejecutar desde: /home/evolucionamos/public_html/estrategia
# Comando: bash sprint_06_claude_voice.sh
# ================================================================
set -e
echo "========================================="
echo "  OKRFEDEF — Sprint 06: IA + Voz"
echo "========================================="

php artisan make:controller AiController
php artisan make:controller VoiceController
php artisan make:job VoiceSynthesisJob
php artisan make:job MonthlyAnalysisJob
php artisan make:job QuarterlyReportJob
php artisan make:event AiResponseReady
php artisan make:event VoicePlaying

echo ">>> Archivos base creados. Escribiendo servicios IA..."

# ── 1. CLAUDE SERVICE (motor central) ────────────────────────────
cat > app/Services/ClaudeService.php << 'PHP'
<?php
namespace App\Services;

use App\Models\AiMessage;
use App\Models\Project;
use Illuminate\Support\Facades\Http;

class ClaudeService
{
    private string $apiKey;
    private string $model;
    private int    $maxTokens;
    private string $baseUrl = 'https://api.anthropic.com/v1/messages';

    public function __construct()
    {
        $this->apiKey    = config('services.anthropic.api_key');
        $this->model     = config('services.anthropic.model', 'claude-sonnet-4-20250514');
        $this->maxTokens = config('services.anthropic.max_tokens', 2000);
    }

    /**
     * Llamada estándar a Claude (síncrona)
     */
    public function ask(string $prompt, string $systemPrompt = '', int $maxTokens = 0): ?string
    {
        $messages = [['role' => 'user', 'content' => $prompt]];

        $body = [
            'model'      => $this->model,
            'max_tokens' => $maxTokens ?: $this->maxTokens,
            'messages'   => $messages,
        ];

        if ($systemPrompt) {
            $body['system'] = $systemPrompt;
        }

        $response = Http::withHeaders([
            'x-api-key'         => $this->apiKey,
            'anthropic-version' => '2023-06-01',
            'content-type'      => 'application/json',
        ])->timeout(120)->post($this->baseUrl, $body);

        if ($response->successful()) {
            return $response->json('content.0.text');
        }

        \Log::error('Claude API error', [
            'status'  => $response->status(),
            'body'    => $response->body(),
        ]);

        return null;
    }

    /**
     * Análisis mensual del plan estratégico
     */
    public function monthlyAnalysis(Project $project, array $cycleData): string
    {
        $systemPrompt = $this->buildSystemPrompt($project);

        $krsText = collect($cycleData['krs'] ?? [])->map(fn($kr) =>
            "- {$kr['title']}: {$kr['current']}/{$kr['target']} {$kr['unit']} → {$kr['score']}% ({$kr['traffic_light']})"
        )->join("\n");

        $prompt = <<<PROMPT
Genera el análisis mensual del plan estratégico de {$project->organization->name}.

ESTADO DEL PLAN — {$cycleData['period']}:
Score general: {$cycleData['score']}%
En verde: {$cycleData['green']} objetivos
En amarillo: {$cycleData['yellow']} objetivos
En rojo: {$cycleData['red']} objetivos

KEY RESULTS:
{$krsText}

Responde en este formato exacto:

**RESUMEN EJECUTIVO** (2 frases)
[Estado del plan este mes]

**LOGROS DEL MES**
- [Logro 1 concreto con número]
- [Logro 2]
- [Logro 3 si aplica]

**ALERTAS QUE REQUIEREN ATENCIÓN**
- [KR o situación en riesgo con causa probable]
- [Otra alerta si aplica]

**RECOMENDACIONES PARA LA PRÓXIMA SEMANA**
- [Acción concreta 1 — responsable sugerido]
- [Acción concreta 2]

**PREGUNTA PARA LA GERENCIA**
[La pregunta más importante que debe resolver la Gerente esta semana]

Tono directo, sin rodeos. Máximo 300 palabras.
PROMPT;

        return $this->ask($prompt, $systemPrompt, 1000) ?? 'Error generando análisis. Por favor intenta nuevamente.';
    }

    /**
     * Análisis de retrospectiva trimestral
     */
    public function quarterlyRetro(Project $project, array $cycleData): string
    {
        $systemPrompt = $this->buildSystemPrompt($project);

        $prompt = <<<PROMPT
Genera el análisis de retrospectiva trimestral del plan estratégico de {$project->organization->name}.

CICLO: Q{$cycleData['quarter']} {$cycleData['year']}
Score final: {$cycleData['final_score']}%
Objetivos alcanzados (≥70%): {$cycleData['achieved']}/{$cycleData['total']}

OBJETIVOS Y RESULTADOS:
{$cycleData['objectives_summary']}

APUESTAS ESTRATÉGICAS ACTIVAS:
{$cycleData['bets_summary']}

Responde en este formato:

**BALANCE DEL TRIMESTRE** (3 párrafos)
[Qué se logró, qué no se logró, qué aprendimos]

**HIPÓTESIS VALIDADAS** ✅
- [Apuesta o supuesto que resultó ser verdad]

**HIPÓTESIS REFUTADAS** ❌
- [Apuesta o supuesto que resultó ser falso — y qué aprendemos de eso]

**AJUSTES RECOMENDADOS PARA EL PRÓXIMO CICLO**
- [OKR o apuesta que debe modificarse]
- [Nueva prioridad que emergió]
- [Renuncia que vale la pena reconsiderar]

**PREGUNTA ESTRATÉGICA PARA EL EQUIPO**
[La pregunta más importante antes de diseñar el siguiente ciclo]

Máximo 400 palabras. Tono de consultor experto.
PROMPT;

        return $this->ask($prompt, $systemPrompt, 1500) ?? 'Error generando retrospectiva.';
    }

    /**
     * Respuesta al chat en vivo (modo consultivo)
     */
    public function liveChat(Project $project, string $userQuestion, array $sessionContext = []): string
    {
        $systemPrompt = $this->buildSystemPrompt($project);

        $contextText = '';
        if (!empty($sessionContext)) {
            $contextText = "\nCONTEXTO DE LA SESIÓN ACTUAL:\n" . json_encode($sessionContext, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        }

        $prompt = "PREGUNTA DEL FACILITADOR O PARTICIPANTE:\n{$userQuestion}\n{$contextText}";

        return $this->ask($prompt, $systemPrompt, 600) ?? 'No pude procesar la pregunta. Intenta nuevamente.';
    }

    /**
     * System prompt con contexto completo del proyecto
     */
    public function buildSystemPrompt(Project $project): string
    {
        $project->load('organization', 'dofaSessions', 'strategicBets');

        $latestDofa = $project->dofaSessions()->where('status','completed')->latest()->first();
        $activeBets = $project->strategicBets()->where('status','active')->get();

        $prompt  = "Eres el consultor estratégico de IA de OKRFEDEF, especializado en el sector solidario colombiano.\n\n";
        $prompt .= "ORGANIZACIÓN: {$project->organization->name}\n";
        $prompt .= "PROYECTO: {$project->name}\n";
        $prompt .= "PERÍODO: {$project->period_start->format('Y')} - {$project->period_end->format('Y')}\n\n";

        if ($latestDofa) {
            $prompt .= "POSICIONAMIENTO ESTRATÉGICO (DOFA):\n";
            $prompt .= "- Cuadrante dominante: {$latestDofa->quadrant}\n";
            $prompt .= "- MEFI: {$latestDofa->mefi_total} | MEFE: {$latestDofa->mefe_total}\n\n";
        }

        if ($activeBets->isNotEmpty()) {
            $prompt .= "APUESTAS ESTRATÉGICAS ACTIVAS:\n";
            foreach ($activeBets as $bet) {
                $prompt .= "- {$bet->hypothesis} (score: {$bet->score})\n";
            }
            $prompt .= "\n";
        }

        $prompt .= "INSTRUCCIONES:\n";
        $prompt .= "- Responde siempre en español, tono directo y profesional\n";
        $prompt .= "- Usa datos concretos cuando estén disponibles\n";
        $prompt .= "- Referencia el contexto específico de FEDEF cuando sea relevante\n";
        $prompt .= "- Sé provocador y honesto, no complaciente\n";

        return $prompt;
    }

    /**
     * Guarda el mensaje en el historial
     */
    public function saveMessage(int $projectId, string $module, string $prompt, string $response, string $trigger = 'consultive', ?int $userId = null): AiMessage
    {
        return AiMessage::create([
            'project_id'  => $projectId,
            'user_id'     => $userId ?? auth()->id(),
            'module'      => $module,
            'prompt'      => $prompt,
            'response'    => $response,
            'model'       => $this->model,
            'trigger'     => $trigger,
        ]);
    }
}
PHP

# ── 2. VOICE SERVICE (ElevenLabs) ────────────────────────────────
cat > app/Services/VoiceService.php << 'PHP'
<?php
namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class VoiceService
{
    private string $apiKey;
    private string $voiceId;
    private string $model;

    // Frases pre-cargadas para los retiros (evitar latencia)
    private array $preloadedPhrases = [
        'bienvenida'    => 'Bienvenidos al retiro estratégico. Hoy vamos a construir el futuro de la organización juntos.',
        'dofa_inicio'   => 'Iniciamos la calificación del DOFA. Cada uno tiene cinco minutos para calificar todos los factores desde su celular. En silencio por favor.',
        'aspiration'    => 'Ahora quiero que cada uno responda esta pregunta de forma individual: ¿cuándo gana esta organización? Escríbelo con sus propias palabras.',
        'renuncias'     => 'La estrategia no es solo elegir qué hacer. Es elegir qué no hacer. Eso duele. Vamos a votar las renuncias estratégicas.',
        'okr_builder'   => 'Ahora construimos los OKRs. Yo propongo los objetivos y ustedes proponen los Key Results desde sus celulares.',
        'cierre_dia1'   => 'Excelente trabajo hoy. Mañana continuamos con las apuestas estratégicas y el diseño de los OKRs.',
        'cierre_retiro' => 'Este plan estratégico ahora les pertenece. Cada uno tiene un rol claro. El éxito depende de la disciplina en el seguimiento semanal.',
    ];

    public function __construct()
    {
        $this->apiKey  = config('services.elevenlabs.api_key');
        $this->voiceId = config('services.elevenlabs.voice_id');
        $this->model   = config('services.elevenlabs.model', 'eleven_multilingual_v2');
    }

    /**
     * Convierte texto a voz y devuelve la URL del audio
     */
    public function synthesize(string $text, string $cacheKey = null): ?string
    {
        if (!$this->apiKey || $this->apiKey === 'PENDIENTE') {
            return null;
        }

        // Revisar cache primero
        if ($cacheKey) {
            $cached = $this->getCached($cacheKey);
            if ($cached) return $cached;
        }

        $response = Http::withHeaders([
            'xi-api-key'   => $this->apiKey,
            'content-type' => 'application/json',
        ])->timeout(30)->post("https://api.elevenlabs.io/v1/text-to-speech/{$this->voiceId}", [
            'text'       => $text,
            'model_id'   => $this->model,
            'voice_settings' => [
                'stability'        => 0.5,
                'similarity_boost' => 0.8,
                'style'            => 0.2,
                'use_speaker_boost'=> true,
            ],
        ]);

        if ($response->successful()) {
            $filename = 'voice_cache/' . ($cacheKey ?? md5($text)) . '.mp3';
            Storage::disk('public')->put($filename, $response->body());
            return Storage::disk('public')->url($filename);
        }

        \Log::error('ElevenLabs error', ['status' => $response->status()]);
        return null;
    }

    /**
     * Pre-cargar frases estándar de los retiros
     */
    public function preloadRetiroAudios(): array
    {
        $urls = [];
        foreach ($this->preloadedPhrases as $key => $text) {
            $url = $this->synthesize($text, $key);
            if ($url) $urls[$key] = $url;
        }
        return $urls;
    }

    /**
     * Obtener URL de frase pre-cargada
     */
    public function getPreloaded(string $key): ?string
    {
        $path = "voice_cache/{$key}.mp3";
        if (Storage::disk('public')->exists($path)) {
            return Storage::disk('public')->url($path);
        }
        // Generar si no existe
        $text = $this->preloadedPhrases[$key] ?? null;
        return $text ? $this->synthesize($text, $key) : null;
    }

    private function getCached(string $key): ?string
    {
        $path = "voice_cache/{$key}.mp3";
        return Storage::disk('public')->exists($path)
            ? Storage::disk('public')->url($path)
            : null;
    }
}
PHP

# ── 3. AI CONTROLLER ─────────────────────────────────────────────
cat > app/Http/Controllers/AiController.php << 'PHP'
<?php
namespace App\Http\Controllers;

use App\Events\AiResponseReady;
use App\Jobs\MonthlyAnalysisJob;
use App\Jobs\QuarterlyReportJob;
use App\Models\AiMessage;
use App\Models\Cycle;
use App\Models\Project;
use App\Services\ClaudeService;
use Illuminate\Http\Request;
use Inertia\Inertia;

class AiController extends Controller
{
    public function __construct(private ClaudeService $claude) {}

    /**
     * Chat en vivo durante sesiones (modo consultivo)
     */
    public function chat(Request $request, Project $project)
    {
        $request->validate(['question' => 'required|string|max:1000']);

        $response = $this->claude->liveChat(
            $project,
            $request->question,
            $request->session_context ?? []
        );

        $message = $this->claude->saveMessage(
            $project->id,
            'chat',
            $request->question,
            $response,
            'consultive'
        );

        // Broadcast a todos en la sesión
        broadcast(new AiResponseReady($project->id, 'chat', $response, $message->id));

        return response()->json([
            'response'   => $response,
            'message_id' => $message->id,
        ]);
    }

    /**
     * Dispara análisis mensual
     */
    public function triggerMonthly(Project $project, Cycle $cycle)
    {
        MonthlyAnalysisJob::dispatch($project, $cycle);
        return response()->json(['message' => 'Análisis mensual en proceso...']);
    }

    /**
     * Dispara retrospectiva trimestral
     */
    public function triggerQuarterly(Project $project, Cycle $cycle)
    {
        QuarterlyReportJob::dispatch($project, $cycle);
        return response()->json(['message' => 'Retrospectiva trimestral en proceso...']);
    }

    /**
     * Historial de mensajes IA del proyecto
     */
    public function history(Project $project)
    {
        $messages = AiMessage::where('project_id', $project->id)
            ->with('user:id,name')
            ->orderByDesc('created_at')
            ->paginate(20);

        return Inertia::render('Ai/History', [
            'project'  => $project->load('organization'),
            'messages' => $messages,
        ]);
    }

    /**
     * Dashboard IA — panel del consultor con todos los análisis
     */
    public function dashboard(Project $project)
    {
        $latestByModule = [];
        $modules = ['dofa', 'bet', 'synthesis', 'monthly', 'quarterly', 'chat'];

        foreach ($modules as $module) {
            $latestByModule[$module] = AiMessage::where('project_id', $project->id)
                ->where('module', $module)
                ->latest()
                ->first();
        }

        return Inertia::render('Ai/Dashboard', [
            'project'         => $project->load('organization'),
            'latestByModule'  => $latestByModule,
            'totalMessages'   => AiMessage::where('project_id', $project->id)->count(),
            'totalTokens'     => AiMessage::where('project_id', $project->id)->sum('tokens_used'),
        ]);
    }
}
PHP

# ── 4. VOICE CONTROLLER ──────────────────────────────────────────
cat > app/Http/Controllers/VoiceController.php << 'PHP'
<?php
namespace App\Http\Controllers;

use App\Events\VoicePlaying;
use App\Jobs\VoiceSynthesisJob;
use App\Models\Project;
use App\Services\VoiceService;
use Illuminate\Http\Request;

class VoiceController extends Controller
{
    public function __construct(private VoiceService $voice) {}

    /**
     * Hablar un texto (genera audio y lo brodcastea)
     */
    public function speak(Request $request, Project $project)
    {
        $request->validate([
            'text'      => 'required|string|max:1000',
            'cache_key' => 'nullable|string|max:100',
        ]);

        VoiceSynthesisJob::dispatch($project->id, $request->text, $request->cache_key);

        return response()->json(['message' => 'Generando audio...']);
    }

    /**
     * Reproducir frase pre-cargada del retiro
     */
    public function playPreloaded(Request $request, Project $project)
    {
        $request->validate(['key' => 'required|string']);

        $url = $this->voice->getPreloaded($request->key);

        if ($url) {
            broadcast(new VoicePlaying($project->id, $url, $request->key));
            return response()->json(['url' => $url]);
        }

        return response()->json(['error' => 'Audio no disponible'], 404);
    }

    /**
     * Pre-cargar todos los audios del retiro (ejecutar antes del retiro)
     */
    public function preload(Project $project)
    {
        $urls = $this->voice->preloadRetiroAudios();
        return response()->json(['preloaded' => count($urls), 'urls' => $urls]);
    }

    /**
     * Estado del servicio de voz
     */
    public function status()
    {
        $apiKey = config('services.elevenlabs.api_key');
        $configured = $apiKey && $apiKey !== 'PENDIENTE';

        return response()->json([
            'configured' => $configured,
            'voice_id'   => config('services.elevenlabs.voice_id'),
            'model'      => config('services.elevenlabs.model'),
        ]);
    }
}
PHP

# ── 5. JOBS ──────────────────────────────────────────────────────
cat > app/Jobs/VoiceSynthesisJob.php << 'PHP'
<?php
namespace App\Jobs;

use App\Events\VoicePlaying;
use App\Services\VoiceService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class VoiceSynthesisJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 2;
    public int $timeout = 30;

    public function __construct(
        public int     $projectId,
        public string  $text,
        public ?string $cacheKey = null
    ) {}

    public function handle(VoiceService $voice): void
    {
        $url = $voice->synthesize($this->text, $this->cacheKey);

        if ($url) {
            broadcast(new VoicePlaying($this->projectId, $url, $this->cacheKey ?? 'custom'));
        }
    }
}
PHP

cat > app/Jobs/MonthlyAnalysisJob.php << 'PHP'
<?php
namespace App\Jobs;

use App\Events\AiResponseReady;
use App\Models\AiMessage;
use App\Models\Cycle;
use App\Models\KeyResult;
use App\Models\Project;
use App\Services\ClaudeService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class MonthlyAnalysisJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 120;

    public function __construct(
        public Project $project,
        public Cycle   $cycle
    ) {}

    public function handle(ClaudeService $claude): void
    {
        $objectives = $this->cycle->objectives()->with('keyResults')->get();

        $krs = KeyResult::whereHas('objective', fn($q) => $q->where('cycle_id', $this->cycle->id))
            ->where('status', 'active')
            ->get()
            ->map(fn($kr) => [
                'title'        => $kr->title,
                'current'      => $kr->current,
                'target'       => $kr->target,
                'unit'         => $kr->unit,
                'score'        => round($kr->score * 100, 1),
                'traffic_light'=> $kr->traffic_light,
            ])->toArray();

        $green  = $objectives->filter(fn($o) => $o->score >= 0.70)->count();
        $yellow = $objectives->filter(fn($o) => $o->score >= 0.40 && $o->score < 0.70)->count();
        $red    = $objectives->filter(fn($o) => $o->score < 0.40)->count();

        $cycleData = [
            'period'  => "Q{$this->cycle->quarter} {$this->cycle->year}",
            'score'   => round($objectives->avg('score') * 100, 1),
            'green'   => $green,
            'yellow'  => $yellow,
            'red'     => $red,
            'krs'     => $krs,
        ];

        $response = $claude->monthlyAnalysis($this->project, $cycleData);

        $message = AiMessage::create([
            'project_id'  => $this->project->id,
            'module'      => 'monthly',
            'context'     => $cycleData,
            'prompt'      => "Análisis mensual Q{$this->cycle->quarter} {$this->cycle->year}",
            'response'    => $response,
            'model'       => config('services.anthropic.model'),
            'trigger'     => 'proactive',
        ]);

        broadcast(new AiResponseReady($this->project->id, 'monthly', $response, $message->id));
    }
}
PHP

cat > app/Jobs/QuarterlyReportJob.php << 'PHP'
<?php
namespace App\Jobs;

use App\Events\AiResponseReady;
use App\Models\AiMessage;
use App\Models\Cycle;
use App\Models\Project;
use App\Services\ClaudeService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class QuarterlyReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 180;

    public function __construct(
        public Project $project,
        public Cycle   $cycle
    ) {}

    public function handle(ClaudeService $claude): void
    {
        $objectives = $this->cycle->objectives()->with('keyResults')->get();
        $achieved   = $objectives->filter(fn($o) => $o->score >= 0.70)->count();

        $objSummary = $objectives->map(fn($o) =>
            "- {$o->title}: " . round($o->score * 100) . "% (" .
            $o->keyResults->count() . " KRs)"
        )->join("\n");

        $bets = $this->project->strategicBets()->where('status','active')->get();
        $betsSummary = $bets->map(fn($b) =>
            "- {$b->hypothesis} (score: {$b->score})"
        )->join("\n") ?: 'Sin apuestas activas';

        $cycleData = [
            'quarter'            => $this->cycle->quarter,
            'year'               => $this->cycle->year,
            'final_score'        => round($objectives->avg('score') * 100, 1),
            'achieved'           => $achieved,
            'total'              => $objectives->count(),
            'objectives_summary' => $objSummary,
            'bets_summary'       => $betsSummary,
        ];

        $response = $claude->quarterlyRetro($this->project, $cycleData);

        $message = AiMessage::create([
            'project_id'  => $this->project->id,
            'module'      => 'quarterly',
            'context'     => $cycleData,
            'prompt'      => "Retrospectiva Q{$this->cycle->quarter} {$this->cycle->year}",
            'response'    => $response,
            'model'       => config('services.anthropic.model'),
            'trigger'     => 'proactive',
        ]);

        broadcast(new AiResponseReady($this->project->id, 'quarterly', $response, $message->id));
    }
}
PHP

# ── 6. EVENTOS ───────────────────────────────────────────────────
cat > app/Events/AiResponseReady.php << 'PHP'
<?php
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class AiResponseReady implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public int    $projectId,
        public string $module,
        public string $response,
        public int    $messageId
    ) {}

    public function broadcastOn(): Channel
    {
        return new Channel("project.{$this->projectId}.ai");
    }

    public function broadcastAs(): string { return 'response.ready'; }

    public function broadcastWith(): array
    {
        return [
            'module'     => $this->module,
            'response'   => $this->response,
            'message_id' => $this->messageId,
        ];
    }
}
PHP

cat > app/Events/VoicePlaying.php << 'PHP'
<?php
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class VoicePlaying implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public int    $projectId,
        public string $audioUrl,
        public string $key
    ) {}

    public function broadcastOn(): Channel
    {
        return new Channel("project.{$this->projectId}.voice");
    }

    public function broadcastAs(): string { return 'voice.play'; }

    public function broadcastWith(): array
    {
        return [
            'audio_url' => $this->audioUrl,
            'key'       => $this->key,
        ];
    }
}
PHP

# ── 7. RUTAS ─────────────────────────────────────────────────────
cat >> routes/web.php << 'PHP'

// ── IA y Voz Routes ───────────────────────────────────────────────
use App\Http\Controllers\AiController;
use App\Http\Controllers\VoiceController;

Route::middleware(['auth', 'verified'])->prefix('projects/{project}')->group(function () {

    // IA — Chat y análisis
    Route::post('/ai/chat',                    [AiController::class, 'chat'])->name('ai.chat');
    Route::post('/cycles/{cycle}/ai/monthly',  [AiController::class, 'triggerMonthly'])->name('ai.monthly');
    Route::post('/cycles/{cycle}/ai/quarterly',[AiController::class, 'triggerQuarterly'])->name('ai.quarterly');
    Route::get('/ai/history',                  [AiController::class, 'history'])->name('ai.history');
    Route::get('/ai/dashboard',                [AiController::class, 'dashboard'])->name('ai.dashboard');

    // Voz ElevenLabs
    Route::post('/voice/speak',      [VoiceController::class, 'speak'])->name('voice.speak');
    Route::post('/voice/preloaded',  [VoiceController::class, 'playPreloaded'])->name('voice.preloaded');
    Route::post('/voice/preload',    [VoiceController::class, 'preload'])->name('voice.preload');
    Route::get('/voice/status',      [VoiceController::class, 'status'])->name('voice.status');
});
PHP

# ── 8. VISTA: PANEL IA DEL CONSULTOR ────────────────────────────
mkdir -p resources/js/Pages/Ai
mkdir -p resources/js/Components

# Componente reutilizable de chat IA
cat > resources/js/Components/AiChatPanel.vue << 'VUE'
<script setup>
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import axios from 'axios'

const props = defineProps({
    project: Object,
    sessionContext: { type: Object, default: () => ({}) },
    placeholder: { type: String, default: 'Pregunta a Claude sobre el plan estratégico...' },
    compact: { type: Boolean, default: false },
})

const question = ref('')
const messages = ref([])
const loading = ref(false)
const messagesEnd = ref(null)

// Escuchar respuestas de IA desde otros Jobs
let channel = null
onMounted(() => {
    if (window.Echo) {
        channel = window.Echo.channel(`project.${props.project.id}.ai`)
            .listen('.response.ready', (data) => {
                if (data.module !== 'chat') {
                    messages.value.push({
                        role: 'ai_proactive',
                        module: data.module,
                        content: data.response,
                        time: new Date().toLocaleTimeString('es-CO', { hour:'2-digit', minute:'2-digit' })
                    })
                    scrollToBottom()
                }
            })
    }
})
onUnmounted(() => channel?.stopListening('.response.ready'))

const send = async () => {
    if (!question.value.trim() || loading.value) return

    const q = question.value
    question.value = ''
    loading.value = true

    messages.value.push({
        role: 'user',
        content: q,
        time: new Date().toLocaleTimeString('es-CO', { hour:'2-digit', minute:'2-digit' })
    })

    await scrollToBottom()

    try {
        const res = await axios.post(route('ai.chat', props.project.id), {
            question: q,
            session_context: props.sessionContext,
        })

        messages.value.push({
            role: 'ai',
            content: res.data.response,
            time: new Date().toLocaleTimeString('es-CO', { hour:'2-digit', minute:'2-digit' })
        })
    } catch (e) {
        messages.value.push({ role: 'error', content: 'Error al contactar a Claude. Intenta nuevamente.' })
    } finally {
        loading.value = false
        await scrollToBottom()
    }
}

const scrollToBottom = async () => {
    await nextTick()
    messagesEnd.value?.scrollIntoView({ behavior: 'smooth' })
}

const moduleLabels = {
    dofa: '🔍 Análisis DOFA',
    bet: '🎯 Validación apuesta',
    synthesis: '💡 Síntesis',
    monthly: '📊 Análisis mensual',
    quarterly: '📋 Retrospectiva',
    kr_validation: '✅ Validación KR',
}
</script>

<template>
    <div :class="['flex flex-col bg-gray-900 rounded-2xl overflow-hidden', compact ? 'h-64' : 'h-96']">

        <!-- Header -->
        <div class="bg-gray-800 px-4 py-3 flex items-center gap-2 shrink-0">
            <div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
            <span class="text-white text-sm font-medium">Claude AI</span>
            <span v-if="loading" class="text-gray-400 text-xs ml-auto">Analizando...</span>
        </div>

        <!-- Mensajes -->
        <div class="flex-1 overflow-y-auto p-3 space-y-3 min-h-0">
            <div v-if="messages.length === 0" class="text-gray-500 text-sm text-center mt-8">
                <p class="text-2xl mb-2">🤖</p>
                <p>{{ placeholder }}</p>
            </div>

            <div v-for="(msg, i) in messages" :key="i">
                <!-- Mensaje del usuario -->
                <div v-if="msg.role === 'user'" class="flex justify-end">
                    <div class="bg-blue-600 text-white text-sm rounded-xl rounded-tr-sm px-3 py-2 max-w-xs">
                        {{ msg.content }}
                        <div class="text-blue-200 text-xs mt-1">{{ msg.time }}</div>
                    </div>
                </div>

                <!-- Respuesta de Claude -->
                <div v-else-if="msg.role === 'ai'" class="flex gap-2">
                    <div class="w-6 h-6 bg-purple-600 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0 mt-1">C</div>
                    <div class="bg-gray-700 text-gray-100 text-sm rounded-xl rounded-tl-sm px-3 py-2 max-w-sm">
                        <div class="whitespace-pre-line leading-relaxed">{{ msg.content }}</div>
                        <div class="text-gray-400 text-xs mt-1">{{ msg.time }}</div>
                    </div>
                </div>

                <!-- Análisis proactivo de IA -->
                <div v-else-if="msg.role === 'ai_proactive'" class="bg-purple-900 bg-opacity-50 rounded-xl p-3 border border-purple-700">
                    <div class="text-purple-300 text-xs font-bold mb-1">{{ moduleLabels[msg.module] || msg.module }}</div>
                    <div class="text-gray-200 text-xs whitespace-pre-line leading-relaxed">{{ msg.content }}</div>
                </div>

                <!-- Error -->
                <div v-else-if="msg.role === 'error'" class="text-red-400 text-xs text-center">{{ msg.content }}</div>
            </div>

            <!-- Loading indicator -->
            <div v-if="loading" class="flex gap-2">
                <div class="w-6 h-6 bg-purple-600 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0">C</div>
                <div class="bg-gray-700 rounded-xl px-3 py-2">
                    <div class="flex gap-1">
                        <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
                        <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-75"></div>
                        <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-150"></div>
                    </div>
                </div>
            </div>

            <div ref="messagesEnd"></div>
        </div>

        <!-- Input -->
        <div class="p-3 bg-gray-800 shrink-0">
            <div class="flex gap-2">
                <input v-model="question"
                       @keydown.enter="send"
                       :placeholder="placeholder"
                       class="flex-1 bg-gray-700 text-white text-sm rounded-xl px-3 py-2 outline-none focus:ring-1 focus:ring-purple-500 placeholder-gray-500" />
                <button @click="send" :disabled="loading || !question.trim()"
                        class="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 text-white px-3 py-2 rounded-xl text-sm font-medium transition-colors">
                    →
                </button>
            </div>
        </div>
    </div>
</template>
VUE

# Componente de control de voz
cat > resources/js/Components/VoiceControl.vue << 'VUE'
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import axios from 'axios'

const props = defineProps({
    project: Object,
})

const voiceEnabled = ref(true)
const playing = ref(false)
const currentAudio = ref(null)
const status = ref('checking')

const preloadedKeys = [
    { key: 'bienvenida',    label: 'Bienvenida al retiro' },
    { key: 'dofa_inicio',   label: 'Iniciar calificación DOFA' },
    { key: 'aspiration',    label: 'Instrucción aspiración' },
    { key: 'renuncias',     label: 'Instrucción renuncias' },
    { key: 'okr_builder',   label: 'Iniciar constructor OKR' },
    { key: 'cierre_dia1',   label: 'Cierre Día 1' },
    { key: 'cierre_retiro', label: 'Cierre del retiro' },
]

let channel = null
onMounted(async () => {
    // Verificar estado del servicio
    try {
        const res = await axios.get(route('voice.status', props.project.id))
        status.value = res.data.configured ? 'ready' : 'not_configured'
    } catch { status.value = 'error' }

    // Escuchar eventos de voz
    if (window.Echo) {
        channel = window.Echo.channel(`project.${props.project.id}.voice`)
            .listen('.voice.play', (data) => {
                if (voiceEnabled.value) {
                    playAudio(data.audio_url)
                }
            })
    }
})
onUnmounted(() => channel?.stopListening('.voice.play'))

const playAudio = (url) => {
    if (currentAudio.value) {
        currentAudio.value.pause()
    }
    const audio = new Audio(url)
    audio.onplay  = () => { playing.value = true }
    audio.onended = () => { playing.value = false; currentAudio.value = null }
    audio.play().catch(() => { playing.value = false })
    currentAudio.value = audio
}

const stop = () => {
    currentAudio.value?.pause()
    currentAudio.value = null
    playing.value = false
}

const playPreloaded = async (key) => {
    try {
        await axios.post(route('voice.preloaded', props.project.id), { key })
    } catch (e) {
        console.error('Error playing preloaded audio', e)
    }
}

const speakCustom = ref('')
const speakCustomText = async () => {
    if (!speakCustom.value.trim()) return
    try {
        await axios.post(route('voice.speak', props.project.id), { text: speakCustom.value })
        speakCustom.value = ''
    } catch (e) {
        console.error('Error speaking custom text', e)
    }
}
</script>

<template>
    <div class="bg-gray-800 rounded-2xl p-4">
        <div class="flex items-center justify-between mb-4">
            <div class="flex items-center gap-2">
                <span class="text-white font-medium text-sm">🎙 Voz IA</span>
                <span v-if="status === 'ready'" class="text-xs text-green-400">Activa</span>
                <span v-else-if="status === 'not_configured'" class="text-xs text-yellow-400">No configurada</span>
                <span v-else class="text-xs text-gray-400">Verificando...</span>
            </div>
            <div class="flex items-center gap-2">
                <span v-if="playing" class="text-xs text-purple-300 animate-pulse">▶ Reproduciendo</span>
                <button v-if="playing" @click="stop"
                        class="text-xs bg-red-600 text-white px-2 py-1 rounded">
                    ■ Detener
                </button>
                <button @click="voiceEnabled = !voiceEnabled"
                        :class="['text-xs px-2 py-1 rounded font-medium',
                            voiceEnabled ? 'bg-green-600 text-white' : 'bg-gray-600 text-gray-300']">
                    {{ voiceEnabled ? 'ON' : 'OFF' }}
                </button>
            </div>
        </div>

        <div v-if="status === 'not_configured'" class="text-yellow-400 text-xs mb-3 p-2 bg-yellow-900 bg-opacity-30 rounded">
            Configura ELEVENLABS_API_KEY en el .env para activar la voz
        </div>

        <!-- Frases pre-cargadas -->
        <div class="grid grid-cols-2 gap-1.5 mb-3">
            <button v-for="item in preloadedKeys" :key="item.key"
                    @click="playPreloaded(item.key)"
                    :disabled="!voiceEnabled || status !== 'ready'"
                    class="text-xs bg-gray-700 hover:bg-gray-600 disabled:opacity-40 text-gray-300 px-2 py-1.5 rounded text-left transition-colors">
                {{ item.label }}
            </button>
        </div>

        <!-- Texto personalizado -->
        <div class="flex gap-2">
            <input v-model="speakCustom"
                   placeholder="Escribe lo que Claude debe decir..."
                   class="flex-1 bg-gray-700 text-white text-xs rounded-lg px-2 py-1.5 outline-none placeholder-gray-500" />
            <button @click="speakCustomText"
                    :disabled="!voiceEnabled || !speakCustom.trim() || status !== 'ready'"
                    class="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 text-white text-xs px-2 py-1.5 rounded transition-colors">
                ▶
            </button>
        </div>
    </div>
</template>
VUE

# Dashboard IA
cat > resources/js/Pages/Ai/Dashboard.vue << 'VUE'
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import AiChatPanel from '@/Components/AiChatPanel.vue'

const props = defineProps({
    project: Object,
    latestByModule: Object,
    totalMessages: Number,
    totalTokens: Number,
})

const moduleLabels = {
    dofa:           '🔍 DOFA',
    bet:            '🎯 Apuestas',
    synthesis:      '💡 Síntesis',
    monthly:        '📊 Mensual',
    quarterly:      '📋 Trimestral',
    chat:           '💬 Chat',
}
</script>

<template>
    <AppLayout :title="`IA Dashboard — ${project.name}`">
        <div class="max-w-5xl mx-auto px-4 py-6">

            <div class="mb-6">
                <h1 class="text-2xl font-bold text-gray-900">Panel de Inteligencia Artificial</h1>
                <p class="text-gray-500 mt-1">{{ project.organization?.name }} — {{ project.name }}</p>
            </div>

            <!-- Métricas -->
            <div class="grid grid-cols-2 gap-4 mb-6">
                <div class="bg-white rounded-xl shadow p-4 text-center">
                    <div class="text-3xl font-black text-purple-600">{{ totalMessages }}</div>
                    <div class="text-xs text-gray-500 mt-1">Análisis generados</div>
                </div>
                <div class="bg-white rounded-xl shadow p-4 text-center">
                    <div class="text-3xl font-black text-blue-600">{{ totalTokens.toLocaleString() }}</div>
                    <div class="text-xs text-gray-500 mt-1">Tokens utilizados</div>
                </div>
            </div>

            <div class="grid grid-cols-3 gap-6">

                <!-- Chat en vivo -->
                <div class="col-span-2">
                    <h2 class="text-sm font-semibold text-gray-600 uppercase mb-3">Chat en vivo con Claude</h2>
                    <AiChatPanel :project="project" />
                </div>

                <!-- Últimos análisis por módulo -->
                <div>
                    <h2 class="text-sm font-semibold text-gray-600 uppercase mb-3">Últimos análisis</h2>
                    <div class="space-y-2">
                        <div v-for="(msg, module) in latestByModule" :key="module"
                             :class="['rounded-xl p-3 border', msg ? 'bg-white border-gray-200' : 'bg-gray-50 border-dashed border-gray-200']">
                            <div class="text-xs font-semibold text-gray-600 mb-1">{{ moduleLabels[module] }}</div>
                            <div v-if="msg" class="text-xs text-gray-500 line-clamp-2">{{ msg.response }}</div>
                            <div v-else class="text-xs text-gray-400 italic">Sin análisis aún</div>
                            <div v-if="msg" class="text-xs text-gray-300 mt-1">{{ new Date(msg.created_at).toLocaleDateString('es-CO') }}</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </AppLayout>
</template>
VUE

# ── 9. COMPILAR Y CACHEAR ───────────────────────────────────────
echo ">>> Compilando assets Vue..."
npm run build

php artisan config:clear
php artisan route:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache

echo ""
echo "========================================="
echo "  Sprint 06 completado exitosamente"
echo ""
echo "  Servicios creados:"
echo "  ✅ ClaudeService (chat, mensual,"
echo "     trimestral, system prompt)"
echo "  ✅ VoiceService (ElevenLabs TTS)"
echo "  ✅ AiController (chat, history,"
echo "     dashboard, análisis)"
echo "  ✅ VoiceController (speak, preload)"
echo "  ✅ MonthlyAnalysisJob"
echo "  ✅ QuarterlyReportJob"
echo "  ✅ VoiceSynthesisJob"
echo "  ✅ Eventos: AiResponseReady,"
echo "     VoicePlaying"
echo "  ✅ Componente AiChatPanel.vue"
echo "  ✅ Componente VoiceControl.vue"
echo "  ✅ Vista Ai/Dashboard.vue"
echo ""
echo "  ⚠ Recuerda configurar en .env:"
echo "  ANTHROPIC_API_KEY=sk-ant-..."
echo "  ELEVENLABS_API_KEY=..."
echo "  ELEVENLABS_VOICE_ID=..."
echo "========================================="
