"""APScheduler yöneticisi — veri toplama ve tweet dağıtımını 7/24 döndürür.

İşler:
- fixtures : her 30 dk  -> fikstür senkronu
- lineups  : her 5 dk   -> yaklaşan maçların ilk 11'i
- live     : her 25 sn  -> canlı olay tarama + kuyruğa alma
- dispatch : her 15 sn  -> kuyruktan tweet gönderme (ritim kurallı)

Veri toplama HER ZAMAN çalışır (panel canlı maçları göstersin diye);
tweet gönderimi yalnız posting_enabled açıkken gerçekleşir (dispatcher içinde kontrol).
"""
import logging
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor

from app.db.database import SessionLocal
from app.db import crud, models
from app.data_engine import sync
from app.scheduler import enqueue, dispatcher, maintenance, daily, budget

log = logging.getLogger("scheduler")

_scheduler: BackgroundScheduler | None = None
_last_status = {"live_matches": 0, "queue": 0, "last_tick": None}


def _safe(fn):
    """İş gövdesini koru: bir hata scheduler'ı durdurmasın."""
    def wrapper():
        db = SessionLocal()
        try:
            fn(db)
        except Exception as e:  # noqa: BLE001
            log.exception("İş hatası: %s", fn.__name__)
            try:
                # Yarım kalmış işi at: aksi halde aşağıdaki log commit'i
                # tamamlanmamış kayıtları kalıcı hale getirir.
                db.rollback()
                crud.add_log(db, f"İş hatası [{fn.__name__}]: {e}", level="error",
                             source="scheduler")
            except Exception:
                pass
        finally:
            db.close()
    return wrapper


def _job_fixtures(db):
    sync.sync_fixtures(db)


def _job_lineups(db):
    events = sync.poll_lineups(db)
    events += sync.poll_previews(db)
    if events:
        enqueue.enqueue_events(db, events)


def _job_news(db):
    """RSS tara ve tweet'lenecek haberi kuyruğa al."""
    from app.news_engine import curator, writer
    if crud.get_setting(db, "news_enabled", "true") != "true":
        return
    curator.ingest(db)

    # Saatlik haber tavanını aşma (zaman çizelgesi haberle dolmasın)
    cap = int(crud.get_setting(db, "news_per_hour", "2") or 2)
    from datetime import datetime, timedelta
    since = datetime.utcnow() - timedelta(hours=1)
    recent = (
        db.query(models.Post)
        .filter(models.Post.kind.in_(["news", "transfer"]),
                models.Post.created_at >= since)
        .count()
    )
    if recent >= cap:
        return

    for item in curator.pick_for_tweet(db, limit=cap - recent):
        text = writer.write_tweet(db, item)
        item.tweeted = True          # değerlendirildi (atlansa bile tekrar bakma)
        if text:
            db.add(models.Post(content=text, kind=item.category, status="queued"))
        else:
            item.skipped = True
        db.commit()


def _job_sources(db):
    """İzlenen hesapları (resmi yayıncı vb.) tara ve alıntı kuyruğa al."""
    from app.x_engine import sources
    from app.content_engine import quote_writer
    if crud.get_setting(db, "sources_enabled", "true") != "true":
        return
    sources.poll_sources(db)

    cap = int(crud.get_setting(db, "quotes_per_hour", "3") or 3)
    from datetime import datetime, timedelta
    since = datetime.utcnow() - timedelta(hours=1)
    recent = (db.query(models.Post)
              .filter(models.Post.kind == "quote",
                      models.Post.created_at >= since).count())
    if recent >= cap:
        return

    for item in sources.pick_for_quote(db, limit=cap - recent):
        comment = quote_writer.write_comment(db, item)
        item.used = True
        text, media_url = ("", "")
        if comment:
            match = db.get(models.Match, item.match_id) if item.match_id else None
            # Alıntı kartı DEĞİL: video -> metin+link, foto -> metin+görsel
            text, media_url = quote_writer.build_tweet(comment, item, match)
        if text:
            # Maçın thread'i açıksa video/görsel oraya yanıt olarak girer.
            # Aksi hâlde aynı maç için ikinci bir kök oluşur ve anlatım
            # ikiye bölünür (bkz. es zamanlı maç tasarımı).
            m = db.get(models.Match, item.match_id) if item.match_id else None
            db.add(models.Post(
                content=text, kind="quote", status="queued",
                match_id=item.match_id, media_source_url=media_url,
                is_reply=bool(m and m.thread_tweet_id),
            ))
        else:
            item.skipped = True
        db.commit()


