import os
import base64
import uuid
import asyncio
import time
import threading
from typing import Dict
from concurrent.futures import ThreadPoolExecutor

from fastapi import FastAPI, UploadFile, File, Form, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel

from tts_util import TTSConfig, TTSEngine


# ===============================================
# CONFIG
# ===============================================
PROMPT_FOLDER = "prompt_source"
os.makedirs(PROMPT_FOLDER, exist_ok=True)

JOBS_FOLDER = "jobs"
os.makedirs(JOBS_FOLDER, exist_ok=True)

OUTPUT_FOLDER = "output"
os.makedirs(OUTPUT_FOLDER, exist_ok=True)

# Job store
job_store: Dict[str, dict] = {}

# Auto cleanup
JOB_EXPIRE_SECONDS = 600

# FIFO queue and turbo workers
TURBO_MODE = True
TURBO_WORKERS = 3
WORKER_THREADPOOL_MAX = 4

# Rate limiting
RATE_LIMIT_TOKENS = 10
RATE_LIMIT_WINDOW = 60

# Token buckets
token_buckets: Dict[str, dict] = {}
token_lock = threading.Lock()

# Executor and TTS engine
thread_pool = ThreadPoolExecutor(max_workers=WORKER_THREADPOOL_MAX)
config = TTSConfig()
tts_engine = TTSEngine(config, thread_pool)

app = FastAPI(title="Chatterbox TTS Server - Turbo + FIFO + RateLimit + WAV")


# ===============================================
# RATE LIMIT UTILITIES
# ===============================================
def get_client_ip(request: Request) -> str:
    """Get client IP from request"""
    xff = request.headers.get("x-forwarded-for")
    if xff:
        return xff.split(",")[0].strip()
    if request.client:
        return request.client.host
    return "unknown"


def allow_request_ip(ip: str) -> bool:
    """Check if request from IP is allowed (token bucket)"""
    now = time.time()
    with token_lock:
        bucket = token_buckets.get(ip)
        if bucket is None:
            token_buckets[ip] = {"tokens": RATE_LIMIT_TOKENS - 1, "last": now}
            return True

        # Refill tokens
        elapsed = now - bucket["last"]
        refill = (elapsed / RATE_LIMIT_WINDOW) * RATE_LIMIT_TOKENS
        if refill > 0:
            bucket["tokens"] = min(RATE_LIMIT_TOKENS, bucket["tokens"] + refill)
            bucket["last"] = now

        if bucket["tokens"] >= 1:
            bucket["tokens"] -= 1
            return True
        else:
            return False


# ===============================================
# BACKGROUND WORKERS
# ===============================================
job_queue: asyncio.Queue = asyncio.Queue()


async def worker_loop(worker_id: int):
    """Background worker for processing TTS jobs"""
    while True:
        job_id = await job_queue.get()
        job = job_store.get(job_id)
        if job is None:
            job_queue.task_done()
            continue

        # Mark as processing
        job["status"] = "processing"
        job["worker"] = worker_id
        job["timestamp"] = time.time()

        try:
            prompt = job.get("prompt")
            text = job.get("text")
            prompt_path = os.path.join(PROMPT_FOLDER, f"{prompt}.wav")

            if not os.path.exists(prompt_path):
                job["status"] = "error"
                job["error"] = "Prompt tidak ditemukan"
                job["timestamp"] = time.time()
                job_queue.task_done()
                continue

            # Generate audio
            out_wav = os.path.join(OUTPUT_FOLDER, f"{job_id}.wav")
            await tts_engine.generate_to_file(text, prompt_path, out_wav)

            job["status"] = "done"
            job["result"] = out_wav
            job["timestamp"] = time.time()

        except Exception as e:
            job["status"] = "error"
            job["error"] = str(e)
            job["timestamp"] = time.time()
        finally:
            job_queue.task_done()


async def cleanup_worker():
    """Background cleanup worker for expired jobs"""
    while True:
        now = time.time()
        expired = []

        for jid, job in list(job_store.items()):
            if (
                job.get("status") == "done"
                and now - job.get("timestamp", 0) > JOB_EXPIRE_SECONDS
            ):
                f = job.get("result")
                # if f and os.path.exists(f):
                #     try:
                #         os.remove(f)
                #     except Exception:
                #         pass
                expired.append(jid)

        for jid in expired:
            job_store.pop(jid, None)

        # Purge stale token buckets
        with token_lock:
            stale_ips = []
            for ip, b in token_buckets.items():
                if now - b.get("last", 0) > RATE_LIMIT_WINDOW * 10:
                    stale_ips.append(ip)
            for ip in stale_ips:
                token_buckets.pop(ip, None)

        await asyncio.sleep(30)


