"""
Appli Flask : formulaire de saisie -> génération de la Fiche Mission TPZ (.docx)
à partir du template Instadrone.

Lancement local :
    pip install -r requirements.txt
    python app.py
puis ouvrir http://127.0.0.1:5000
"""
import json
import os
import socket
import sys
import threading
import uuid
import webbrowser
from datetime import datetime

from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for

from docx_engine import build_docx
import insee_density
import missions_store
import pdf_export

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PILOTS_PATH = os.path.join(BASE_DIR, "data", "pilots.json")
GENERATED_DIR = os.path.join(BASE_DIR, "generated")
UPLOADS_DIR = os.path.join(BASE_DIR, "uploads")
os.makedirs(GENERATED_DIR, exist_ok=True)
os.makedirs(UPLOADS_DIR, exist_ok=True)

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024  # 50 Mo (KML + images annexes)

MITIGATION_OPTIONS = ["M1(A) - Low", "M1(C) - Low", "M2 - Medium"]
GRC_CATEGORY_OPTIONS = [
    "Zone contrôlée au sol",
    "< 5 ppl/km² (iGRC 2)",
    "< 50 ppl/km² (iGRC 3)",
    "< 500 ppl/km² (iGRC 4)",
    "< 5000 ppl/km² (iGRC 5)",
]
DENSITE_5KM_OPTIONS = ["< 500 ppl/km²", "< 5000 ppl/km²"]
RASSEMBLEMENT_OPTIONS = ["Non", "< 40 000"]
N_PARCELLES_MAX = 5
N_PILOTES_MAX = 4


def load_pilots():
    with open(PILOTS_PATH, encoding="utf-8") as f:
        return json.load(f)


def save_pilots(pilots):
    with open(PILOTS_PATH, "w", encoding="utf-8") as f:
        json.dump(pilots, f, ensure_ascii=False, indent=2)


@app.route("/")
def index():
    pilots = load_pilots()
    return render_template(
        "form.html",
        pilots=pilots,
        mitigation_options=MITIGATION_OPTIONS,
        grc_category_options=GRC_CATEGORY_OPTIONS,
        densite_5km_options=DENSITE_5KM_OPTIONS,
        rassemblement_options=RASSEMBLEMENT_OPTIONS,
        n_parcelles_max=N_PARCELLES_MAX,
        n_pilotes_max=N_PILOTES_MAX,
        insee_label=insee_density.INSEE_DATASET_LABEL,
        insee_url=insee_density.INSEE_DATASET_URL,
        today=datetime.now().strftime("%Y-%m-%d"),
    )


@app.route("/api/pilots", methods=["GET"])
def api_pilots_list():
    """Liste des pilotes (utilisé par la modale, sans jamais quitter le
    formulaire principal — évite toute perte de saisie en cours)."""
    return jsonify(load_pilots())


@app.route("/api/pilots", methods=["POST"])
def api_pilots_upsert():
    """Ajoute un pilote, ou met à jour son téléphone si le nom existe déjà."""
    payload = request.get_json(silent=True) or request.form
    nom = (payload.get("nom") or "").strip()
    telephone = (payload.get("telephone") or "").strip()
    if not nom:
        return jsonify({"error": "Le nom est obligatoire."}), 400
    pilots = load_pilots()
    existing = next((p for p in pilots if p["nom"].lower() == nom.lower()), None)
    if existing:
        existing["telephone"] = telephone
    else:
        pilots.append({"nom": nom, "telephone": telephone})
    pilots.sort(key=lambda p: p["nom"])
    save_pilots(pilots)
    return jsonify(pilots)


@app.route("/api/pilots/<path:nom>", methods=["DELETE"])
def api_pilots_delete(nom):
    pilots = load_pilots()
    remaining = [p for p in pilots if p["nom"].lower() != nom.strip().lower()]
    save_pilots(remaining)
    return jsonify(remaining)


@app.route("/api/insee/status")
def api_insee_status():
    return jsonify(insee_density.get_status())