def _job_imagegen(db):
    """Kuyruktaki görselsiz kartlara görsel üret (ht/ft/preview)."""
    from app.content_engine import imagegen
    imagegen.fill_queued_images(db)


def _job_budget(db):
    """Günün kapsam planını güncelle (tavan sabit, kapsam esner)."""
    if crud.get_setting(db, "budget_enabled", "true") == "true":
        budget.plan(db)


def _job_daily_morning(db):
    daily.morning_fixture(db)


def _job_daily_evening(db):
    daily.evening_results(db)


def _job_standings(db):
    daily.standings(db)


def _job_live(db):
    # Paylaşım kapalıyken 25 sn'lik canlı yoklamaya gerek yok: canlı olayları
    # tweetlemiyoruz. Dahası bu yüksek hacim (günde ~3500 istek) 19-20.08 gecesi
    # SofaScore'un sunucu IP'mizi 403 ile engellemesine yol açtı. Paylaşım
    # kapalıyken canlı yoklamayı durdur; blok yaşlanıp kalksın, fikstür/kadro
    # gibi düşük hacimli işler sürsün. Paylaşım açılınca otomatik geri gelir.
    from app.db import crud
    if crud.get_setting(db, "posting_enabled", "false") != "true":
        return
    events = sync.poll_live(db)
    if events:
        enqueue.enqueue_events(db, events)


def _job_dispatch(db):
    from datetime import datetime
    dispatcher.tick(db)
    _last_status["last_tick"] = datetime.utcnow().isoformat()


def _job_health(db):
    maintenance.health_check(db)
    maintenance.resume_after_daily_limit(db)


def _job_cleanup(db):
    maintenance.cleanup(db)


def start_scheduler():
    global _scheduler
    if _scheduler and _scheduler.running:
        return _scheduler
    _scheduler = BackgroundScheduler(
        executors={"default": ThreadPoolExecutor(4)},
        job_defaults={"coalesce": True, "max_instances": 1, "misfire_grace_time": 30},
        timezone="Europe/Istanbul",
    )
    _scheduler.add_job(_safe(_job_fixtures), "interval", minutes=30, id="fixtures",
                       next_run_time=_now_plus(5))
    _scheduler.add_job(_safe(_job_lineups), "interval", minutes=5, id="lineups")
    _scheduler.add_job(_safe(_job_live), "interval", seconds=25, id="live")
    _scheduler.add_job(_safe(_job_dispatch), "interval", seconds=15, id="dispatch")
    _scheduler.add_job(_safe(_job_health), "interval", hours=6, id="health",
                       next_run_time=_now_plus(120))
    _scheduler.add_job(_safe(_job_cleanup), "cron", hour=4, minute=30, id="cleanup")
    # Bütçe planı: 20 dk'da bir yeniden hesapla
    _scheduler.add_job(_safe(_job_budget), "interval", minutes=20, id="budget",
                       next_run_time=_now_plus(20))
    # Haber akışı: 20 dk'da bir tara
    _scheduler.add_job(_safe(_job_news), "interval", minutes=20, id="news",
                       next_run_time=_now_plus(30))
    # İzlenen hesaplar (yayıncı vb.): 3 dk'da bir — gol videoları tazeyken alıntılansın
    _scheduler.add_job(_safe(_job_sources), "interval", minutes=3, id="sources",
                       next_run_time=_now_plus(45))
    # Görsel üretimi: 45 sn'de bir kuyruğu yokla (tek seferde 1 görsel, ~80 sn)
    _scheduler.add_job(_safe(_job_imagegen), "interval", seconds=45, id="imagegen",
                       next_run_time=_now_plus(20))
    # Günlük planlı içerikler (TR saati)
    _scheduler.add_job(_safe(_job_daily_morning), "cron", hour=10, minute=0,
                       id="daily_morning")
    _scheduler.add_job(_safe(_job_daily_evening), "cron", hour=23, minute=30,
                       id="daily_evening")
    _scheduler.add_job(_safe(_job_standings), "cron", day_of_week="mon", hour=12,
                       minute=0, id="standings")
    _scheduler.start()
    log.info("Scheduler başladı")
    db = SessionLocal()
    try:
        crud.add_log(db, "Scheduler başladı (veri toplama + dağıtım aktif)",
                     source="scheduler")
    finally:
        db.close()
    return _scheduler


