"""
Unified calendar sync.
  1. FF live JSON (nfs.faireconomy.media) → this week + next week  (actual values, precise)
  2. FF HTML scraper → weeks 3..N ahead                            (upcoming forecasts)
  3. Fallback for weeks 3..N, only if step 2 got blocked (ForexFactory now 403s the
     scraper — likely Cloudflare): Trading Economics first (services.te_calendar), then
     FMP (services.fmp_calendar) — whichever has a configured key and actually returns
     events. NOTE: neither is actually free for this endpoint — TE's calendar needs
     their $149/mo base plan, FMP's free tier excludes economic_calendar (needs their
     $14.99/mo Starter). Kept wired for whichever the user ends up paying for; look for
     a cheaper source before recommending either.
Called by the background loop in main.py and manually via POST /api/eco/calendar-sync.
"""
import logging
from datetime import datetime, timezone
from typing import Any, Dict

logger = logging.getLogger(__name__)

_sync_status: Dict[str, Any] = {
    "running": False,
    "last_run": None,
    "last_result": None,
    "source": None,
    "next_run": None,
}


def calendar_sync(weeks_ahead: int = 8, force_scrape: bool = False) -> Dict[str, Any]:
    """
    Sync upcoming economic events from ForexFactory only:
    - FF live JSON covers this week + next week (with actuals as they publish) — cheap,
      runs every background-loop tick (calendar_refresh_h).
    - FF HTML scraper (via FlareSolverr) covers weeks 3..weeks_ahead for forecasts — each
      call launches a real headless-browser fetch per week, which is heavier and riskier
      to run often (more requests FF's Cloudflare sees from us = more chance of getting
      re-flagged), so it's throttled to its own interval (calendar_scrape_refresh_h,
      config, default once/day) independent of the live-JSON cadence. `force_scrape=True`
      (manual "Sync Now" in the UI) bypasses the throttle.
    """
    global _sync_status
    _sync_status["running"] = True
    _sync_status["last_run"] = datetime.now(timezone.utc).isoformat()

    result: Dict[str, Any] = {}
    source = "ff"

    try:
        from services.ff_calendar import sync_live, scrape_upcoming
        from services.database import get_config, set_config

        # Step 1 — live JSON (thisweek + nextweek) — picks up actuals in real time
        print("[calendar_sync] step 1/3 — FF live JSON (thisweek + nextweek)", flush=True)
        r_live = sync_live()
        result["live"] = r_live
        print(f"[calendar_sync] live result: {r_live}", flush=True)

        # Throttle: is the scraper (+ TE/FMP fallback) actually due?
        scrape_interval_h = float(get_config("calendar_scrape_refresh_h") or 24)
        last_scrape_iso = get_config("calendar_last_scrape_at")
        elapsed_h = None
        if last_scrape_iso:
            try:
                elapsed_h = (datetime.now(timezone.utc) - datetime.fromisoformat(last_scrape_iso)).total_seconds() / 3600
            except ValueError:
                elapsed_h = None
        scrape_due = force_scrape or elapsed_h is None or elapsed_h >= scrape_interval_h

        scrape_weeks = max(0, weeks_ahead - 2)
        if scrape_weeks == 0:
            result["scrape"] = {"skipped": True}
        elif not scrape_due:
            wait_h = round(scrape_interval_h - (elapsed_h or 0), 1)
            print(f"[calendar_sync] step 2/3 — scrape not due yet (next in ~{wait_h}h), skipping", flush=True)
            result["scrape"] = {"skipped": "not_due", "next_due_in_h": wait_h}
        else:
            # Step 2 — HTML scraper for weeks 3..N ahead (forecasts only, no actuals yet)
            print(f"[calendar_sync] step 2/3 — FF HTML scrape, {scrape_weeks} week(s) ahead", flush=True)
            r_scrape = scrape_upcoming(weeks_ahead=scrape_weeks)
            result["scrape"] = r_scrape
            print(f"[calendar_sync] scrape result: {r_scrape}", flush=True)
            set_config("calendar_last_scrape_at", datetime.now(timezone.utc).isoformat())

            # FF's scraper gets 403'd (Cloudflare) often enough that "weeks 3..N" can
            # silently come back empty — fall back to Trading Economics (free tier), then
            # FMP (needs their paid plan for this endpoint), whichever is configured.
            if not r_scrape.get("total_upserted"):
                print("[calendar_sync] step 3/3 — scrape got 0 events, checking TE/FMP fallback", flush=True)

                from services.te_calendar import check_te_key, fetch_upcoming as te_fetch_upcoming
                te_status = check_te_key()
                print(f"[calendar_sync] TE key configured: {te_status['configured']}", flush=True)
                if te_status["configured"]:
                    r_te = te_fetch_upcoming(weeks_ahead=weeks_ahead)
                    result["te_fallback"] = r_te
                    print(f"[calendar_sync] TE fallback result: {r_te}", flush=True)
                    if r_te.get("total_upserted"):
                        source = "ff+te"
                else:
                    result["te_fallback"] = {"skipped": "no_key"}

                if not result["te_fallback"].get("total_upserted"):
                    from services.fmp_calendar import check_fmp_key, fetch_upcoming as fmp_fetch_upcoming
                    fmp_status = check_fmp_key()
                    print(f"[calendar_sync] FMP key configured: {fmp_status['configured']}", flush=True)
                    if fmp_status["configured"]:
                        r_fmp = fmp_fetch_upcoming(weeks_ahead=weeks_ahead)
                        result["fmp_fallback"] = r_fmp
                        print(f"[calendar_sync] FMP fallback result: {r_fmp}", flush=True)
                        if r_fmp.get("total_upserted"):
                            source = "ff+fmp"
                    else:
                        result["fmp_fallback"] = {"skipped": "no_key"}
            else:
                print("[calendar_sync] step 3/3 — scrape got events, TE/FMP fallback not needed", flush=True)

    except Exception as e:
        logger.error(f"[calendar_sync] FF sync failed: {e}")
        print(f"[calendar_sync] EXCEPTION: {e!r}", flush=True)
        result = {"error": str(e)}
        source = "error"

    # Log forecast/actual changes to macro_series_log
    try:
        from services.database import get_conn
        from services.macro_series_log import scan_and_log_all
        _conn = get_conn()
        log_result = scan_and_log_all(_conn, source=source)
        _conn.close()
        result["_log"] = log_result
    except Exception as e:
        logger.warning(f"[calendar_sync] macro_series_log scan failed: {e}")

    _sync_status["running"] = False
    _sync_status["last_result"] = result
    _sync_status["source"] = source
    return result


def get_sync_status() -> Dict[str, Any]:
    return dict(_sync_status)


def set_next_run(ts: str):
    _sync_status["next_run"] = ts