@app.route("/api/insee/prepare", methods=["POST"])
def api_insee_prepare():
    """Lance (si pas déjà en cours) le téléchargement + l'indexation des
    données INSEE dans un thread d'arrière-plan, et répond immédiatement.
    Le front-end suit la progression via /api/insee/status (polling)."""
    status = insee_density.get_status()
    if status["state"] in ("downloading", "converting"):
        return jsonify(status)
    thread = threading.Thread(target=insee_density.prepare_insee_data, kwargs={"force": False}, daemon=True)
    thread.start()
    return jsonify({"state": "downloading", "message": "Préparation démarrée.", "progress": 0})


@app.route("/api/kml-density", methods=["POST"])
def api_kml_density():
    """Reçoit un fichier KML, calcule la densité de population MAXIMALE dans
    l'empreinte du buffer/tampon GRC via la grille INSEE (section B), renvoie
    la valeur + catégorie suggérée."""
    file = request.files.get("kml")
    if not file:
        return jsonify({"error": "Aucun fichier KML reçu"}), 400
    path = os.path.join(UPLOADS_DIR, f"{uuid.uuid4().hex}_{file.filename}")
    file.save(path)
    try:
        result = insee_density.compute_density_from_kml(path)
        return jsonify(result)
    except insee_density.InseeDataNotReady as e:
        return jsonify({"error": str(e), "not_ready": True, "status": e.status}), 503
    except insee_density.InseeDataUnavailable as e:
        return jsonify({"error": str(e), "unavailable": True}), 503
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/parcelle-density-5km", methods=["POST"])
def api_parcelle_density_5km():
    """Reçoit un fichier KML + une référence de parcelle optionnelle, calcule
    la densité MOYENNE sur un cercle de 5 km de rayon centré sur le centre du
    buffer de cette parcelle (section D — confinement)."""
    file = request.files.get("kml")
    if not file:
        return jsonify({"error": "Aucun fichier KML reçu"}), 400
    parcelle_ref = request.form.get("parcelle_ref", "").strip()
    path = os.path.join(UPLOADS_DIR, f"{uuid.uuid4().hex}_{file.filename}")
    file.save(path)
    try:
        result = insee_density.compute_avg_density_5km(path, parcelle_ref or None)
        return jsonify(result)
    except insee_density.InseeDataNotReady as e:
        return jsonify({"error": str(e), "not_ready": True, "status": e.status}), 503
    except insee_density.InseeDataUnavailable as e:
        return jsonify({"error": str(e), "unavailable": True}), 503
    except Exception as e:
        return jsonify({"error": str(e)}), 500