# ===============================================
# STARTUP
# ===============================================
@app.on_event("startup")
async def startup():
    """Initialize TTS engine and start background workers"""
    # Load TTS model
    tts_engine.load_model()

    # Start cleanup worker
    asyncio.create_task(cleanup_worker())

    # Start turbo workers
    worker_count = TURBO_WORKERS if TURBO_MODE else 1
    for i in range(worker_count):
        asyncio.create_task(worker_loop(i + 1))


# ===============================================
# PYDANTIC MODELS
# ===============================================
class RegisterPromptBase64(BaseModel):
    prompt_name: str
    base64_audio: str


class DeletePrompt(BaseModel):
    prompt_name: str


class RenamePrompt(BaseModel):
    old_name: str
    new_name: str


# ===============================================
# PROMPT MANAGEMENT ENDPOINTS
# ===============================================
@app.post("/register-prompt-base64")
async def register_prompt_base64(data: RegisterPromptBase64):
    """Register a voice prompt from base64 audio"""
    filename = f"{data.prompt_name}.wav"
    path = os.path.join(PROMPT_FOLDER, filename)
    try:
        raw = base64.b64decode(data.base64_audio)
        with open(path, "wb") as f:
            f.write(raw)
        return {"status": "ok", "file": filename}
    except Exception as e:
        return JSONResponse(status_code=400, content={"error": str(e)})


@app.post("/register-prompt-file")
async def register_prompt_file(prompt: UploadFile = File(...), name: str = Form(None)):
    """Register a voice prompt from uploaded file"""
    prompt_name = name or os.path.splitext(prompt.filename)[0]
    save_path = os.path.join(PROMPT_FOLDER, f"{prompt_name}.wav")
    try:
        with open(save_path, "wb") as f:
            f.write(await prompt.read())
        return {"status": "ok", "file": f"{prompt_name}.wav"}
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


@app.get("/list-prompt")
async def list_prompt():
    """List all registered voice prompts"""
    lst = [f for f in os.listdir(PROMPT_FOLDER) if f.lower().endswith(".wav")]
    return {
        "count": len(lst),
        "prompts": lst,
        "prompt_names": [os.path.splitext(f)[0] for f in lst],
    }


@app.post("/delete-prompt")
async def delete_prompt(data: DeletePrompt):
    """Delete a voice prompt"""
    path = os.path.join(PROMPT_FOLDER, f"{data.prompt_name}.wav")
    if not os.path.exists(path):
        return JSONResponse(
            status_code=404, content={"error": "Prompt tidak ditemukan"}
        )
    try:
        os.remove(path)
        return {"status": "ok", "deleted": f"{data.prompt_name}.wav"}
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


@app.post("/rename-prompt")
async def rename_prompt(data: RenamePrompt):
    """Rename a voice prompt"""
    old = os.path.join(PROMPT_FOLDER, f"{data.old_name}.wav")
    new = os.path.join(PROMPT_FOLDER, f"{data.new_name}.wav")
    if not os.path.exists(old):
        return JSONResponse(
            status_code=404, content={"error": "Prompt lama tidak ditemukan"}
        )
    if os.path.exists(new):
        return JSONResponse(
            status_code=400, content={"error": "Nama baru sudah digunakan"}
        )
    try:
        os.rename(old, new)
        return {"status": "ok", "from": data.old_name, "to": data.new_name}
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


@app.post("/tts-async")
async def tts_async(request: Request, text: str = Form(...), prompt: str = Form(...)):
    """Asynchronous TTS - enqueue job and return job_id"""
    client_ip = get_client_ip(request)
    if not allow_request_ip(client_ip):
        return JSONResponse(status_code=429, content={"error": "rate limit exceeded"})

    job_id = str(uuid.uuid4())
    job_store[job_id] = {
        "status": "pending",
        "timestamp": time.time(),
        "prompt": prompt,
        "text": text,
        "client_ip": client_ip,
    }

    # Enqueue (FIFO)
    await job_queue.put(job_id)

    return {"status": "queued", "job_id": job_id, "check": f"/result/{job_id}"}


