# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""
Bridge HTTP -> TPE Concert V3
=============================
Serveur HTTP local sur localhost:9876 qui expose POST /payment et GET /health
en reutilisant le TpeDriver de native-host/bridge_v3.py.

Usage :
    python bridge_server.py --tpe-ip 192.168.1.42
    TPE_IP=192.168.1.42 python bridge_server.py
    python bridge_server.py --tpe-ip 10.0.0.5 --listen 8888 --debug

Endpoints :
    POST /payment   body: {"amount": 12.50}
                    -> {"success": bool, "card_masked": str, "auth_code": str, "error": null|str}
    GET  /health    -> {"status": "ok", "tpe_connected": bool}
"""

import argparse
import json
import logging
import os
import sys
import threading
import time
import importlib.util
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn

# ─── Importer TpeDriver depuis native-host/bridge_v3.py ────────────────────
# Le dossier s'appelle "native-host" (tiret, pas tiret bas) donc on ne peut pas
# faire un import Python classique. On utilise importlib pour charger le module
# par son chemin absolu -- fonctionne sur Windows, Mac et Linux.
_HERE = os.path.dirname(os.path.abspath(__file__))
_BRIDGE_V3_PATH = os.path.join(_HERE, "native-host", "bridge_v3.py")

_spec = importlib.util.spec_from_file_location("bridge_v3", _BRIDGE_V3_PATH)
_bridge_v3 = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_bridge_v3)

TpeDriver = _bridge_v3.TpeDriver

# ─── Constantes ────────────────────────────────────────────────────────────
DEFAULT_TPE_IP = "192.168.1.42"
DEFAULT_TPE_PORT = 8888
DEFAULT_BRIDGE_PORT = 9876
DEFAULT_CAISSE_NUM = "01"

# ─── Logging ───────────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    stream=sys.stderr,
)
logger = logging.getLogger("bridge_server")


# ─── Helpers ───────────────────────────────────────────────────────────────
def extract_card_info(tags):
    """
    Extrait card_masked et auth_code des tags de reponse Concert V3.
    
    Tags d'interet (venant du TPE) :
    - AA : numero de carte masque (ex: "4970XXXXXX1234")
    - AC : numero d'autorisation (ex: "A12345")
    
    Retourne (card_masked: str, auth_code: str).
    """
    card_masked = tags.get("AA", "")
    auth_code = tags.get("AC", "")
    return card_masked, auth_code


# ─── Handler HTTP ──────────────────────────────────────────────────────────
class PaymentHandler(BaseHTTPRequestHandler):
    """
    Handler HTTP pour le bridge TPE.
    
    Attributs de classe (partages entre toutes les instances) :
    - tpe_driver : instance unique de TpeDriver
    - tpe_lock    : threading.Lock() pour eviter 2 paiements simultanes
    - tpe_ip / tpe_port / caisse_num : config TPE
    """
    
    # Attributs partages -- affectes dans main() avant de lancer le serveur
    tpe_driver = None     # instance TpeDriver
    tpe_lock = None       # threading.Lock()
    
    # Timeout socket > 120s pour supporter les paiements longs
    timeout = 130
    
    def log_message(self, fmt, *args):
        """Override : log Python au lieu de stderr brut."""
        logger.info(f"{self.client_address[0]} - {fmt % args}")
    
    # ── Helpers HTTP ──
    
    def _send_json(self, status_code, data):
        """Envoie une reponse JSON avec le code HTTP donne."""
        body = json.dumps(data, ensure_ascii=False).encode("utf-8")
        self.send_response(status_code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        # Pas de restriction CORS : l'extension Chrome fait fetch vers localhost
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)
    
    def do_OPTIONS(self):
        """CORS preflight pour les requetes cross-origin."""
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()
    
    # ── Routes ──
    
    def do_GET(self):
        if self.path == "/health":
            self._handle_health()
        else:
            self._send_json(404, {"error": "Not found"})
    
    def do_POST(self):
        if self.path == "/payment":
            self._handle_payment()
        elif self.path == "/cancel":
            self._handle_cancel()
        else:
            self._send_json(404, {"error": "Not found"})
    
    # ── GET /health ──
    
    def _handle_health(self):
        """GET /health -- statut du bridge et du TPE."""
        connected = False
        if self.tpe_driver is not None:
            connected = self.tpe_driver.connected
        self._send_json(200, {"status": "ok", "tpe_connected": connected})
    
    # ── POST /payment ──
    
    def _handle_payment(self):
        """
        POST /payment -- initie un paiement via le TPE.
        
        Body attendu : {"amount": <float>} -- montant en euros decimaux.
        Retourne      : {"success": bool, "card_masked": str, "auth_code": str, "error": null|str}
        """
        # Lire le body de la requete
        content_length = int(self.headers.get("Content-Length", 0))
        if content_length == 0:
            self._send_json(400, self._error("Empty body"))
            return
        
        raw = self.rfile.read(content_length)
        try:
            data = json.loads(raw)
        except json.JSONDecodeError as e:
            self._send_json(400, self._error(f"Invalid JSON: {e}"))
            return
        
        # Valider le champ amount
        amount = data.get("amount")
        if amount is None:
            self._send_json(400, self._error("Missing 'amount' field"))
            return
        
        try:
            amount = float(amount)
        except (TypeError, ValueError):
            self._send_json(400, self._error("Invalid amount"))
            return
        
        if amount <= 0:
            self._send_json(400, self._error("Amount must be positive"))
            return
        
        # Convertir en centimes entiers (arrondi standard)
        cents = int(round(amount * 100))
        
        # Verifier que le bridge est initialise
        if self.tpe_driver is None or self.tpe_lock is None:
            self._send_json(503, self._error("Server not ready"))
            return
        
        # Lock pour eviter 2 paiements simultanes (le TPE ne gere qu'une transaction a la fois)
        acquired = self.tpe_lock.acquire(blocking=False)
        if not acquired:
            self._send_json(503, self._error("TPE is busy -- another payment in progress"))
            return
        
        try:
            driver = self.tpe_driver
            
            # Verifier que le TPE est connecte (le thread daemon de main()
            # s'occupe de la reconnexion automatique en arriere-plan)
            if not driver.connected:
                self._send_json(503, self._error("TPE not reachable"))
                return
            
            # Lancer le paiement (bloquant -- peut prendre jusqu'a 120s)
            logger.info(f"[PAY] Paiement {amount:.2f}€ ({cents} centimes)")
            result = driver.start_payment(cents)
            
            if result.get("ok"):
                tags = result.get("tags", {})
                card_masked, auth_code = extract_card_info(tags)
                logger.info(f"   [OK] SUCCÈS -- carte {card_masked} auth {auth_code}")
                self._send_json(200, {
                    "success": True,
                    "card_masked": card_masked,
                    "auth_code": auth_code,
                    "error": None,
                })
            else:
                error_msg = result.get("error", "Unknown TPE error")
                # Recuperer quand meme les tags partiels (peuvent contenir AA/AC meme en erreur)
                tags = result.get("tags", {})
                card_masked, auth_code = extract_card_info(tags)
                logger.warning(f"   [FAIL] ECHEC: {error_msg}")
                self._send_json(200, {
                    "success": False,
                    "card_masked": card_masked,
                    "auth_code": auth_code,
                    "error": error_msg,
                })
        except Exception as e:
            logger.error(f"   [FAIL] Exception non geree: {e}", exc_info=True)
            self._send_json(500, self._error(f"Internal error: {e}"))
        finally:
            self.tpe_lock.release()
    
    # ── POST /cancel ──
    
    def _handle_cancel(self):
        """POST /cancel -- annule la transaction en cours."""
        if self.tpe_driver is None:
            self._send_json(503, self._error("Server not ready"))
            return
        
        try:
            self.tpe_driver.cancel()
            logger.info("[CANCEL] Transaction annulee")
            self._send_json(200, {"success": True, "error": None})
        except Exception as e:
            logger.error(f"Erreur cancel: {e}")
            self._send_json(500, self._error(str(e)))
    
    # ── Helpers metier ──
    
    @staticmethod
    def _error(message):
        """Construit la structure de reponse standard pour une erreur."""
        return {
            "success": False,
            "card_masked": "",
            "auth_code": "",
            "error": message,
        }


# ─── Serveur HTTP multithread ──────────────────────────────────────────────
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Serveur HTTP multithread qui cree un thread par requete."""
    # Permettre au processus principal de quitter meme si des threads survivent
    daemon_threads = True