def _parse_form_snapshot(f):
    """Construit le snapshot structuré de la fiche à partir du formulaire.
    Ce snapshot sert à la fois à la persistance (rouvrir/modifier) et de base
    pour la génération du docx."""
    parcelles = []
    for i in range(1, N_PARCELLES_MAX + 1):
        nom = f.get(f"parcelle_nom_{i}", "").strip()
        commune = f.get(f"parcelle_commune_{i}", "").strip()
        ref = f.get(f"parcelle_ref_{i}", "").strip()
        if not (nom or commune or ref):
            continue
        parcelles.append({
            "nom": nom or f"Parcelle {i}",
            "commune": commune,
            "ref_cadastrale": ref,
            "mitigations": f.getlist(f"mitigation_{i}"),
            "densite_5km": f.get(f"densite_5km_{i}", "").strip(),
        })
    if not parcelles:
        parcelles = [{"nom": "Parcelle 1", "commune": "", "ref_cadastrale": "",
                      "mitigations": [], "densite_5km": ""}]

    pilotes = []
    for i in range(1, N_PILOTES_MAX + 1):
        nom = f.get(f"pilote_{i}", "").strip()
        if nom and nom != "/":
            pilotes.append(nom)

    return {
        "ref_dossier": f.get("ref_dossier", ""),
        "fiche_mission_n": f.get("fiche_mission_n", ""),
        "type_mission": f.get("type_mission", ""),
        "dates_vols": f.get("dates_vols", ""),
        "mode_vol": f.get("mode_vol", "VLOS"),
        "pilotes": pilotes,
        "parcelles": parcelles,
        "kml_noms": f.get("kml_noms", ""),
        "kml_conforme_manex": f.get("kml_conforme_manex") == "oui",
        "densite_grc_categorie": f.get("densite_grc_categorie", GRC_CATEGORY_OPTIONS[1]),
        "remarques_sol": f.get("remarques_sol", ""),
        "hauteur_vol_max": f.get("hauteur_vol_max", "").strip() or "40 m AGL",
        "arc_final": f.get("arc_final", "").strip() or "ARC-B",
        "proximite_aerodrome": f.get("proximite_aerodrome") == "oui",
        "nom_aerodrome": f.get("nom_aerodrome", ""),
        "operation_ctr": f.get("operation_ctr") == "oui",
        "nom_ctr": f.get("nom_ctr", ""),
        "accords_joints": f.get("accords_joints") == "oui",
        "remarques_air": f.get("remarques_air", ""),
        "densite_5km_categorie": f.get("densite_5km_categorie", DENSITE_5KM_OPTIONS[0]),
        "rassemblement_1km": f.get("rassemblement_1km", RASSEMBLEMENT_OPTIONS[0]),
        # fichiers d'annexes stockés côté serveur (remplis plus bas) :
        # annexe 1 = liste (plusieurs cartes possibles), annexe 2 = fichier unique
        "annexe1_filenames": [],
        "annexe2_filename": "",
    }


def _snapshot_to_docx_data(snapshot, mission_id):
    """Dérive le dict attendu par build_docx (téléphones résolus via l'annuaire,
    chemins d'images résolus depuis le dossier de la fiche)."""
    pilots_dir = {p["nom"]: p["telephone"] for p in load_pilots()}
    pilotes = [{"nom": n, "telephone": pilots_dir.get(n, "")} for n in snapshot["pilotes"]]

    mdir = missions_store.mission_dir(mission_id)
    annexe1_paths = [os.path.join(mdir, n) for n in snapshot.get("annexe1_filenames", []) if n]
    annexe2_path = os.path.join(mdir, snapshot["annexe2_filename"]) if snapshot.get("annexe2_filename") else None

    return {
        "ref_dossier": snapshot["ref_dossier"],
        "fiche_mission_n": snapshot["fiche_mission_n"],
        "type_mission": snapshot["type_mission"],
        "parcelles": [{"nom": p["nom"], "commune": p["commune"], "ref_cadastrale": p["ref_cadastrale"]}
                      for p in snapshot["parcelles"]],
        "dates_vols": snapshot["dates_vols"],
        "pilotes": pilotes,
        "mode_vol": snapshot["mode_vol"],
        "kml_noms": snapshot["kml_noms"],
        "kml_conforme_manex": snapshot["kml_conforme_manex"],
        "densite_grc_categorie": snapshot["densite_grc_categorie"],
        "mitigations_par_parcelle": [p.get("mitigations", []) for p in snapshot["parcelles"]],
        "remarques_sol": snapshot["remarques_sol"],
        "hauteur_vol_max": snapshot["hauteur_vol_max"],
        "arc_final": snapshot["arc_final"],
        "proximite_aerodrome": snapshot["proximite_aerodrome"],
        "nom_aerodrome": snapshot["nom_aerodrome"],
        "operation_ctr": snapshot["operation_ctr"],
        "nom_ctr": snapshot["nom_ctr"],
        "accords_joints": snapshot["accords_joints"],
        "remarques_air": snapshot["remarques_air"],
        "densite_5km_par_parcelle": [p.get("densite_5km", "") for p in snapshot["parcelles"]],
        "densite_5km_categorie": snapshot["densite_5km_categorie"],
        "rassemblement_1km": snapshot["rassemblement_1km"],
        "annexe1_image_paths": annexe1_paths,
        "annexe2_image_path": annexe2_path,
        "insee_footer_note": insee_density.INSEE_FOOTER_NOTE,
    }


