#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Le worker. Lancé par cron toutes les minutes :

    * * * * * cd ~/plateforme && ./venv/bin/python worker.py >> worker.log 2>&1

À chaque passage il prend un projet, enchaîne des tours d'API tant qu'il lui
reste du temps, puis s'arrête de lui-même avant la limite de l'hébergeur.
Aucun processus ne vit longtemps ; une interruption ne coûte qu'un tour.
"""

import fcntl
import os
import sys
import time
import traceback

import config
import claude
import livraison
import projets


VERROU = os.path.join(config.RACINE, ".worker.lock")

# Échecs consécutifs au-delà desquels on abandonne le projet. Laisse passer
# les indisponibilités passagères de l'API sans boucler indéfiniment.
MAX_ECHECS = 6


def _trace(message):
    print("[%s] %s" % (time.strftime("%Y-%m-%d %H:%M:%S"), message), flush=True)


def _prendre_verrou():
    """Un seul worker à la fois. Le cron peut se déclencher pendant qu'un
    passage précédent tourne encore : celui-ci ressort immédiatement."""
    descripteur = open(VERROU, "w")
    try:
        fcntl.flock(descripteur, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except OSError:
        descripteur.close()
        return None
    descripteur.write(str(os.getpid()))
    descripteur.flush()
    return descripteur


def _clore(projet_id, resume):
    anomalies = livraison.verifier(projet_id)
    if anomalies:
        for anomalie in anomalies[:40]:
            projets.journaliser(projet_id, "anomalie", anomalie)
        _trace("%s : %d anomalie(s) relevée(s)" % (projet_id, len(anomalies)))
    try:
        archive = livraison.archiver(projet_id)
        projets.journaliser(projet_id, "archive",
                            "%d Ko" % (os.path.getsize(archive) // 1024))
    except OSError as e:
        projets.journaliser(projet_id, "erreur", "archive impossible : %s" % e)
    projets.changer_etat(projet_id, projets.TERMINE, resume=resume)


def traiter(projet, echeance):
    projet_id = projet["id"]
    if projet["etat"] == projets.EN_ATTENTE:
        projets.changer_etat(projet_id, projets.EN_COURS)
        _trace("%s : démarrage" % projet_id)

    while time.time() < echeance:
        try:
            suite, detail = claude.un_tour(projet_id)
        except Exception as e:
            echecs = projets.compter_echec(projet_id)
            message = "%s : %s" % (type(e).__name__, e)
            projets.journaliser(projet_id, "erreur", message[:1000])
            _trace("%s : échec %d/%d — %s" % (projet_id, echecs, MAX_ECHECS, message))
            if echecs >= MAX_ECHECS:
                projets.changer_etat(projet_id, projets.ECHEC, erreur=message[:1000])
            else:
                # On laisse la main : le passage suivant réessaiera, avec le
                # délai du cron comme temporisation naturelle.
                projets.changer_etat(projet_id, projets.EN_ATTENTE)
            return

        _trace("%s : %s" % (projet_id, detail))

        if suite == "termine":
            _clore(projet_id, detail)
            _trace("%s : terminé" % projet_id)
            return
        if suite == "echec":
            projets.changer_etat(projet_id, projets.ECHEC, erreur=detail)
            _trace("%s : abandon — %s" % (projet_id, detail))
            return

    _trace("%s : budget de temps épuisé, reprise au passage suivant" % projet_id)


def main():
    verrou = _prendre_verrou()
    if verrou is None:
        return 0

    try:
        projets.initialiser()
        projet = projets.prochain_en_attente()
        if not projet:
            return 0
        echeance = time.time() + config.BUDGET_SECONDES
        traiter(projet, echeance)
        return 0
    except Exception:
        traceback.print_exc()
        return 1
    finally:
        fcntl.flock(verrou, fcntl.LOCK_UN)
        verrou.close()


if __name__ == "__main__":
    sys.exit(main())
