<?php
declare(strict_types=1);

// api/chunk.php
ini_set('display_errors', '1');
error_reporting(E_ALL);
header('Content-Type: application/json; charset=utf-8');

require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once __DIR__ . '/config.php';

use Google\Cloud\Core\Exception\GoogleException;

// Speech-to-Text V2
use Google\Cloud\Speech\V2\Client\SpeechClient as SpeechClientV2;
use Google\Cloud\Speech\V2\RecognitionConfig as RecognitionConfigV2;
use Google\Cloud\Speech\V2\RecognitionFeatures as RecognitionFeaturesV2;
use Google\Cloud\Speech\V2\AutoDetectDecodingConfig as AutoDetectDecodingConfigV2;
use Google\Cloud\Speech\V2\ExplicitDecodingConfig as ExplicitDecodingConfigV2;
use Google\Cloud\Speech\V2\ExplicitDecodingConfig\AudioEncoding as AudioEncodingV2;
use Google\Cloud\Speech\V2\RecognizeRequest as RecognizeRequestV2;

// Translate (v1 lib)
use Google\Cloud\Translate\V2\TranslateClient;

// Text-to-Speech (v1 lib)
use Google\Cloud\TextToSpeech\V1\TextToSpeechClient;
use Google\Cloud\TextToSpeech\V1\SynthesisInput;
use Google\Cloud\TextToSpeech\V1\VoiceSelectionParams;
use Google\Cloud\TextToSpeech\V1\AudioConfig;
use Google\Cloud\TextToSpeech\V1\AudioEncoding;

/* --------------------------- Helpers --------------------------- */
function out(int $code, array $payload): never {
  http_response_code($code);
  echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  exit;
}

function get_project_id_from_credentials(?string $credPath): ?string {
  if (!$credPath || !is_file($credPath)) return null;
  $json = json_decode((string)@file_get_contents($credPath), true);
  return $json['project_id'] ?? null;
}

function detectEncoding(string $mime, string $filename): int {
  $m = strtolower($mime);
  $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  if (str_contains($m, 'ogg') || $ext === 'ogg' || $ext === 'oga') {
    return AudioEncodingV2::OGG_OPUS;
  }
  return AudioEncodingV2::WEBM_OPUS; // défaut
}

function sanitize_channel(string $raw): array {
  // Attendu: nombre 0..111.11 (2 décimales max)
  $raw = trim($raw);
  if ($raw === '' || !is_numeric($raw)) return [null, null, 'channel invalide'];
  $f = (float)$raw;
  if ($f < 0 || $f > 111.11) return [null, null, 'channel hors limites'];
  $norm = number_format($f, 2, '.', '');  // "12.34"
  $file = str_replace('.', '_', $norm);   // "12_34"
  return [$norm, $file, null];
}

function normalize_id(string $id): string {
  return strtolower(preg_replace('/[^a-z0-9-]/i', '', $id));
}

/* --------------------------- Préflight --------------------------- */
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
  http_response_code(204);
  exit;
}