def _save_annexe_file_single(mission_id, field_name, stored_name):
    """Annexe à image unique (annexe 2) : upload -> stockage, sinon réutilise l'existant."""
    up = request.files.get(field_name)
    if up and up.filename:
        ext = os.path.splitext(up.filename)[1] or ".png"
        fname = f"{field_name}{ext}"
        up.save(os.path.join(missions_store.mission_dir(mission_id), fname))
        return fname
    return stored_name or ""


def _save_annexe_files_multi(mission_id, field_name, stored_names):
    """Annexe à images multiples (annexe 1) : si de nouveaux fichiers sont
    téléversés, ils remplacent l'ensemble ; sinon on réutilise les fichiers
    déjà stockés."""
    ups = [u for u in request.files.getlist(field_name) if u and u.filename]
    if not ups:
        return list(stored_names or [])
    mdir = missions_store.mission_dir(mission_id)
    # nettoie les anciens fichiers de cette annexe
    for old in (stored_names or []):
        try:
            os.remove(os.path.join(mdir, old))
        except OSError:
            pass
    names = []
    for idx, up in enumerate(ups, start=1):
        ext = os.path.splitext(up.filename)[1] or ".png"
        fname = f"{field_name}_{idx}{ext}"
        up.save(os.path.join(mdir, fname))
        names.append(fname)
    return names


def _base_out_name(snapshot):
    name = f"Fiche_Mission_TPZ_{snapshot['ref_dossier'] or 'sans_ref'}_{snapshot['fiche_mission_n'] or ''}"
    return name.replace("#", "").replace(" ", "_")


@app.route("/generate", methods=["POST"])
def generate():
    """Enregistre la fiche (toujours), puis selon `output_format` :
    - 'save' : ne renvoie rien à télécharger (JSON de confirmation) ;
    - 'docx' : renvoie le document Word ;
    - 'pdf'  : renvoie le document converti en PDF."""
    f = request.form
    output_format = f.get("output_format", "docx")
    mission_id = f.get("mission_id") or None

    previous = missions_store.get_mission(mission_id) if mission_id else None
    prev_data = previous["data"] if previous else {}

    snapshot = _parse_form_snapshot(f)
    mission_id = missions_store.save_mission(snapshot, mission_id)

    # fichiers d'annexes
    snapshot["annexe1_filenames"] = _save_annexe_files_multi(
        mission_id, "annexe1_image", prev_data.get("annexe1_filenames"))
    snapshot["annexe2_filename"] = _save_annexe_file_single(
        mission_id, "annexe2_image", prev_data.get("annexe2_filename"))
    missions_store.save_mission(snapshot, mission_id)

    if output_format == "save":
        return jsonify({"ok": True, "mission_id": mission_id,
                        "message": "Fiche enregistrée."})

    data = _snapshot_to_docx_data(snapshot, mission_id)
    base = _base_out_name(snapshot)
    docx_path = os.path.join(GENERATED_DIR, base + ".docx")
    build_docx(data, docx_path)

    if output_format == "pdf":
        pdf_path = os.path.join(GENERATED_DIR, base + ".pdf")
        try:
            pdf_export.docx_to_pdf(docx_path, pdf_path)
        except pdf_export.PdfExportUnavailable as e:
            return jsonify({"error": str(e), "pdf_unavailable": True}), 503
        resp = send_file(pdf_path, as_attachment=True, download_name=base + ".pdf")
    else:
        resp = send_file(docx_path, as_attachment=True, download_name=base + ".docx")

    resp.headers["X-Mission-Id"] = str(mission_id)
    resp.headers["Access-Control-Expose-Headers"] = "X-Mission-Id, Content-Disposition"
    return resp


@app.route("/api/missions", methods=["GET"])
def api_missions_list():
    return jsonify(missions_store.list_missions())