@app.get("/result/{job_id}")
async def tts_result(job_id: str):
    """Get result of async TTS job"""
    job = job_store.get(job_id)
    if not job:
        return JSONResponse(
            status_code=404, content={"error": "Job ID tidak ditemukan"}
        )

    # Still processing
    if job["status"] in ("pending", "processing"):
        return {
            "status": job["status"],
            "job_id": job_id,
            "worker": job.get("worker"),
            "timestamp": job.get("timestamp"),
        }

    # Error
    if job["status"] == "error":
        return job

    # Done - return file
    result_path = job.get("result")
    if not result_path or not os.path.exists(result_path):
        return JSONResponse(
            status_code=500,
            content={"status": "error", "error": "File hasil tidak ditemukan"},
        )

    return JSONResponse(
        status_code=200,
        content={"status": "done", "job_id": job_id, "file": result_path},
    )

@app.get("/list-file")
async def list_file():
    """List all files inside OUTPUT_FOLDER"""
    try:
        files = [
            f for f in os.listdir(OUTPUT_FOLDER)
            if os.path.isfile(os.path.join(OUTPUT_FOLDER, f))
        ]

        # Hanya file WAV (sesuai output engine)
        wav_files = [f for f in files if f.lower().endswith(".wav")]

        # Include metadata timestamp dari job_store
        detailed = []
        for f in wav_files:
            full_path = os.path.join(OUTPUT_FOLDER, f)
            size = os.path.getsize(full_path)

            # Cari job yang terkait (jika ada)
            related_job = None
            for jid, job in job_store.items():
                if job.get("result") == full_path:
                    related_job = {
                        "job_id": jid,
                        "status": job.get("status"),
                        "timestamp": job.get("timestamp"),
                        "prompt": job.get("prompt"),
                    }
                    break

            detailed.append(
                {
                    "file": f,
                    "size_bytes": size,
                    "path": full_path,
                    "job": related_job,
                }
            )

        return {
            "count": len(wav_files),
            "files": detailed,
        }

    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


async def iterfile(file_path):
    with open(file_path, "rb") as f:
        chunk = f.read(4096)
        while chunk:
            yield chunk
            chunk = f.read(4096)


@app.get("/file/{file_name}")
async def get_output_file(file_name: str):
    if not file_name.endswith(".wav"):
        file_name = f"{file_name}.wav"
    file_path = os.path.join(OUTPUT_FOLDER, file_name)
    if not os.path.exists(file_path):
        return JSONResponse(status_code=404, content={"error": "File tidak ditemukan"})

    return StreamingResponse(
        iterfile(file_path),
        media_type="audio/wav",
        headers={"Content-Disposition": f"attachment; filename={file_name}"},
    )


# ===============================================
# FILE MANAGEMENT ENDPOINTS
# ===============================================
@app.delete("/rm/{filename}")
async def remove_file(filename: str):
    """Delete a single output file"""
    if not filename.endswith(".wav"):
        filename = f"{filename}.wav"
    path = os.path.join(OUTPUT_FOLDER, filename)

    if not os.path.exists(path):
        return JSONResponse(status_code=404, content={"error": "File tidak ditemukan"})

    try:
        os.remove(path)
        # Remove from job_store
        for jid, job in list(job_store.items()):
            if job.get("result") == path:
                job_store.pop(jid, None)
        return {"status": "ok", "deleted": filename}
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})

@app.post("/cleanup")
async def manual_cleanup():
    """Manual cleanup - remove all output files and clear done/error jobs"""
    removed = []
    for f in os.listdir(OUTPUT_FOLDER):
        fp = os.path.join(OUTPUT_FOLDER, f)
        if os.path.isfile(fp):
            try:
                os.remove(fp)
                removed.append(f)
            except:
                pass

    # Clear done/error jobs
    cleared = []
    for jid in list(job_store.keys()):
        if job_store[jid]["status"] in ("done", "error"):
            job_store.pop(jid, None)
            cleared.append(jid)

    return {"status": "ok", "removed_files": removed, "jobs_cleared": cleared}
