"""
Transcripteur Audio - application web locale
==============================================
Transcrit des fichiers audio (.mp3 / .wav), sans limite de taille,
en texte (.txt) ou document Word (.docx), grâce au modèle Whisper
exécuté entièrement en local (hors-ligne, gratuit).

Lancement :
    pip install -r requirements.txt
    python app.py
Puis ouvrir http://localhost:5000 dans un navigateur.
"""

import os
import uuid
import threading
import traceback
from datetime import datetime

from flask import Flask, render_template, request, jsonify, send_file, abort
from docx import Document
from docx.shared import Pt

# --------------------------------------------------------------------------
# Configuration
# --------------------------------------------------------------------------

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
OUTPUT_DIR = os.path.join(BASE_DIR, "jobs")
ALLOWED_EXTENSIONS = {".mp3", ".wav"}

os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(OUTPUT_DIR, exist_ok=True)

app = Flask(__name__)
# Aucune limite de taille de fichier envoyé (None = illimité).
app.config["MAX_CONTENT_LENGTH"] = None

# Stockage en mémoire de l'état des tâches de transcription.
# Pour un usage mono-utilisateur local, un simple dict suffit.
JOBS = {}
JOBS_LOCK = threading.Lock()

# Cache des modèles Whisper déjà chargés (évite de recharger à chaque fois).
_MODEL_CACHE = {}
_MODEL_CACHE_LOCK = threading.Lock()

LANGUAGE_CHOICES = {
    "auto": None,
    "fr": "fr",
    "en": "en",
    "es": "es",
    "de": "de",
    "it": "it",
    "pt": "pt",
    "nl": "nl",
    "ar": "ar",
}


def get_model(model_size):
    """Charge (ou récupère depuis le cache) un modèle Whisper local."""
    with _MODEL_CACHE_LOCK:
        if model_size not in _MODEL_CACHE:
            from faster_whisper import WhisperModel
            # compute_type int8 = rapide et léger sur CPU. Utilise le GPU
            # automatiquement s'il est disponible (device="auto").
            _MODEL_CACHE[model_size] = WhisperModel(
                model_size, device="auto", compute_type="int8"
            )
        return _MODEL_CACHE[model_size]


def get_audio_duration(filepath):
    """Retourne la durée du fichier audio en secondes, ou None si inconnue."""
    try:
        import mutagen
        f = mutagen.File(filepath)
        if f is not None and f.info is not None:
            return float(f.info.length)
    except Exception:
        pass
    return None


def format_timestamp(seconds):
    seconds = max(0, int(seconds))
    h = seconds // 3600
    m = (seconds % 3600) // 60
    s = seconds % 60
    if h:
        return f"{h:02d}:{m:02d}:{s:02d}"
    return f"{m:02d}:{s:02d}"


SENTENCE_END_CHARS = (".", "!", "?", "…", "。", "！", "？")


def build_sentences(segments):
    """Reconstruit des phrases (avec un horodatage précis en début de phrase)
    à partir des horodatages mot-par-mot fournis par Whisper.

    Sans cela, un horodatage n'apparaîtrait qu'une fois par "segment" détecté
    par la détection d'activité vocale (VAD) de Whisper - un segment pouvant
    couvrir plusieurs phrases, voire plusieurs dizaines de secondes de parole
    continue. Ici, on regroupe les mots par phrase (sur la ponctuation finale)
    pour obtenir un horodatage réellement placé avant chaque phrase.
    """
    sentences = []
    current_words = []
    current_start = None

    def flush():
        if not current_words:
            return
        text = "".join(current_words)
        text = " ".join(text.split())  # normalise les espaces
        if text:
            sentences.append({"start": current_start, "text": text})

    for seg in segments:
        words = seg.get("words") or []
        if not words:
            # Pas d'horodatage mot-par-mot disponible pour ce segment (ne
            # devrait arriver qu'en secours) : on garde le segment entier.
            if current_words:
                flush()
                current_words = []
                current_start = None
            sentences.append({"start": seg["start"], "text": seg["text"]})
            continue

        for w in words:
            if current_start is None:
                current_start = w["start"]
            current_words.append(w["text"])
            if w["text"].strip().endswith(SENTENCE_END_CHARS):
                flush()
                current_words = []
                current_start = None

    flush()
    return sentences


def write_txt(job_id, filename, segments, include_timestamps):
    path = os.path.join(OUTPUT_DIR, f"{job_id}.txt")
    with open(path, "w", encoding="utf-8") as fh:
        for seg in segments:
            if include_timestamps:
                fh.write(f"[{format_timestamp(seg['start'])}] {seg['text']}\n")
            else:
                fh.write(f"{seg['text']}\n")
    return path


def write_docx(job_id, filename, segments, include_timestamps, detected_language):
    path = os.path.join(OUTPUT_DIR, f"{job_id}.docx")
    doc = Document()

    title = doc.add_heading("Transcription audio", level=1)

    meta = doc.add_paragraph()
    meta_run = meta.add_run(
        f"Fichier source : {filename}\n"
        f"Date de transcription : {datetime.now().strftime('%d/%m/%Y %H:%M')}\n"
        f"Langue détectée : {detected_language or 'inconnue'}"
    )
    meta_run.italic = True
    doc.add_paragraph("")

    for seg in segments:
        p = doc.add_paragraph()
        if include_timestamps:
            ts_run = p.add_run(f"[{format_timestamp(seg['start'])}] ")
            ts_run.bold = True
            ts_run.font.size = Pt(10)
        p.add_run(seg["text"])

    doc.save(path)
    return path


