
import fs from "fs";
import path from "path";
import { v4 as uuid } from "uuid";

let sessionId: string | null = null;
const BASE =
    "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/";

/** Konfigurasi sessionId */
export function config(sid: string) {
    sessionId = sid;
}

export async function ensureOutputDir() {
    const OUTPUT_DIR_BASE = "./tts_tiktok_output";
    const OUTPUT_DIR_PARTS = path.resolve(OUTPUT_DIR_BASE, "./part");
    const OUTPUT_DIR_FINAL = path.resolve(OUTPUT_DIR_BASE, "./final");
    await fs.promises.mkdir(OUTPUT_DIR_BASE, { recursive: true }).catch(() => { });
    await fs.promises.mkdir(OUTPUT_DIR_PARTS, { recursive: true }).catch(() => { });
    await fs.promises.mkdir(OUTPUT_DIR_FINAL, { recursive: true }).catch(() => { });

    return {
        OUTPUT_DIR_BASE,
        OUTPUT_DIR_PARTS,
        OUTPUT_DIR_FINAL,
    }
}

const { OUTPUT_DIR_BASE, OUTPUT_DIR_PARTS, OUTPUT_DIR_FINAL } = await ensureOutputDir();

/** ---- Utility: split text pintar (maxLen, tidak memotong kata) ---- */
function splitTextSmart(text: string, maxLen = 200): string[] {
    const parts: string[] = [];
    let remaining = text.trim();

    while (remaining.length > maxLen) {
        let splitPos = remaining.lastIndexOf(" ", maxLen);

        if (splitPos === -1) {
            // tidak ada spasi — paksa split di maxLen
            splitPos = maxLen;
        }

        const chunk = remaining.slice(0, splitPos).trim();
        if (chunk.length === 0) {
            // safety guard: jika chunk kosong karena whitespace, skip satu char
            parts.push(remaining.slice(0, maxLen));
            remaining = remaining.slice(maxLen).trim();
        } else {
            parts.push(chunk);
            remaining = remaining.slice(splitPos).trim();
        }
    }

    if (remaining.length > 0) parts.push(remaining);

    return parts;
}

/** ---- Simple semaphore / concurrency limiter ---- */
class Semaphore {
    private permits: number;
    private waiters: Array<() => void> = [];

    constructor(permits: number) {
        this.permits = permits;
    }

    async acquire(): Promise<void> {
        if (this.permits > 0) {
            this.permits--;
            return;
        }
        await new Promise<void>((resolve) => {
            this.waiters.push(() => {
                this.permits--;
                resolve();
            });
        });
    }

    release(): void {
        this.permits++;
        const next = this.waiters.shift();
        if (next) {
            // immediately give to next waiter
            next();
        }
    }
}

/** ---- Helper: sleep ms ---- */
function sleep(ms: number) {
    return new Promise((res) => setTimeout(res, ms));
}

/** ---- Fetch single TTS part with retry logic ---- */
async function fetchTTSPartWithRetry(
    partText: string,
    partIndex: number,
    fileName: string,
    speaker: string,
    maxRetries: number
): Promise<string> {
    const attemptFetch = async (attempt: number): Promise<string> => {
        if (!sessionId) throw new Error("sessionId belum dikonfigurasi");

        const url =
            BASE +
            "?" +
            new URLSearchParams({
                text_speaker: speaker,
                req_text: partText,
                speaker_map_type: "0",
                aid: "1233",
            }).toString();

        const headers = {
            Cookie: `sessionid=${sessionId}`,
            "User-Agent":
                "com.zhiliaoapp.musically/2023101630 (Linux; U; Android 13; en_US; Pixel 7; Build/TQ3A.230805.001)",
        };

        let resp: Response;
        try {
            resp = await fetch(url, { method: "POST", headers });
        } catch (err) {
            throw new Error(`Network error on fetch (attempt ${attempt}): ${err}`);
        }

        const contentType = resp.headers.get("content-type");
        const outputName = `${fileName}_part-${partIndex + 1}.mp3`;
        const outputPath = path.resolve(OUTPUT_DIR_PARTS, outputName);

        try {
            if (contentType?.includes("application/json")) {
                const json = await resp.json();
                if (json.status_code !== 0) {
                    throw new Error(
                        `TikTok TTS error (status_code != 0) on attempt ${attempt}: ${JSON.stringify(
                            json
                        )}`
                    );
                }
                const base64 = json.data?.v_str;
                if (!base64) throw new Error("Tidak menemukan v_str pada respons");
                const buffer = Buffer.from(base64, "base64");
                await fs.promises.writeFile(outputPath, buffer);
            } else {
                // langsung audio
                const arr = await resp.arrayBuffer();
                const buf = Buffer.from(arr);
                await fs.promises.writeFile(outputPath, buf);
            }

            // success
            return outputPath;
        } catch (err) {
            // jika file ditulis parsial — hapus sebelum retry
            try {
                if (fs.existsSync(outputPath)) await fs.promises.unlink(outputPath);
            } catch (_) {
                // ignore
            }
            throw err;
        }
    };

    let attempt = 0;
    let lastErr: any = null;
    while (attempt <= maxRetries) {
        try {
            return await attemptFetch(attempt + 1);
        } catch (err) {
            lastErr = err;
            attempt++;
            if (attempt > maxRetries) break;
            // exponential backoff: 500ms * 2^(attempt-1)
            const backoff = 500 * 2 ** (attempt - 1);
            await sleep(backoff + Math.random() * 200);
        }
    }

    throw new Error(
        `Failed to fetch TTS part after ${maxRetries} retries. Last error: ${lastErr}`
    );
}

