/**
 * Production-Ready TTS Async Client - Fixed WAV Merge
 * Author: ChatGPT (Optimized & Fixed)
 */

import fs from "fs";
import path from "path";

const HOST = "https://office4-chatterbox.wibudev.com";
const TEMP_DIR = "./temp-tts";

// Pastikan folder temp ada
if (!fs.existsSync(TEMP_DIR)) {
    fs.mkdirSync(TEMP_DIR, { recursive: true });
}

/* ============================================================
   CLEAN TEXT (AMAN UNTUK UNICODE)
============================================================ */

function cleanText(text: string): string {
    return text.replace(/[^\p{L}\p{N}\p{P}\p{Zs}]/gu, "").trim();
}

/* ============================================================
   SPLIT TEXT (MAKSIMAL 200 CHAR + CARI TITIK/KOMA/!?)
============================================================ */

function splitText(text: string, max = 200): string[] {
    const chunks: string[] = [];
    text = text.trim();

    while (text.length > 0) {
        if (text.length <= max) {
            chunks.push(text.trim());
            break;
        }

        const slice = text.slice(0, max);

        // cari tanda baca terakhir
        const lastPunct = Math.max(
            slice.lastIndexOf("."),
            slice.lastIndexOf(","),
            slice.lastIndexOf(";"),
            slice.lastIndexOf("!"),
            slice.lastIndexOf("?")
        );

        const cutIndex = lastPunct !== -1 ? lastPunct + 1 : max;

        const part = text.slice(0, cutIndex).trim();
        if (part.length > 0) chunks.push(part);

        text = text.slice(cutIndex).trim();

        if (text.length === 0) break;
    }

    return chunks;
}

/* ============================================================
   FETCH WITH RETRY
============================================================ */

async function fetchRetry(url: string, options: any = {}, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetch(url, options);
        } catch (err) {
            console.warn(`Fetch attempt ${i + 1} failed:`, err);
            if (i === retries - 1) throw err;
            await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
        }
    }
}

/* ============================================================
   TTS ASYNC REQUEST
============================================================ */

async function generate(text: string) {
    const res = await fetchRetry(`${HOST}/tts-async`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({ text, prompt: "dayu" }),
    });

    if (!res) {
        throw new Error("Failed to fetch result");
    }

    let data;
    try {
        data = await res.json();
    } catch {
        throw new Error("Invalid JSON response from server");
    }

    if (!data.job_id) {
        console.error("❌ Response tidak mengandung job_id:", data);
        throw new Error("job_id missing in generate response");
    }

    return data;
}

/* ============================================================
   FETCH RESULT + DOWNLOAD AUDIO
============================================================ */

async function fetchResult(jobId: string) {
    const res = await fetchRetry(`${HOST}/result/${jobId}`);

    if (!res) {
        throw new Error("Failed to fetch result");
    }

    const type = res.headers.get("content-type") || "";

    if (type.includes("application/json")) {
        try {
            return await res.json();
        } catch {
            throw new Error("Invalid JSON response on fetchResult");
        }
    }

    // Audio response
    const buf = Buffer.from(await res.arrayBuffer());

    if (buf.length < 44) {
        throw new Error("Invalid WAV: too small");
    }

    const file = `${TEMP_DIR}/audio_${jobId}.wav`;
    fs.writeFileSync(file, buf);
    return { status: "done", filePath: file };
}

/* ============================================================
   READ WAV HEADER INFO
============================================================ */

interface WavInfo {
    sampleRate: number;
    numChannels: number;
    bitsPerSample: number;
    byteRate: number;
    blockAlign: number;
    dataSize: number;
}

function readWavInfo(buffer: Buffer): WavInfo {
    // Read WAV header
    const sampleRate = buffer.readUInt32LE(24);
    const byteRate = buffer.readUInt32LE(28);
    const blockAlign = buffer.readUInt16LE(32);
    const bitsPerSample = buffer.readUInt16LE(34);
    const numChannels = buffer.readUInt16LE(22);
    
    // Find data chunk (skip any extra chunks like LIST, etc)
    let dataOffset = 36;
    while (dataOffset < buffer.length - 8) {
        const chunkId = buffer.toString('ascii', dataOffset, dataOffset + 4);
        const chunkSize = buffer.readUInt32LE(dataOffset + 4);
        
        if (chunkId === 'data') {
            return {
                sampleRate,
                numChannels,
                bitsPerSample,
                byteRate,
                blockAlign,
                dataSize: chunkSize
            };
        }
        
        dataOffset += 8 + chunkSize;
    }
    
    // Fallback to old method if data chunk not found properly
    const dataSize = buffer.readUInt32LE(40);
    return {
        sampleRate,
        numChannels,
        bitsPerSample,
        byteRate,
        blockAlign,
        dataSize
    };
}