try {
  /* ----------------------- Params front ----------------------- */
  $seq          = isset($_POST['seq']) ? (int)$_POST['seq'] : null;
  $sessionId    = $_POST['session_id']   ?? null;
  $speakerIdRaw = $_POST['speaker_id']   ?? null;
  $speakerRole  = $_POST['speaker_role'] ?? 'listener'; // "host" | "listener"
  $channelRaw   = $_POST['channel']      ?? null;
  $sourceLang   = $_POST['source_lang']  ?? 'fr-FR';    // BCP-47
  $targetLang   = $_POST['target_lang']  ?? 'en';       // ISO 639-1
  $location     = 'global';                              // 'global' / 'us' / 'eu'

  if (!$sessionId || !preg_match('/^[a-f0-9-]{8,}$/i', $sessionId)) {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'session_id invalide']);
  }
  if (!$speakerIdRaw) {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'speaker_id invalide']);
  }
  $speakerId = normalize_id($speakerIdRaw);
  if ($speakerId === '') {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'speaker_id invalide']);
  }

  $speakerRole = (strtolower($speakerRole) === 'host') ? 'host' : 'listener';

  if (!$channelRaw) {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'channel requis']);
  }
  [$channelNorm, $channelFile, $chanErr] = sanitize_channel($channelRaw);
  if ($chanErr !== null) {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'channel invalide (0..111.11)']);
  }

  if (!isset($_FILES['audio']['tmp_name'])) {
    out(400, ['ok'=>false, 'seq'=>$seq, 'error'=>'Aucun fichier audio reçu']);
  }

  /* ----------------------- Stockage chunk ---------------------- */
  $sessDir = SESSIONS_DIR . '/' . $sessionId;
  if (!is_dir($sessDir) && !@mkdir($sessDir, 0775, true)) {
    out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'Impossible de créer le répertoire session']);
  }

  $seqName    = ($seq !== null) ? $seq : uniqid('seq_', true);
  $uploadName = $_FILES['audio']['name'] ?? ('seq-'.$seqName);
  $mime       = $_FILES['audio']['type'] ?? 'application/octet-stream';
  $ext        = (str_contains($mime, 'ogg') || preg_match('/\.ogg$/i', $uploadName)) ? 'ogg' : 'webm';

  $tmp       = $_FILES['audio']['tmp_name'];
  $chunkPath = $sessDir . '/' . $seqName . '.' . $ext;

  if (!@move_uploaded_file($tmp, $chunkPath)) {
    if (!@rename($tmp, $chunkPath)) {
      out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'Impossible de stocker le chunk audio']);
    }
  }

  $audioBytes = @file_get_contents($chunkPath);
  if ($audioBytes === false || $audioBytes === '') {
    out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'Lecture du chunk échouée']);
  }

  // Option : ignorer les "mini-chunks" trop petits (headers/silences) pour éviter les erreurs
  $minBytes = 1024; // tu peux mettre 0 si tu veux absolument tout passer
  if (strlen($audioBytes) < $minBytes) {
    out(200, [
      'ok'          => true,
      'seq'         => $seq,
      'channel'     => $channelNorm,
      'source_lang' => $sourceLang,
      'target_lang' => $targetLang,
      'text'        => '',
      'translated'  => '',
      'tts_url'     => null,
    ]);
  }

  /* ----------------- Project & recognizer (V2) ----------------- */
  $gcpCred   = getenv('GOOGLE_APPLICATION_CREDENTIALS') ?: (defined('GCP_CREDENTIALS') ? GCP_CREDENTIALS : null);
  $projectId = get_project_id_from_credentials($gcpCred) ?: getenv('GOOGLE_CLOUD_PROJECT') ?: getenv('GCLOUD_PROJECT');
  if (!$projectId) {
    out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'project_id introuvable (depuis la clé JSON)']);
  }
  $recognizer = "projects/{$projectId}/locations/{$location}/recognizers/_";

  /* ----------------------- SPEECH-TO-TEXT ---------------------- */
  if (!class_exists(SpeechClientV2::class)) {
    out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'Client Speech V2 introuvable']);
  }
  $speech = new SpeechClientV2();

  $features   = (new RecognitionFeaturesV2())->setEnableAutomaticPunctuation(true);
  $encGuessed = detectEncoding($mime, $uploadName);

  // Fabrique une config V2 (explicite OU auto) — force 48k mono en explicite
  $makeConfig = function(?int $explicitEncoding) use ($features, $sourceLang): RecognitionConfigV2 {
    $cfg = new RecognitionConfigV2();
    if ($explicitEncoding !== null) {
      $dec = (new ExplicitDecodingConfigV2())
        ->setEncoding($explicitEncoding)
        ->setSampleRateHertz(48000)
        ->setAudioChannelCount(1);
      $cfg->setExplicitDecodingConfig($dec);
    } else {
      $cfg->setAutoDecodingConfig(new AutoDetectDecodingConfigV2());
    }
    return $cfg
      ->setLanguageCodes([$sourceLang])
      ->setModel('latest_long')   // plus tolérant pour 4s+ ou longs holds
      ->setFeatures($features);
  };

  // Essai A: explicite (encodage deviné)
  $tried   = [];
  $config  = $makeConfig($encGuessed);
  $request = (new RecognizeRequestV2())
    ->setRecognizer($recognizer)
    ->setConfig($config)
    ->setContent($audioBytes);

  try {
    $tried[] = 'explicit:' . ($encGuessed === AudioEncodingV2::OGG_OPUS ? 'OGG_OPUS' : 'WEBM_OPUS');
    $response = $speech->recognize($request);
  } catch (GoogleException $eA) {
    // Essai B: auto-detect
    try {
      $tried[] = 'auto-detect';
      $config = $makeConfig(null);
      $request = (new RecognizeRequestV2())
        ->setRecognizer($recognizer)
        ->setConfig($config)
        ->setContent($audioBytes);
      $response = $speech->recognize($request);
    } catch (GoogleException $eB) {
      // Essai C: explicite inversé
      try {
        $other = ($encGuessed === AudioEncodingV2::OGG_OPUS) ? AudioEncodingV2::WEBM_OPUS : AudioEncodingV2::OGG_OPUS;
        $tried[] = 'explicit:' . ($other === AudioEncodingV2::OGG_OPUS ? 'OGG_OPUS' : 'WEBM_OPUS');
        $config = $makeConfig($other);
        $request = (new RecognizeRequestV2())
          ->setRecognizer($recognizer)
          ->setConfig($config)
          ->setContent($audioBytes);
        $response = $speech->recognize($request);
      } catch (GoogleException $eC) {
        out(500, [
          'ok'    => false,
          'seq'   => $seq,
          'error' => 'GoogleException: ' . $eC->getMessage(),
          'debug' => ['mime'=>$mime,'name'=>$uploadName,'size'=>strlen($audioBytes),'tried'=>$tried]
        ]);
      }
    }
  }

  // Concatène la transcription
  $transcript = '';
  foreach ($response->getResults() as $res) {
    $alts = $res->getAlternatives();
    if (count($alts) > 0 && $alts[0]->getTranscript() !== '') {
      $transcript .= ($transcript === '' ? '' : ' ') . trim($alts[0]->getTranscript());
    }
  }

  if ($transcript === '') {
    out(200, [
      'ok'          => true,
      'seq'         => $seq,
      'channel'     => $channelNorm,
      'source_lang' => $sourceLang,
      'target_lang' => $targetLang,
      'text'        => '',
      'translated'  => '',
      'tts_url'     => null,
    ]);
  }

  /* ------------------------- TRADUCTION ------------------------ */
  $translate  = new TranslateClient(['keyFilePath' => $gcpCred]);
  $tr         = $translate->translate($transcript, ['target' => $targetLang, 'format' => 'text']);
  $translated = $tr['text'] ?? '';

  /* -------------------------- TTS ------------------------------ */
  $voiceLangMap = ['en'=>'en-US','fr'=>'fr-FR','ar'=>'ar-XA','es'=>'es-ES'];
  $voiceLang    = $voiceLangMap[strtolower($targetLang)] ?? $targetLang;

  $ttsClient = new TextToSpeechClient(['transport'=>'rest','credentials'=>$gcpCred]);
  $inputText = (new SynthesisInput())->setText($translated);
  $voice     = (new VoiceSelectionParams())->setLanguageCode($voiceLang);
  $audioCfg  = (new AudioConfig())->setAudioEncoding(AudioEncoding::MP3);

  $ttsResp  = $ttsClient->synthesizeSpeech($inputText, $voice, $audioCfg);
  $ttsAudio = $ttsResp->getAudioContent();
  if (method_exists($ttsClient, 'close')) $ttsClient->close();

  $ttsSess = TTS_PUBLIC_DIR . '/' . $sessionId;
  if (!is_dir($ttsSess) && !@mkdir($ttsSess, 0775, true)) {
    out(500, ['ok'=>false, 'seq'=>$seq, 'error'=>'Impossible de créer le répertoire TTS']);
  }
  $ttsFile = $ttsSess . '/' . $seqName . '.mp3';
  file_put_contents($ttsFile, $ttsAudio);

  // URL publique (si ton vhost pointe sur /public)
  $ttsUrl = "tts/{$sessionId}/{$seqName}.mp3";
  // Si le vhost pointe sur la racine du projet, utilise plutôt :
  // $ttsUrl = "/public/tts/{$sessionId}/{$seqName}.mp3";

  /* ------------------ Publication dans le channel -------------- */
  if (!defined('FEED_DIR')) {
    define('FEED_DIR', __DIR__ . '/feeds');
    @mkdir(FEED_DIR, 0775, true);
  }
  $feedPath = FEED_DIR . '/' . $channelFile . '.ndjson';
  $event = [
    'ts'           => microtime(true),
    'id'           => $sessionId . '-' . $seqName,
    'session_id'   => strtolower($sessionId),
    'channel'      => $channelNorm,
    'speaker_id'   => $speakerId,
    'speaker_role' => $speakerRole,    // "host" ou "listener"
    'source_lang'  => $sourceLang,
    'target_lang'  => $targetLang,
    'text'         => $translated,     // texte traduit (lu par TTS)
    'stt'          => $transcript,     // texte source reconnu
    'tts_url'      => $ttsUrl
  ];
  $line = json_encode($event, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
  $fh = @fopen($feedPath, 'ab');
  if ($fh) {
    if (function_exists('flock')) { @flock($fh, LOCK_EX); }
    @fwrite($fh, $line);
    if (function_exists('flock')) { @flock($fh, LOCK_UN); }
    @fclose($fh);
  }

  /* -------------------- Réponse au client (speaker) ------------ */
  out(200, [
    'ok'          => true,
    'seq'         => $seq,
    'channel'     => $channelNorm,
    'source_lang' => $sourceLang,
    'target_lang' => $targetLang,
    'text'        => $transcript,
    'translated'  => $translated,
    'tts_url'     => $ttsUrl, // côté front, lecture via feed (self-mute)
  ]);

} catch (GoogleException $ge) {
  out(500, ['ok'=>false, 'seq'=>$seq ?? null, 'error'=>'GoogleException: '.$ge->getMessage()]);
} catch (Throwable $e) {
  out(500, ['ok'=>false, 'seq'=>$seq ?? null, 'error'=>'Exception: '.$e->getMessage()]);
}