/** ---- Merge parts lossless (concatenate bytes) ---- */
async function mergeMP3Files(parts: string[], outputFile: string) {
    // buat write stream (Bun + Node compatible)
    const writeStream = fs.createWriteStream(outputFile);

    for (const file of parts) {
        const buffer = await fs.promises.readFile(file);
        writeStream.write(buffer);
    }

    writeStream.end();

    // pastikan stream selesai (wrap event)
    await new Promise<void>((resolve, reject) => {
        writeStream.on("finish", () => resolve());
        writeStream.on("error", (e) => reject(e));
    });
}

/** ---- Cleanup helper: try delete files (ignore errors) ---- */
async function tryCleanupFiles(files: string[]) {
    for (const f of files) {
        try {
            if (fs.existsSync(f)) await fs.promises.unlink(f);
        } catch (_) {
            // ignore cleanup errors
        }
    }
}

async function createAudioFromText(
    params:
        {
            text: string,
            fileName?: string,
            speaker?: string
        },
    options?: { concurrency?: number; maxRetries?: number }
): Promise<string> {
    const concurrency = options?.concurrency ?? 5;
    const maxRetries = options?.maxRetries ?? 3;

    const { text , speaker = "id_001" } = params;
    const fileName = `${params.fileName}_${uuid()}`;

    if (!text || !text.trim()) throw new Error("Text kosong");
    if (!sessionId) throw new Error("sessionId belum dikonfigurasi");

    // split teks
    const chunks = splitTextSmart(text, 200);

    // prepare semaphore
    const sem = new Semaphore(concurrency);

    const partFiles: string[] = [];
    const tasks: Promise<void>[] = [];
    let failed = false;
    let failureError: any = null;

    // for each chunk, create a task that respects concurrency
    for (let i = 0; i < chunks.length; i++) {
        const idx = i;
        const chunk = chunks[i];

        const task = (async () => {
            await sem.acquire();
            try {
                const outputPath = await fetchTTSPartWithRetry(
                    chunk!,
                    idx,
                    fileName,
                    speaker,
                    maxRetries
                );
                partFiles[idx] = outputPath;
            } catch (err) {
                failed = true;
                failureError = err;
            } finally {
                sem.release();
            }
        })();

        tasks.push(task);
    }

    // wait all tasks
    await Promise.all(tasks);

    if (failed) {
        // cleanup any part files created
        await tryCleanupFiles(partFiles.filter(Boolean));
        throw new Error(
            `Gagal membuat beberapa part TTS. Error: ${String(failureError)}`
        );
    }

    // merge parts
    const finalFile = path.resolve(OUTPUT_DIR_FINAL, `${fileName}.mp3`);
    try {
        // ensure parts are in order
        const orderedParts = partFiles.slice(0, chunks.length);
        await mergeMP3Files(orderedParts, finalFile);

        // cleanup part files after merge
        await tryCleanupFiles(orderedParts);
    } catch (err) {
        // cleanup partial final file and parts
        try {
            if (fs.existsSync(finalFile)) await fs.promises.unlink(finalFile);
        } catch (_) { }
        await tryCleanupFiles(partFiles.filter(Boolean));
        throw new Error(`Gagal merge/cleanup: ${err}`);
    }

    return finalFile;
}


export async function tts_tiktok(params: { text: string, sessionId: string, fileName?: string }, options?: { concurrency?: number; maxRetries?: number }) {
    config(params.sessionId)
    const create = await createAudioFromText({ text: params.text, fileName: params.fileName }, options);
    return path.basename(create)
}

if (import.meta.main) {

    const text = `
Saat ini layanan pengurusan KTP secara langsung di Desa Darmasaba sedang tidak tersedia. Namun Anda tetap bisa mengurus KTP dengan langkah-langkah berikut:

1. Persiapkan dokumen sesuai kebutuhan, misalnya fotokopi KK, akta kelahiran, surat nikah (jika sudah menikah), atau surat keterangan hilang jika KTP lama hilang.
2. Datang ke kantor Desa Darmasaba untuk mendapatkan surat pengantar pembuatan KTP.
3. Serahkan dokumen dan surat pengantar tersebut ke kantor Kecamatan Abiansemal untuk proses verifikasi.
4. Permohonan akan diteruskan ke Disdukcapil Kabupaten Badung untuk pencetakan KTP.
5. Pengambilan KTP biasanya setelah 14 hari kerja.

Anda juga bisa memanfaatkan layanan perekaman e-KTP yang sudah tersedia di kantor desa agar proses lebih mudah. Untuk informasi lebih lengkap, Anda dapat mengunjungi situs resmi desa atau Disdukcapil Badung.

Mau saya bantu carikan info prosedur surat lain seperti surat keterangan domisili atau surat lainnya yang bisa diurus di Desa Darmasaba?
 
    `
    tts_tiktok({
        text,
        sessionId: "7b7a48e1313f9413825a8544e52b1481"
    })
}