<?php
/**
 * Studio TTS Téléphone — fonctions partagées
 * TTS ElevenLabs + mixage musique de fond (ffmpeg) → WAV 8kHz mono (G.711 friendly)
 */

declare(strict_types=1);

const APP_DIR    = __DIR__;
const DATA_DIR   = __DIR__ . '/data';
const OUTPUT_DIR = __DIR__ . '/output';
const CONFIG_FILE = __DIR__ . '/data/config.php';

const EL_API = 'https://api.elevenlabs.io';
const COOKIES_FILE = __DIR__ . '/data/yt_cookies.txt';

/* ------------------------------------------------------------------ config */

function cfg_defaults(): array
{
    return [
        'api_key'       => '',
        'model_id'      => 'eleven_multilingual_v2',
        'stability'     => 0.5,
        'similarity'    => 0.75,
        'style'         => 0.0,
        'speaker_boost' => true,
        'music_volume'  => 0.18,
        'intro'         => 2.0,
        'tail'          => 3.0,
        'ducking'       => true,
        'app_password_hash' => '',
    ];
}

function cfg_load(): array
{
    $cfg = cfg_defaults();
    if (is_file(CONFIG_FILE)) {
        $saved = include CONFIG_FILE;
        if (is_array($saved)) {
            $cfg = array_merge($cfg, $saved);
        }
    }
    return $cfg;
}

function cfg_save(array $cfg): void
{
    $clean = array_merge(cfg_defaults(), $cfg);
    $php   = "<?php\n// Généré automatiquement — ne pas exposer.\nreturn " . var_export($clean, true) . ";\n";
    file_put_contents(CONFIG_FILE, $php, LOCK_EX);
}

/* -------------------------------------------------------------- utilities */

function json_out($data, int $code = 200): never
{
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function fail(string $msg, int $code = 400): never
{
    json_out(['ok' => false, 'error' => $msg], $code);
}

/** Token de fichier sûr (uniquement nos fichiers data/) */
function safe_token(string $t): string
{
    if (!preg_match('/^[a-zA-Z0-9_]+\.[a-zA-Z0-9]{2,4}$/', $t)) {
        fail('Référence de fichier invalide.');
    }
    return $t;
}

function rand_id(): string
{
    return bin2hex(random_bytes(8));
}

/* ----------------------------------------------------------------- ffmpeg */

function ff_duration(string $file): float
{
    $cmd = 'ffprobe -v error -show_entries format=duration -of csv=p=0 ' . escapeshellarg($file) . ' 2>/dev/null';
    $out = trim((string) shell_exec($cmd));
    return $out === '' ? 0.0 : (float) $out;
}

/* ------------------------------------------------------------- ElevenLabs */

function el_key(): string
{
    $cfg = cfg_load();
    $key = trim((string) $cfg['api_key']);
    if ($key === '') {
        fail("Clé API ElevenLabs manquante. Renseigne-la dans l'onglet Configuration.", 412);
    }
    return $key;
}

/**
 * Requête générique ElevenLabs.
 * @return array{code:int, body:string, ctype:string}
 */
function el_request(string $method, string $path, ?array $json = null, ?string $accept = null): array
{
    $ch = curl_init(EL_API . $path);
    $headers = ['xi-api-key: ' . el_key()];
    if ($accept) {
        $headers[] = 'Accept: ' . $accept;
    }
    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_HTTPHEADER     => $headers,
    ];
    if ($json !== null) {
        $opts[CURLOPT_POSTFIELDS] = json_encode($json, JSON_UNESCAPED_UNICODE);
        $headers[] = 'Content-Type: application/json';
        $opts[CURLOPT_HTTPHEADER] = $headers;
    }
    curl_setopt_array($ch, $opts);
    $body  = (string) curl_exec($ch);
    if ($body === '' && curl_errno($ch)) {
        $err = curl_error($ch);
        curl_close($ch);
        fail('Erreur réseau ElevenLabs : ' . $err, 502);
    }
    $code  = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);
    return ['code' => $code, 'body' => $body, 'ctype' => $ctype];
}

/** Liste des voix du compte, normalisée. */
function el_voices(): array
{
    $r = el_request('GET', '/v1/voices');
    if ($r['code'] !== 200) {
        $msg = el_error_msg($r['body']) ?? ('HTTP ' . $r['code']);
        fail('Impossible de récupérer les voix : ' . $msg, 502);
    }
    $data = json_decode($r['body'], true);
    $out  = [];
    foreach (($data['voices'] ?? []) as $v) {
        $labels = $v['labels'] ?? [];
        $out[] = [
            'voice_id'    => $v['voice_id'] ?? '',
            'name'        => $v['name'] ?? 'Sans nom',
            'category'    => $v['category'] ?? '',
            'gender'      => $labels['gender'] ?? '',
            'accent'      => $labels['accent'] ?? '',
            'description' => $labels['description'] ?? '',
            'use_case'    => $labels['use_case'] ?? ($labels['use case'] ?? ''),
            'preview_url' => $v['preview_url'] ?? '',
        ];
    }
    return $out;
}

