import os
import uuid
import json
import subprocess
import tempfile
import shutil


AUDIO_FORMATS = {"mp3", "wav", "ogg", "flac"}


def _parse_url_entry(url_entry):
    if isinstance(url_entry, str):
        try:
            parsed = json.loads(url_entry)
            if isinstance(parsed, dict):
                return parsed.get("path", "")
        except (json.JSONDecodeError, TypeError):
            return url_entry
    elif isinstance(url_entry, dict):
        return url_entry.get("path", "")
    return ""


def _extract_text(file_path):
    """Extract plain text from a document or ebook file."""
    ext = os.path.splitext(file_path)[1].lower().lstrip(".")

    # Already plain text
    if ext == "txt":
        with open(file_path, encoding="utf-8", errors="replace") as f:
            return f.read()

    # PDF — use PyMuPDF (fitz) if available, else pdftotext
    if ext == "pdf":
        try:
            import fitz
            doc = fitz.open(file_path)
            return "\n".join(page.get_text() for page in doc)
        except Exception:
            with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
                tmp_txt = tmp.name
            try:
                subprocess.run(["pdftotext", file_path, tmp_txt],
                               capture_output=True, timeout=60)
                if os.path.exists(tmp_txt):
                    with open(tmp_txt, encoding="utf-8", errors="replace") as f:
                        return f.read()
            finally:
                try:
                    os.unlink(tmp_txt)
                except OSError:
                    pass
        return ""

    # Ebook / Office formats — convert to TXT via ebook-convert (Calibre)
    with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
        tmp_txt = tmp.name
    try:
        subprocess.run(["ebook-convert", file_path, tmp_txt],
                       capture_output=True, timeout=180)
        if os.path.exists(tmp_txt) and os.path.getsize(tmp_txt) > 0:
            with open(tmp_txt, encoding="utf-8", errors="replace") as f:
                return f.read()
    except Exception:
        pass
    finally:
        try:
            os.unlink(tmp_txt)
        except OSError:
            pass

    return ""


def _speak(text, output_path, lang="en"):
    """
    Convert text to speech using espeak-ng.
    Produces WAV directly; ffmpeg converts to other formats.

    System requirement: sudo apt install espeak-ng
    """
    if not text or not text.strip():
        raise RuntimeError("No text to synthesize.")

    # espeak-ng accepts a text file to avoid shell-escaping issues
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".txt", delete=False, encoding="utf-8"
    ) as tf:
        tf.write(text[:200_000])   # cap at ~200 k chars
        tf_path = tf.name

    ext = os.path.splitext(output_path)[1].lower()
    need_wav = ext != ".wav"

    wav_path = output_path if not need_wav else output_path.replace(ext, ".wav")

    try:
        result = subprocess.run(
            ["espeak-ng", "-v", lang, "-f", tf_path, "-w", wav_path],
            capture_output=True, timeout=600
        )
        if result.returncode != 0:
            raise RuntimeError(
                f"espeak-ng failed: {result.stderr.decode(errors='replace')[-400:]}"
            )

        if need_wav:
            ffmpeg = subprocess.run(
                ["ffmpeg", "-y", "-i", wav_path, output_path],
                capture_output=True, timeout=300
            )
            if ffmpeg.returncode != 0:
                raise RuntimeError(
                    f"ffmpeg TTS encode failed: {ffmpeg.stderr.decode(errors='replace')[-400:]}"
                )
    finally:
        try:
            os.unlink(tf_path)
        except OSError:
            pass
        if need_wav:
            try:
                os.unlink(wav_path)
            except OSError:
                pass


def convert(urls, target_format, options, config):
    """
    Text-to-Speech converter.
    Accepts ebook / document / PDF / TXT input.
    target_format: mp3 | wav | ogg | flac  (default: mp3)
    options:
        tts_format: mp3 | wav | ogg | flac
        tts_lang: BCP-47 language code for espeak-ng (default: en)
    """
    try:
        upload_dir = config.get("UPLOAD_DIR", "static/uploads")
        opts = options if isinstance(options, dict) else {}

        fmt = (opts.get("tts_format") or target_format or "mp3").lower().strip()
        if fmt not in AUDIO_FORMATS:
            fmt = "mp3"
        lang = (opts.get("tts_lang") or "en").strip()

        results = []
        errors = []

        for url_entry in urls:
            file_path = _parse_url_entry(url_entry)
            if not file_path or file_path == "empty":
                continue

            try:
                full_path = os.path.join(upload_dir, file_path)
                if not os.path.exists(full_path):
                    full_path = file_path
                if not os.path.exists(full_path):
                    errors.append(f"File not found: {os.path.basename(file_path)}")
                    continue

                source_name = os.path.splitext(os.path.basename(file_path))[0]
                text = _extract_text(full_path)
                if not text.strip():
                    errors.append(
                        f"No text could be extracted from {os.path.basename(file_path)}"
                    )
                    continue

                output_folder = uuid.uuid4().hex
                output_dir = os.path.join(upload_dir, output_folder)
                os.makedirs(output_dir, exist_ok=True)
                output_path = os.path.join(output_dir, f"{source_name}.{fmt}")

                _speak(text, output_path, lang=lang)
                results.append(output_path)

            except Exception as e:
                errors.append(
                    f"TTS failed for {os.path.basename(file_path)}: {e}"
                )

        if not results and errors:
            return {"error": True, "message": "; ".join(errors)}
        if not results:
            return {"error": True, "message": "No files were provided."}

        return {
            "error": False,
            "results": results,
            "output_path": results[0],
            "errors": errors if errors else None,
        }

    except Exception as e:
        return {"error": True, "message": f"TTS conversion failed: {e}"}