/* ============================================================
   MERGE WAV FILES (PRODUCTION SAFE - FIXED)
============================================================ */

function mergeWav(files: string[], output: string) {
    if (files.length === 0) throw new Error("No files to merge");

    console.log(`\n🔧 Merging ${files.length} WAV files...`);

    // Read first file to get format info
    const firstFile = fs.readFileSync(files[0] as string);
    const wavInfo = readWavInfo(firstFile);
    
    console.log(`📊 WAV Format:`);
    console.log(`   Sample Rate: ${wavInfo.sampleRate} Hz`);
    console.log(`   Channels: ${wavInfo.numChannels}`);
    console.log(`   Bits per Sample: ${wavInfo.bitsPerSample}`);
    console.log(`   Block Align: ${wavInfo.blockAlign}`);

    // Collect all PCM data
    const pcmBuffers: Buffer[] = [];
    let totalPCM = 0;

    for (let i = 0; i < files.length; i++) {
        const f = files[i];
        if (!fs.existsSync(f as string)) {
            throw new Error(`File not found: ${f}`);
        }

        const fileBuffer = fs.readFileSync(f as string);
        if (fileBuffer.length < 44) {
            throw new Error(`Invalid WAV file: ${f}`);
        }

        // Find data chunk position (handle files with extra metadata)
        let dataOffset = 36;
        let dataSize = 0;
        
        while (dataOffset < fileBuffer.length - 8) {
            const chunkId = fileBuffer.toString('ascii', dataOffset, dataOffset + 4);
            const chunkSize = fileBuffer.readUInt32LE(dataOffset + 4);
            
            if (chunkId === 'data') {
                dataSize = chunkSize;
                dataOffset += 8; // Skip 'data' + size fields
                break;
            }
            
            dataOffset += 8 + chunkSize;
        }
        
        // Fallback if data chunk not found properly
        if (dataSize === 0) {
            dataSize = fileBuffer.readUInt32LE(40);
            dataOffset = 44;
        }

        // Validate data size
        const availableData = fileBuffer.length - dataOffset;
        const actualDataSize = Math.min(dataSize, availableData);
        
        if (actualDataSize <= 0) {
            console.warn(`⚠️  Warning: File ${i + 1} has no valid PCM data, skipping...`);
            continue;
        }

        const pcmData = fileBuffer.slice(dataOffset, dataOffset + actualDataSize);
        
        // Ensure data is aligned to block size
        const remainder = pcmData.length % wavInfo.blockAlign;
        const alignedData = remainder === 0 
            ? pcmData 
            : pcmData.slice(0, pcmData.length - remainder);
        
        pcmBuffers.push(alignedData);
        totalPCM += alignedData.length;
        
        console.log(`   ✓ File ${i + 1}/${files.length}: ${alignedData.length} bytes`);
    }

    if (totalPCM === 0) {
        throw new Error("No valid PCM data found in any files");
    }

    console.log(`\n📦 Total PCM data: ${totalPCM} bytes`);

    // Create new WAV header
    const header = Buffer.alloc(44);
    
    // RIFF header
    header.write('RIFF', 0);
    header.writeUInt32LE(36 + totalPCM, 4); // ChunkSize
    header.write('WAVE', 8);
    
    // fmt subchunk
    header.write('fmt ', 12);
    header.writeUInt32LE(16, 16); // Subchunk1Size (16 for PCM)
    header.writeUInt16LE(1, 20);  // AudioFormat (1 for PCM)
    header.writeUInt16LE(wavInfo.numChannels, 22); // NumChannels
    header.writeUInt32LE(wavInfo.sampleRate, 24);  // SampleRate
    header.writeUInt32LE(wavInfo.byteRate, 28);    // ByteRate
    header.writeUInt16LE(wavInfo.blockAlign, 32);  // BlockAlign
    header.writeUInt16LE(wavInfo.bitsPerSample, 34); // BitsPerSample
    
    // data subchunk
    header.write('data', 36);
    header.writeUInt32LE(totalPCM, 40); // Subchunk2Size

    // Combine header and all PCM data
    const combined = Buffer.concat([header, ...pcmBuffers]);

    fs.writeFileSync(output, combined);
    console.log(`✅ Merged file saved: ${output} (${combined.length} bytes)\n`);
}

/* ============================================================
   CLEANUP TEMP FILES
============================================================ */