/** Synthèse vocale → octets MP3. */
function el_tts(string $voiceId, string $text, array $settings, string $modelId): string
{
    if ($voiceId === '' || $text === '') {
        fail('Voix et texte requis.');
    }
    $payload = [
        'text'           => $text,
        'model_id'       => $modelId,
        'voice_settings' => [
            'stability'        => (float) $settings['stability'],
            'similarity_boost' => (float) $settings['similarity'],
            'style'            => (float) $settings['style'],
            'use_speaker_boost' => (bool) $settings['speaker_boost'],
        ],
    ];
    $r = el_request('POST', '/v1/text-to-speech/' . rawurlencode($voiceId), $payload, 'audio/mpeg');
    if ($r['code'] !== 200 || stripos($r['ctype'], 'audio') === false) {
        $msg = el_error_msg($r['body']) ?? ('HTTP ' . $r['code']);
        fail('Échec de la synthèse vocale : ' . $msg, 502);
    }
    return $r['body'];
}

/**
 * Clé de cache d'une synthèse : dépend de la voix, du texte, des réglages et du modèle.
 * Deux demandes identiques → même fichier, aucune nouvelle facturation.
 */
function tts_cache_key(string $voiceId, string $text, array $s, string $modelId): string
{
    return sha1(implode('|', [
        $modelId, $voiceId,
        sprintf('%.3f', (float) $s['stability']),
        sprintf('%.3f', (float) $s['similarity']),
        sprintf('%.3f', (float) $s['style']),
        !empty($s['speaker_boost']) ? '1' : '0',
        trim($text),
    ]));
}

/**
 * Synthèse vocale avec cache disque.
 * @return array{path:string, cached:bool}  mp3 réutilisable (ne pas supprimer)
 */
function el_tts_file(string $voiceId, string $text, array $settings, string $modelId): array
{
    $dir = DATA_DIR . '/cache';
    if (!is_dir($dir)) {
        @mkdir($dir, 0775, true);
    }
    $path = $dir . '/' . tts_cache_key($voiceId, $text, $settings, $modelId) . '.mp3';
    if (is_file($path) && filesize($path) > 0) {
        return ['path' => $path, 'cached' => true];
    }
    $mp3 = el_tts($voiceId, $text, $settings, $modelId);   // appel facturé
    file_put_contents($path, $mp3, LOCK_EX);
    return ['path' => $path, 'cached' => false];
}

function el_error_msg(string $body): ?string
{
    $j = json_decode($body, true);
    if (!is_array($j)) {
        return null;
    }
    $d = $j['detail'] ?? $j['error'] ?? null;
    if (is_array($d)) {
        return (string) ($d['message'] ?? json_encode($d, JSON_UNESCAPED_UNICODE));
    }
    return $d !== null ? (string) $d : null;
}

/* --------------------------------------------------------------- mixage */

/**
 * Mixe une voix (mp3) avec une musique de fond optionnelle.
 * Produit un WAV 8kHz mono 16-bit (compatible standard téléphonique G.711).
 *
 * @param string|null $musicPath chemin musique, ou null pour voix seule
 * @return array{file:string, duration:float}  fichier dans output/
 */
