from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger

_flask_app = None

scheduler = BackgroundScheduler(
    # misfire_grace_time généreux : par défaut (1s), un job planifié à la même
    # seconde que d'autres (ex: plusieurs "0 3 * * *") peut être traité trop
    # tard par l'ordonnanceur et est alors sauté silencieusement, sans trace.
    job_defaults={"coalesce": True, "max_instances": 1, "misfire_grace_time": 3600},
    timezone="UTC",
)


def init_scheduler(flask_app):
    global _flask_app
    _flask_app = flask_app
    # Nettoyage immédiat au démarrage : tout run "running" en DB est forcément
    # un vestige d'un process précédent (app redémarrée pendant un backup).
    with flask_app.app_context():
        from db import db, Run
        import datetime as _dt
        stale = Run.query.filter_by(status="running").all()
        now = _dt.datetime.utcnow()
        for run in stale:
            run.status = "error"
            run.log_text = (run.log_text or "") + "\n[interrompu] Run interrompu par un redémarrage de l'application."
            run.finished_at = now
        if stale:
            db.session.commit()
    if not scheduler.running:
        scheduler.start()
    # Filet de sécurité : marque en erreur tout run resté bloqué > 6h
    scheduler.add_job(
        func=_cleanup_stuck_runs,
        trigger="interval",
        hours=1,
        id="cleanup_stuck_runs",
        replace_existing=True,
    )


def _cleanup_stuck_runs():
    import datetime as _dt
    with _flask_app.app_context():
        from db import db, Run
        cutoff = _dt.datetime.utcnow() - _dt.timedelta(hours=6)
        stuck = Run.query.filter(
            Run.status == "running",
            Run.started_at < cutoff,
        ).all()
        now = _dt.datetime.utcnow()
        for run in stuck:
            duration = now - run.started_at
            hours = int(duration.total_seconds() // 3600)
            minutes = int((duration.total_seconds() % 3600) // 60)
            run.status = "error"
            run.finished_at = now

            if run.archive_name:
                # L'archive a été créée localement ; le timeout est survenu pendant le transfert
                detail = (
                    f" Archive locale présente ({run.archive_name})."
                    " Le processus de transfert était probablement encore en cours."
                )
            else:
                # Le backup lui-même n'a pas terminé dans les 6h
                detail = " Aucune archive locale détectée : la création du backup n'a pas abouti dans les délais."

            run.log_text = (run.log_text or "") + (
                f"\n\n[timeout] Run bloqué depuis {hours}h{minutes:02d} — "
                f"marqué en erreur par le nettoyage automatique.{detail}"
            )
        if stuck:
            db.session.commit()


def _execute_job(job_id):
    with _flask_app.app_context():
        from jobs.ynh_backup import execute_job
        execute_job(job_id)


def schedule_job(job):
    import logging
    job_key = f"job_{job.id}"
    if not job.cron_expr:
        return  # job manuel uniquement, pas de planification APScheduler
    try:
        trigger = CronTrigger.from_crontab(job.cron_expr)
    except Exception:
        logging.warning(f"Job #{job.id} « {job.name} » : expression cron invalide « {job.cron_expr} » — job non planifié.")
        return
    if scheduler.get_job(job_key):
        scheduler.reschedule_job(job_key, trigger=trigger)
    else:
        scheduler.add_job(
            func=_execute_job,
            trigger=trigger,
            id=job_key,
            kwargs={"job_id": job.id},
            replace_existing=True,
        )


def remove_job(job_id):
    job_key = f"job_{job_id}"
    if scheduler.get_job(job_key):
        scheduler.remove_job(job_key)


def get_next_run(job_id):
    job_key = f"job_{job_id}"
    apsjob = scheduler.get_job(job_key)
    if apsjob and apsjob.next_run_time:
        return apsjob.next_run_time
    return None