@app.route("/api/missions/<int:mission_id>", methods=["GET"])
def api_mission_get(mission_id):
    m = missions_store.get_mission(mission_id)
    if not m:
        return jsonify({"error": "Fiche introuvable"}), 404
    return jsonify({"id": m["id"], "updated_at": m["updated_at"], "data": m["data"]})


@app.route("/api/missions/<int:mission_id>", methods=["DELETE"])
def api_mission_delete(mission_id):
    missions_store.delete_mission(mission_id)
    return jsonify({"ok": True})


@app.route("/api/missions/find")
def api_mission_find():
    """Garde-fou anti-doublon : renvoie la fiche existante ayant la même réf.
    dossier et le même numéro, le cas échéant."""
    m = missions_store.find_by_ref_num(request.args.get("ref", ""),
                                        request.args.get("num", ""))
    return jsonify(m or {})


def _port_in_use(host, port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(0.5)
        return s.connect_ex((host, port)) == 0


def _lan_ip():
    """Meilleure estimation de l'adresse IP du poste sur le réseau local."""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.connect(("8.8.8.8", 80))  # pas d'envoi réel, juste pour connaître l'IP locale
            return s.getsockname()[0]
    except Exception:
        return "127.0.0.1"


if __name__ == "__main__":
    # Affichage immédiat des messages (sinon, lancé depuis un panneau non-TTY
    # comme "Code Runner" de VS Code, tout est mis en tampon et n'apparaît
    # jamais tant que le serveur tourne).
    try:
        sys.stdout.reconfigure(line_buffering=True)
        sys.stderr.reconfigure(line_buffering=True)
    except Exception:
        pass

    # FICHE_HOST :
    #   127.0.0.1 (défaut) = usage solo sur ce poste uniquement ;
    #   0.0.0.0             = instance partagée, accessible par les autres postes
    #                         du réseau local (voir lancer_serveur_partage.bat).
    HOST = os.environ.get("FICHE_HOST", "127.0.0.1")
    PORT = int(os.environ.get("FICHE_PORT", "5000"))
    shared = HOST not in ("127.0.0.1", "localhost")

    # Le contrôle "port déjà utilisé" se fait sur l'interface locale.
    if _port_in_use("127.0.0.1", PORT):
        print(f"\n[!] Le port {PORT} est déjà utilisé : une instance tourne probablement déjà.\n"
              f"    -> Ouvrez http://127.0.0.1:{PORT} dans votre navigateur, fermez l'ancienne\n"
              f"       fenêtre, ou utilisez le lanceur qui libère le port automatiquement.\n")
        sys.exit(1)

    local_url = f"http://127.0.0.1:{PORT}"
    if shared:
        lan_url = f"http://{_lan_ip()}:{PORT}"
        host_url = f"http://{socket.gethostname()}:{PORT}"
        print("\n" + "=" * 66)
        print("  FICHE MISSION TPZ — instance partagée démarrée")
        print("  Sur CE poste                        : " + local_url)
        print("  Depuis les autres postes (par IP)   : " + lan_url)
        print("  Depuis les autres postes (par nom)  : " + host_url)
        print("  -> Partagez de préférence l'adresse PAR NOM : elle ne change")
        print("     pas si l'IP du poste évolue (WiFi 2.4/5 GHz, redémarrage).")
        print("  Le poste hôte doit rester allumé pendant l'utilisation.")
        print("=" * 66 + "\n")
    else:
        print(f"\nApplication démarrée : {local_url}\n(CTRL+C pour arrêter)\n")

    # Ouvre le navigateur local (dans les deux modes : confirmation visuelle
    # que le serveur tourne, même en mode partagé).
    threading.Timer(1.3, lambda: webbrowser.open(local_url)).start()

    # Serveur : waitress (robuste, adapté à plusieurs utilisateurs) si disponible,
    # sinon le serveur de développement Flask en repli.
    try:
        from waitress import serve
        serve(app, host=HOST, port=PORT, threads=8)
    except ImportError:
        app.run(debug=False, use_reloader=False, threaded=True, host=HOST, port=PORT)
