#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Petit serveur local pour l'éditeur de panneaux TIVALA.

Il sert les fichiers du dossier et ajoute trois routes :

    GET  /api/ping        -> vérifie que l'écriture est disponible
    GET  /api/pictos      -> liste les images du dossier pictos/
    GET  /api/lignes      -> contenu de lignes.json
    POST /api/lignes      -> réécrit lignes.json (sauvegarde .bak)
    GET  /api/panneaux    -> contenu de panneaux.json
    POST /api/panneaux    -> réécrit panneaux.json (sauvegarde .bak)

Usage :
    python3 serveur.py            # http://localhost:8787
    python3 serveur.py 9000       # autre port
"""

import http.server
import json
import os
import shutil
import socket
import sys
import threading
import urllib.parse
import webbrowser

RACINE = os.path.dirname(os.path.abspath(__file__))
FICHIERS = {"lignes": "lignes.json", "panneaux": "panneaux.json"}
EXT_IMAGES = (".png", ".jpg", ".jpeg", ".webp", ".gif")
TAILLE_MAX = 4 * 1024 * 1024  # garde-fou sur les POST


def gabarit_vide(nom):
    return {"version": 1, nom: []}


class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=RACINE, **kw)

    # -- utilitaires ------------------------------------------------------
    def repondre_json(self, code, obj):
        corps = json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(corps)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(corps)

    def chemin(self, nom):
        return os.path.join(RACINE, FICHIERS[nom])

    def lire(self, nom):
        try:
            with open(self.chemin(nom), "r", encoding="utf-8") as f:
                return json.load(f)
        except FileNotFoundError:
            return gabarit_vide(nom)
        except json.JSONDecodeError as e:
            return {"version": 1, nom: [], "erreur": "JSON invalide : %s" % e}

    def ecrire(self, nom, data):
        cible = self.chemin(nom)
        if os.path.exists(cible):
            shutil.copyfile(cible, cible + ".bak")
        temporaire = cible + ".tmp"
        with open(temporaire, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
            f.write("\n")
        os.replace(temporaire, cible)

    def end_headers(self):
        # Pas de cache : on édite les fichiers en direct.
        if self.path.endswith((".js", ".css", ".html", ".json")):
            self.send_header("Cache-Control", "no-store")
        super().end_headers()

    # -- routes -----------------------------------------------------------
    def do_GET(self):
        route = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"

        if route == "/api/ping":
            return self.repondre_json(200, {"ok": True, "ecriture": os.access(RACINE, os.W_OK)})

        if route == "/api/pictos":
            dossier = os.path.join(RACINE, "pictos")
            noms = []
            if os.path.isdir(dossier):
                noms = sorted(
                    f for f in os.listdir(dossier)
                    if f.lower().endswith(EXT_IMAGES) and not f.startswith(".")
                )
            return self.repondre_json(200, {"pictos": noms})

        if route.startswith("/api/"):
            nom = route[len("/api/"):]
            if nom in FICHIERS:
                return self.repondre_json(200, self.lire(nom))
            return self.repondre_json(404, {"erreur": "route inconnue"})

        if route == "/":
            self.path = "/index.html"
        return super().do_GET()

    def do_POST(self):
        route = urllib.parse.urlparse(self.path).path.rstrip("/")
        nom = route[len("/api/"):] if route.startswith("/api/") else None
        if nom not in FICHIERS:
            return self.repondre_json(404, {"erreur": "route inconnue"})

        try:
            taille = int(self.headers.get("Content-Length") or 0)
        except ValueError:
            taille = 0
        if taille <= 0 or taille > TAILLE_MAX:
            return self.repondre_json(413, {"erreur": "corps de requête invalide"})

        try:
            data = json.loads(self.rfile.read(taille).decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            return self.repondre_json(400, {"erreur": "JSON invalide : %s" % e})

        if not isinstance(data, dict) or nom not in data:
            return self.repondre_json(400, {"erreur": "clé « %s » attendue" % nom})

        try:
            self.ecrire(nom, data)
        except OSError as e:
            return self.repondre_json(500, {"erreur": "écriture impossible : %s" % e})

        print("  → %s mis à jour (%d entrées)" % (FICHIERS[nom], len(data[nom])))
        return self.repondre_json(200, {"ok": True, "fichier": FICHIERS[nom]})

    def log_message(self, fmt, *args):
        if "/api/" in (args[0] if args else ""):
            super().log_message(fmt, *args)


def port_libre(depart):
    for p in range(depart, depart + 20):
        with socket.socket() as s:
            try:
                s.bind(("127.0.0.1", p))
                return p
            except OSError:
                continue
    raise SystemExit("Aucun port libre entre %d et %d" % (depart, depart + 20))


def main():
    depart = int(sys.argv[1]) if len(sys.argv) > 1 else 8787
    port = port_libre(depart)
    url = "http://localhost:%d/" % port

    serveur = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
    print("TIVALA — éditeur de panneaux")
    print("  dossier : %s" % RACINE)
    print("  ouvre   : %s" % url)
    print("  arrêt   : Ctrl+C")
    threading.Timer(0.6, lambda: webbrowser.open(url)).start()
    try:
        serveur.serve_forever()
    except KeyboardInterrupt:
        print("\nArrêt du serveur.")
    finally:
        serveur.server_close()


if __name__ == "__main__":
    main()