function mix_message(string $voicePath, ?string $musicPath, array $opt): array
{
    $voiceDur = ff_duration($voicePath);
    if ($voiceDur <= 0) {
        fail('Voix générée invalide.', 500);
    }

    $outName = 'message_' . date('Ymd_His') . '_' . substr(rand_id(), 0, 6) . '.wav';
    $outPath = OUTPUT_DIR . '/' . $outName;

    if ($musicPath === null) {
        // Voix seule → conversion téléphonie
        $cmd = sprintf(
            'ffmpeg -y -i %s -af %s -ar 8000 -ac 1 -c:a pcm_s16le %s 2>&1',
            escapeshellarg($voicePath),
            escapeshellarg('aresample=8000,aformat=sample_fmts=s16:channel_layouts=mono,alimiter=limit=0.97'),
            escapeshellarg($outPath)
        );
    } else {
        $intro   = max(0.0, (float) $opt['intro']);
        $tail    = max(0.0, (float) $opt['tail']);
        $vol     = max(0.0, min(1.0, (float) $opt['music_volume']));
        $duck    = (bool) $opt['ducking'];
        $introMs = (int) round($intro * 1000);
        $total   = $intro + $voiceDur + $tail;
        $fadeOut = max(0.0, $total - 2.0);

        // Voix : mono, décalée de l'intro, paddée jusqu'à la fin
        $fc  = sprintf(
            '[0:a]aresample=44100,aformat=sample_fmts=fltp:channel_layouts=mono,adelay=%d:all=1,apad=pad_dur=%s[vp];',
            $introMs,
            rtrim(rtrim(number_format($tail, 3, '.', ''), '0'), '.')
        );
        $fc .= '[vp]asplit=2[vsc][vmix];';
        // Musique : mono, volume réduit, fades, coupée à la durée totale
        $fc .= sprintf(
            '[1:a]aresample=44100,aformat=sample_fmts=fltp:channel_layouts=mono,volume=%s,afade=t=in:st=0:d=1.5,afade=t=out:st=%s:d=2,atrim=0:%s[mt];',
            number_format($vol, 3, '.', ''),
            number_format($fadeOut, 3, '.', ''),
            number_format($total, 3, '.', '')
        );
        if ($duck) {
            // La musique baisse automatiquement quand la voix parle (ducking)
            $fc .= '[mt][vsc]sidechaincompress=threshold=0.03:ratio=8:attack=20:release=450[mduck];';
            $fc .= '[mduck][vmix]amix=inputs=2:normalize=0:duration=longest[mx];';
        } else {
            $fc .= '[mt][vmix]amix=inputs=2:normalize=0:duration=longest[mx];';
        }
        $fc .= '[mx]aresample=8000,aformat=sample_fmts=s16:channel_layouts=mono,alimiter=limit=0.95[out]';

        $cmd = sprintf(
            'ffmpeg -y -i %s -stream_loop -1 -i %s -filter_complex %s -map %s -t %s -ar 8000 -ac 1 -c:a pcm_s16le %s 2>&1',
            escapeshellarg($voicePath),
            escapeshellarg($musicPath),
            escapeshellarg($fc),
            escapeshellarg('[out]'),
            number_format($total, 3, '.', ''),
            escapeshellarg($outPath)
        );
    }

    exec($cmd, $log, $ret);
    if ($ret !== 0 || !is_file($outPath)) {
        fail('Échec du mixage ffmpeg : ' . implode("\n", array_slice($log, -4)), 500);
    }

    return ['file' => $outName, 'duration' => ff_duration($outPath)];
}

/* --------------------------------------------------------------- youtube */

function youtube_download(string $url): array
{
    if (!preg_match('~^https?://(www\.|m\.|music\.)?(youtube\.com/|youtu\.be/)~i', $url)) {
        fail('Lien YouTube invalide.');
    }
    $id   = 'yt_' . rand_id();
    $tmpl = DATA_DIR . '/' . $id . '.%(ext)s';

    $cookies = is_file(COOKIES_FILE) ? ' --cookies ' . escapeshellarg(COOKIES_FILE) : '';
    $deno    = is_file('/usr/local/bin/deno') ? ' --js-runtimes deno' : '';

    $cmd  = sprintf(
        'PATH=/usr/local/bin:$PATH yt-dlp%s%s -x --audio-format mp3 --audio-quality 0 --no-playlist ' .
        '--max-filesize 60M -o %s --print after_move:filepath %s 2>&1',
        $deno,
        $cookies,
        escapeshellarg($tmpl),
        escapeshellarg($url)
    );
    exec($cmd, $log, $ret);

    $file = DATA_DIR . '/' . $id . '.mp3';
    if ($ret !== 0 || !is_file($file)) {
        $tail = implode(' ', array_slice($log, -3));
        if (stripos($tail, 'confirm') !== false || stripos($tail, 'bot') !== false || stripos($tail, 'cookies') !== false) {
            fail('YouTube bloque le serveur (« confirm you\'re not a bot »). Déplie « YouTube bloque le serveur ? » sous le champ et importe un fichier cookies.txt pour débloquer.', 502);
        }
        fail('Téléchargement YouTube échoué : ' . $tail, 502);
    }
    return [
        'token'    => $id . '.mp3',
        'name'     => yt_title($log),
        'duration' => ff_duration($file),
    ];
}

function yt_title(array $log): string
{
    foreach ($log as $line) {
        if (stripos($line, '[download] Destination') !== false) {
            continue;
        }
    }
    return 'Audio YouTube';
}