def run_transcription(job_id, filepath, model_size, language_key, include_timestamps):
    try:
        with JOBS_LOCK:
            JOBS[job_id]["status"] = "loading_model"

        model = get_model(model_size)
        duration = get_audio_duration(filepath)

        with JOBS_LOCK:
            JOBS[job_id]["status"] = "transcribing"
            JOBS[job_id]["duration"] = duration

        language = LANGUAGE_CHOICES.get(language_key)
        segments_gen, info = model.transcribe(
            filepath,
            language=language,
            beam_size=5,
            vad_filter=True,
            word_timestamps=True,
        )

        segments = []
        for seg in segments_gen:
            words = [
                {"start": w.start, "end": w.end, "text": w.word}
                for w in (seg.words or [])
            ]
            segments.append(
                {
                    "start": seg.start,
                    "end": seg.end,
                    "text": seg.text.strip(),
                    "words": words,
                }
            )
            with JOBS_LOCK:
                if duration and duration > 0:
                    JOBS[job_id]["progress"] = min(99, int(seg.end / duration * 100))
                JOBS[job_id]["partial_text"] = " ".join(s["text"] for s in segments[-3:])

        filename = JOBS[job_id]["filename"]
        detected_language = getattr(info, "language", None)

        # Phrases reconstruites mot-par-mot : un horodatage précis avant
        # chaque phrase plutôt qu'un seul par segment VAD (qui peut couvrir
        # plusieurs phrases).
        sentences = build_sentences(segments)

        txt_path = write_txt(job_id, filename, sentences, include_timestamps)
        docx_path = write_docx(
            job_id, filename, sentences, include_timestamps, detected_language
        )

        with JOBS_LOCK:
            JOBS[job_id].update(
                {
                    "status": "done",
                    "progress": 100,
                    "txt_path": txt_path,
                    "docx_path": docx_path,
                    "language_detected": detected_language,
                    "full_text": "\n".join(s["text"] for s in segments),
                }
            )
    except Exception as exc:
        traceback.print_exc()
        with JOBS_LOCK:
            JOBS[job_id]["status"] = "error"
            JOBS[job_id]["error"] = str(exc)
    finally:
        # Nettoyage du fichier audio uploadé (on garde seulement les résultats).
        try:
            os.remove(filepath)
        except OSError:
            pass


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/upload", methods=["POST"])
def upload():
    file = request.files.get("audio")
    if not file or file.filename == "":
        return jsonify({"error": "Aucun fichier reçu."}), 400

    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        return jsonify({"error": "Formats acceptés : .mp3 et .wav uniquement."}), 400

    model_size = request.form.get("model", "small")
    language_key = request.form.get("language", "auto")
    include_timestamps = request.form.get("timestamps") == "true"

    job_id = uuid.uuid4().hex
    filepath = os.path.join(UPLOAD_DIR, f"{job_id}{ext}")
    file.save(filepath)

    with JOBS_LOCK:
        JOBS[job_id] = {
            "status": "queued",
            "progress": 0,
            "filename": file.filename,
            "error": None,
        }

    thread = threading.Thread(
        target=run_transcription,
        args=(job_id, filepath, model_size, language_key, include_timestamps),
        daemon=True,
    )
    thread.start()

    return jsonify({"job_id": job_id})


@app.route("/status/<job_id>")
def status(job_id):
    with JOBS_LOCK:
        job = JOBS.get(job_id)
        if not job:
            return jsonify({"error": "Tâche introuvable."}), 404
        # On ne renvoie pas le texte complet à chaque poll pour rester léger.
        safe_job = {k: v for k, v in job.items() if k != "full_text"}
    return jsonify(safe_job)


DOWNLOAD_MIMETYPES = {
    "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "txt": "text/plain; charset=utf-8",
}


@app.route("/download/<job_id>/<fmt>")
def download(job_id, fmt):
    if fmt not in DOWNLOAD_MIMETYPES:
        print(f"[download] format inconnu demandé : {fmt!r}")
        return jsonify({"error": f"Format inconnu : {fmt}"}), 400

    with JOBS_LOCK:
        job = JOBS.get(job_id)

    if not job:
        print(f"[download] job introuvable : {job_id}")
        return jsonify({"error": "Tâche introuvable (le serveur a peut-être redémarré depuis la transcription)."}), 404

    if job.get("status") != "done":
        print(f"[download] job {job_id} pas encore terminé (status={job.get('status')})")
        return jsonify({"error": "La transcription n'est pas encore terminée."}), 409

    key = f"{fmt}_path"
    path = job.get(key)
    if not path or not os.path.exists(path):
        print(f"[download] fichier absent sur le disque pour {job_id}/{fmt} : {path!r}")
        return jsonify({"error": "Le fichier de résultat est introuvable sur le disque. Relancez la transcription."}), 404

    try:
        base_name = os.path.splitext(job["filename"])[0] or "transcription"
        ext = ".docx" if fmt == "docx" else ".txt"
        response = send_file(
            path,
            as_attachment=True,
            download_name=f"{base_name}{ext}",
            mimetype=DOWNLOAD_MIMETYPES[fmt],
            conditional=False,
        )
        # Empêche toute mise en cache par le navigateur qui pourrait renvoyer
        # une réponse vide (304) pour un clic répété.
        response.headers["Cache-Control"] = "no-store"
        print(f"[download] envoi réussi de {job_id}/{fmt} ({os.path.getsize(path)} octets)")
        return response
    except Exception as exc:
        traceback.print_exc()
        return jsonify({"error": f"Erreur lors de l'envoi du fichier : {exc}"}), 500


if __name__ == "__main__":
    # threaded=True pour pouvoir suivre la progression pendant la transcription.
    # Pour un usage prolongé/production, préférer un serveur WSGI (waitress, gunicorn).
    app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)