function cleanup() {
    console.log("🧹 Cleaning up temporary files...");
    const files = fs.readdirSync(TEMP_DIR);
    for (const f of files) {
        fs.unlinkSync(path.join(TEMP_DIR, f));
    }
    console.log(`   Removed ${files.length} temporary files\n`);
}

/* ============================================================
   MAIN
============================================================ */

(async () => {
    console.log("🎙️  TTS Async Client Starting...\n");

    let text = `
Ayu dan Niko tinggal serumah sebagai teman kost. Mereka dekat, tapi selalu bersitegang soal satu hal: kopi instan.
Ayu percaya kopi instan adalah anugerah Tuhan untuk mahasiswa miskin. Niko, sebaliknya, sok jadi barista—ngopi harus pakai French press, biji kopi digiling sendiri, dan airnya harus 92 derajat Celsius persis.
Suatu pagi, Ayu kehabisan stok kopi instannya. Dengan mata setengah terbuka dan rambut acak-acakan, ia merayap ke dapur dan melihat Niko sedang sibuk "mengritik" suhu air yang baru direbus.
"Boleh pinjam kopimu?" tanya Ayu lemas.
Niko menoleh dramatis. "Boleh… asal kamu janji: jangan campur gula lebih dari satu sendok, jangan tambah susu kental manis, dan jangan bilang ini 'kopi tubruk ala warung'!"
Ayu mengangguk patuh
Tapi begitu Niko ke kamar ganti baju, Ayu diam-diam mengambil seluruh bubuk kopi Niko, mencampurnya dengan susu kental manis, gula aren, dan… sedikit krimer dari sachet kopi instannya yang terakhir.
Saat Niko kembali, ia langsung mencium aroma "kiamat kopi". Matanya melotot. "APA YANG KAMU LAKUKAN PADA BIJI KENYA SINGLE ORIGIN-ku?!"
Ayu menyeruput santai. "Namanya sekarang… Kopi Cinta Gagal Fokus. Enak, lho."
Niko menghela napas, lalu duduk di sebelahnya. Dengan berat hati, ia mencicipi… dan ternyata… enak juga.
Sejak hari itu, mereka punya ritual baru: Sabtu pagi, mereka bikin "Kopi Cinta Gagal Fokus" bersama—dengan tetap berdebat soal apakah itu kopi atau susu manis beraroma kopi.
`;

    text = cleanText(text);
    const parts = splitText(text, 400);

    console.log(`📝 Total chunks: ${parts.length}\n`);

    const jobs: string[] = [];

    // SEND JOBS
    console.log("📤 Sending jobs to server...");
    for (let i = 0; i < parts.length; i++) {
        try {
            const res = await generate(parts[i] as string) ;
            jobs.push(res.job_id);
            console.log(`   ✓ Job ${i + 1}/${parts.length}: ${res.job_id}`);
        } catch (error) {
            console.error(`❌ Failed to generate job for chunk ${i + 1}:`, error);
            process.exit(1);
        }
    }

    console.log(`\n✅ All ${jobs.length} jobs submitted\n`);

    // POLLING SEMUA
    console.log("⏳ Waiting for processing...");
    const resultFiles: string[] = [];

    for (let i = 0; i < jobs.length; i++) {
        const jobId = jobs[i];
        let status = "processing";
        let delay = 1500;
        let attempts = 0;

        while (status === "processing" || status === "pending") {
            try {
                const res = await fetchResult(jobId as string);
                status = res.status;

                if (status === "done" && res.filePath) {
                    resultFiles.push(res.filePath as string);
                    console.log(`   ✓ Job ${i + 1}/${jobs.length} completed: ${jobId}`);
                    break;
                } else if (status === "error") {
                    console.error(`   ❌ Job ${i + 1}/${jobs.length} failed: ${res.error || 'Unknown error'}`);
                    throw new Error(`Job ${jobId} failed`);
                }
            } catch (error) {
                console.error(`❌ Error fetching result for job ${i + 1}:`, error);
                process.exit(1);
            }

            attempts++;
            await new Promise((r) => setTimeout(r, delay));
            delay = Math.min(delay * 1.2, 5000);

            // Timeout after 2 minutes
            if (attempts > 80) {
                console.error(`❌ Job ${jobId} timeout after 2 minutes`);
                process.exit(1);
            }
        }
    }

    console.log(`\n✅ All jobs completed!\n`);

    // MERGE SEMUA WAV
    const outputFile = "./final_merged.wav";
    mergeWav(resultFiles, outputFile);

    console.log("🎉 DONE! Output:", outputFile);

    // Cleanup temp folder
    cleanup();
})();