#!/usr/bin/env python3
"""
JuriSign : premiers pas avec l'API, en un seul script.

Enchaîne les quatre appels d'une intégration : vérifier le jeton, déposer un
PDF, créer une demande de signature, l'envoyer. Seule dépendance : requests
(pip install requests).

AVANT DE LANCER
  1. Récupérez votre jeton :
       - centre d'essai : dans la console de https://test-api.jurisign.fr,
         bouton « Générer un jeton pour mon code » ;
       - production : POST https://www.jurisign.fr/api/v1/auth/token avec
         l'e-mail et le mot de passe de votre compte jurisign.fr.
     Le jeton s'envoie tel quel, préfixe « sandbox_ » compris.
  2. Placez-le dans la variable d'environnement JURISIGN_TOKEN :
       bash       : export JURISIGN_TOKEN='sandbox_12|abc...'
       PowerShell : $env:JURISIGN_TOKEN = 'sandbox_12|abc...'

LANCER
  python jurisign_premiers_pas.py vous@exemple.fr               (PDF d'essai généré)
  python jurisign_premiers_pas.py vous@exemple.fr contrat.pdf

Le signataire est l'adresse indiquée. Le script affiche à la fin le lien de
signature : ouvrez-le pour signer vous-même. En bac à sable, aucun e-mail
ne part et le code de vérification est toujours 123456 ; le PDF signé et sa
preuve sont marqués « document de test ».

Le script vise le centre d'essai. Pour la production :
  JURISIGN_API=https://www.jurisign.fr/api/v1
"""

import argparse
import os
import sys
import uuid

import requests

API = os.environ.get("JURISIGN_API", "https://test-api.jurisign.fr/api/v1").rstrip("/")
JETON = os.environ.get("JURISIGN_TOKEN", "").strip()

# Les causes les plus fréquentes, par code de réponse.
AIDE = {
    401: "Jeton absent, mal copié ou remplacé. Générez-en un nouveau dans la console "
         "et envoyez-le tel quel, préfixe sandbox_ compris. Attention : un jeton du centre "
         "d'essai ne fonctionne que sur test-api.jurisign.fr.",
    403: "Le jeton n'a pas le droit nécessaire, ou l'option n'est pas incluse dans votre offre.",
    404: "Identifiant inconnu : un jeton de bac à sable ne voit que les données de bac à sable.",
    405: "Méthode HTTP incorrecte pour cette adresse (par exemple GET au lieu de POST).",
    422: "Données refusées : le détail champ par champ est ci-dessus.",
    429: "Trop d'appels rapprochés : patientez une minute.",
}


def appel(methode, chemin, **options):
    """Un appel à l'API ; en cas d'erreur, affiche la réponse et la cause probable."""
    entetes = {"Authorization": f"Bearer {JETON}", "Accept": "application/json"}
    entetes.update(options.pop("headers", {}))
    reponse = requests.request(methode, API + chemin, headers=entetes, timeout=60, **options)

    if reponse.status_code >= 400:
        print(f"\n  ÉCHEC {methode} {chemin} : HTTP {reponse.status_code}")
        try:
            corps = reponse.json()
            print("  " + str(corps.get("message", "")))
            for champ, erreurs in (corps.get("errors") or {}).items():
                print(f"    - {champ} : {' '.join(erreurs)}")
        except ValueError:
            print("  " + reponse.text[:300])
        if reponse.status_code in AIDE:
            print("  Cause probable : " + AIDE[reponse.status_code])
        sys.exit(1)

    return reponse.json()


def pdf_d_essai():
    """Un PDF d'une page, fabriqué ici pour n'exiger aucun fichier."""
    texte = b"Contrat d'essai JuriSign - la signature se place dans le cadre ci-dessous."
    flux = b"BT /F1 12 Tf 60 780 Td (" + texte + b") Tj ET"
    objets = [
        b"<< /Type /Catalog /Pages 2 0 R >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] "
        b"/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
        b"<< /Length %d >>\nstream\n" % len(flux) + flux + b"\nendstream",
    ]
    pdf = bytearray(b"%PDF-1.4\n")
    positions = []
    for numero, objet in enumerate(objets, start=1):
        positions.append(len(pdf))
        pdf += b"%d 0 obj\n" % numero + objet + b"\nendobj\n"
    debut_xref = len(pdf)
    pdf += b"xref\n0 %d\n0000000000 65535 f \n" % (len(objets) + 1)
    for position in positions:
        pdf += b"%010d 00000 n \n" % position
    pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (len(objets) + 1, debut_xref)
    return bytes(pdf)


def main():
    parser = argparse.ArgumentParser(description="Premiers pas avec l'API JuriSign.")
    parser.add_argument("email", help="adresse du signataire (la vôtre, pour signer vous-même)")
    parser.add_argument("pdf", nargs="?", help="PDF à faire signer (facultatif)")
    parser.add_argument("--prenom", default="Claire")
    parser.add_argument("--nom", default="Essai")
    args = parser.parse_args()

    if not JETON:
        sys.exit("JURISIGN_TOKEN est vide : voir « AVANT DE LANCER » en tête de ce fichier.")

    bac_a_sable = JETON.startswith("sandbox_")
    print(f"API : {API}  ({'bac à sable' if bac_a_sable else 'PRODUCTION'})")

    # 1. Le jeton est-il accepté ?
    compte = appel("GET", "/account/credits")["data"]
    print(f"1. Jeton accepté. Offre : {compte['plan']}, crédits restants : {compte['credits_remaining']}")

    # 2. Le document.
    if args.pdf:
        with open(args.pdf, "rb") as f:
            fichier = (os.path.basename(args.pdf), f.read(), "application/pdf")
    else:
        fichier = ("contrat-essai.pdf", pdf_d_essai(), "application/pdf")
    document = appel("POST", "/documents", files={"file": fichier}, data={"title": "Premier essai API"})["data"]
    print(f"2. Document déposé : {document['id']} ({document['page_count']} page(s))")

    # 3. La demande de signature. La clé d'idempotence évite un doublon si
    #    l'appel est rejoué après une coupure réseau.
    demande = appel("POST", "/sign-requests", headers={"Idempotency-Key": str(uuid.uuid4())}, json={
        "document_id": document["id"],
        "subject": "Premier essai API",
        "signing_order_type": 0,
        "expiry_hours": 72,
        "signers": [{"prenom": args.prenom, "nom": args.nom, "email": args.email, "otp_channel": "email"}],
        # Positions en pourcentage de la page : à 10 % du bord gauche, à 75 % du haut.
        "zones": [{"signer_index": 0, "page": 1, "x": 10, "y": 75, "width": 35, "height": 10}],
    })["data"]
    print(f"3. Demande créée : {demande['id']} (statut {demande['status']})")

    if not bac_a_sable:
        if input("   PRODUCTION : envoyer pour de vrai ? (oui/non) ").strip().lower() != "oui":
            sys.exit("   Demande laissée en brouillon.")

    # 4. L'envoi.
    envoi = appel("POST", f"/sign-requests/{demande['id']}/send")["data"]
    print(f"4. Demande envoyée (statut {envoi['status']}).")

    # Le détail d'une demande donne, pour chaque signataire, son lien de signature.
    detail = appel("GET", f"/sign-requests/{demande['id']}")["data"]
    for signataire in detail.get("signers", []):
        if signataire.get("signing_link"):
            print(f"   Lien de signature de {signataire['email']} :\n   {signataire['signing_link']}")

    print(f"\nPour suivre la demande : GET {API}/sign-requests/{demande['id']}")


if __name__ == "__main__":
    main()