def _now_plus(seconds):
    """Scheduler'ın saat dilimine göre 'şimdi + n saniye'.

    Naif `datetime.now()` döndürülüyordu; APScheduler bunu kendi zaman
    dilimiyle (Europe/Istanbul) yorumluyor. Sunucu UTC-7 olduğu için
    verilen zaman 10 saat GEÇMİŞTE kalıyor ve ilk çalıştırma
    misfire_grace_time'ı aştığı için atlanabiliyordu.
    """
    from datetime import datetime, timedelta
    tz = _scheduler.timezone if _scheduler else None
    return datetime.now(tz) + timedelta(seconds=seconds)


def shutdown_scheduler():
    global _scheduler
    if _scheduler and _scheduler.running:
        _scheduler.shutdown(wait=False)
        log.info("Scheduler durdu")


def scheduler_running() -> bool:
    return bool(_scheduler and _scheduler.running)


# Panelde işleri anlaşılır göstermek için açıklamalar
JOB_INFO = {
    "live":          ("Canlı olay taraması", "Oynanan maçlarda gol/kart/devre gibi olayları yakalar ve kuyruğa alır."),
    "dispatch":      ("Tweet gönderimi", "Kuyruktan sıradaki tweeti insansı tempoyla gönderir."),
    "lineups":       ("İlk 11 & maç öncesi", "Başlamasına az kalan maçların kadrolarını ve tanıtımını hazırlar."),
    "fixtures":      ("Fikstür senkronu", "Açık liglerin gelecek ve son maçlarını günceller."),
    "budget":        ("Bütçe planı", "Günün maç yoğunluğuna göre lig kapsamlarını ayarlar."),
    "news":          ("Haber taraması", "6 RSS kaynağını tarar, uygun haberi tweete çevirir."),
    "sources":       ("İzlenen hesaplar", "Resmi yayıncı gibi hesapların yeni gönderilerini alıntılamak üzere toplar."),
    "imagegen":      ("Görsel üretimi", "Maç önü / devre arası / maç sonu kartlarını Wiro.ai ile üretir."),
    "health":        ("Hesap sağlık kontrolü", "X oturumlarının hâlâ geçerli olduğunu doğrular."),
    "cleanup":       ("Bakım / temizlik", "Eski log, maç ve tweet kayıtlarını budar."),
    "daily_morning": ("Sabah fikstür paylaşımı", "Günün maç programını thread olarak kuyruğa alır."),
    "daily_evening": ("Akşam sonuç paylaşımı", "Gün içinde biten maçların skor özetini hazırlar."),
    "standings":     ("Puan durumu", "Haftalık puan tablosunu paylaşır."),
}


def job_status() -> list[dict]:
    """Panelde göstermek için işlerin durumu."""
    if not _scheduler or not _scheduler.running:
        return []
    out = []
    for job in _scheduler.get_jobs():
        label, desc = JOB_INFO.get(job.id, (job.id, ""))
        out.append({
            "id": job.id,
            "label": label,
            "desc": desc,
            "trigger": str(job.trigger),
            "next_run": job.next_run_time,
        })
    out.sort(key=lambda j: (j["next_run"] is None, j["next_run"]))
    return out


def run_job_now(job_id: str) -> bool:
    """Bir işi hemen tetikle (panelden 'şimdi çalıştır')."""
    if not _scheduler or not _scheduler.running:
        return False
    job = _scheduler.get_job(job_id)
    if not job:
        return False
    from datetime import datetime as _dt
    job.modify(next_run_time=_dt.now(_scheduler.timezone))
    return True