# ─── CLI ───────────────────────────────────────────────────────────────────
def parse_args():
    """Parse les arguments CLI avec fallback aux variables d'environnement."""
    parser = argparse.ArgumentParser(
        description="Bridge HTTP -> TPE Concert V3",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Exemples :
  python bridge_server.py --tpe-ip 192.168.1.42
  TPE_IP=10.0.0.5 python bridge_server.py --debug
  python bridge_server.py --tpe-ip 192.168.1.42 --listen 8888
        """,
    )
    parser.add_argument(
        "--tpe-ip",
        default=os.environ.get("TPE_IP", DEFAULT_TPE_IP),
        help=f"IP du TPE Ingenico (defaut: $TPE_IP ou {DEFAULT_TPE_IP})",
    )
    parser.add_argument(
        "--tpe-port",
        type=int,
        default=int(os.environ.get("TPE_PORT", str(DEFAULT_TPE_PORT))),
        help=f"Port TCP du TPE (defaut: $TPE_PORT ou {DEFAULT_TPE_PORT})",
    )
    parser.add_argument(
        "--listen",
        type=int,
        default=int(os.environ.get("BRIDGE_PORT", str(DEFAULT_BRIDGE_PORT))),
        help=f"Port d'ecoute HTTP sur localhost (defaut: {DEFAULT_BRIDGE_PORT})",
    )
    parser.add_argument(
        "--caisse",
        default=os.environ.get("TPE_CAISSE", DEFAULT_CAISSE_NUM),
        help=f"Numero de caisse pour le TPE (defaut: $TPE_CAISSE ou {DEFAULT_CAISSE_NUM})",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Active les logs DEBUG (affichage detaille)",
    )
    return parser.parse_args()


def main():
    """Point d'entree principal."""
    args = parse_args()
    
    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        logger.debug("Mode DEBUG active")
    
    logger.info("=" * 60)
    logger.info("[BRIDGE] Bridge HTTP -> TPE Concert V3")
    logger.info(f"   TPE       : {args.tpe_ip}:{args.tpe_port}")
    logger.info(f"   Caisse    : {args.caisse}")
    logger.info(f"   Ecoute    : http://localhost:{args.listen}")
    logger.info("=" * 60)
    
    # ── Initialiser le TpeDriver (connexion en thread daemon) ──
    # Le TpeDriver a un mecanisme de retry avec sleep integre (1s, 2s, 5s, …).
    # Pour ne pas bloquer le demarrage du serveur HTTP, on lance la connexion
    # dans un thread daemon. Le serveur demarre immediatement et le endpoint
    # /payment tentera une reconnexion si le TPE n'est pas encore connecte.
    driver = TpeDriver(args.tpe_ip, args.tpe_port, args.caisse)
    
    def connect_tpe():
        """Tente la connexion au TPE dans un thread daemon."""
        driver.start()
        if driver.connected:
            logger.info("   [OK] TPE connecte")
        else:
            logger.warning(
                "   [WARN]  TPE non connecte -- les paiements tenteront une reconnexion automatique."
            )
    
    conn_thread = threading.Thread(target=connect_tpe, daemon=True, name="tpe-connect")
    conn_thread.start()
    
    # Attendre brievement que le premier attempt demarre (pas bloque longtemps)
    time.sleep(0.2)
    
    # ── Configurer le handler avec les attributs partages ──
    PaymentHandler.tpe_driver = driver
    PaymentHandler.tpe_lock = threading.Lock()
    
    # ── Lancer le serveur ──
    server = ThreadedHTTPServer(("127.0.0.1", args.listen), PaymentHandler)
    
    logger.info(f"   [OK] Bridge pret -- http://localhost:{args.listen}")
    logger.info(f"      POST /payment  -- initier un paiement")
    logger.info(f"      GET  /health   -- statut du TPE")
    logger.info("      Ctrl+C pour arreter")
    logger.info("=" * 60)
    
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        logger.info("")
        logger.info("[STOP] Arret demande...")
    finally:
        driver.stop()
        server.server_close()
        logger.info("   [OK] Bridge arrete proprement.")


if __name__ == "__main__":
    main()
