"""오케스트레이션 — 주간 스케줄 fetch + fetcher heartbeat."""
from __future__ import annotations

import asyncio
import datetime as dt
import json
import logging
from zoneinfo import ZoneInfo

logger = logging.getLogger(__name__)
KST = ZoneInfo("Asia/Seoul")
HEARTBEAT_KEY = "worker:naver-fetch:heartbeat"


class FetchState:
    """fetch_loop가 갱신, heartbeat_loop가 읽는 공유 상태."""
    def __init__(self):
        self.state = "idle"                 # idle | fetching
        self.last_fetch_at: int | None = None
        self.listings_pushed = 0
        self.errors = 0


def _parse_hhmm(s: str) -> dt.time:
    hh, mm = s.split(":")
    return dt.time(int(hh), int(mm))


def in_daytime(now: dt.datetime, start: str, end: str) -> bool:
    t = now.timetz().replace(tzinfo=None)
    return _parse_hhmm(start) <= t < _parse_hhmm(end)


def _epoch_now() -> int:
    return int(dt.datetime.now(dt.timezone.utc).timestamp())


def _iso_utc_now() -> str:
    return dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


async def run_cycle(nas, naver, state, settings) -> None:
    state.state = "fetching"
    try:
        await naver.acquire_token()
    except Exception:
        logger.exception("naver 토큰 획득 실패")
        state.errors += 1
        state.state = "idle"
        return
    try:
        tg = await nas.get_targets()
    except Exception:
        logger.exception("targets 조회 실패")
        state.errors += 1
        state.state = "idle"
        return

    dongs = tg.get("dongs", [])
    page_limit = int(tg.get("page_limit", 1))
    batches: list[dict] = []
    for d in dongs:
        cortar = d.get("cortar_no")
        dong = d.get("dong")
        articles: list[dict] = []
        try:
            for page in range(1, page_limit + 1):
                resp = await naver.fetch_articles(cortar, page)
                articles += resp.get("articleList", [])
                if not resp.get("isMoreData"):
                    break
        except Exception:
            logger.exception("naver fetch 실패 dong=%s", dong)
            state.errors += 1
        batches.append({"dong": dong, "articles": articles})

    fetched_at = _iso_utc_now()
    try:
        r = await nas.post_ingest(fetched_at, batches)
        logger.info("ingest received=%s new=%s matched=%s",
                    r.get("received"), r.get("new"), r.get("matched"))
    except Exception:
        logger.exception("listings-ingest 실패")
        state.errors += 1
        state.state = "idle"
        return

    state.listings_pushed += sum(len(b["articles"]) for b in batches)
    state.last_fetch_at = _epoch_now()
    state.state = "idle"


async def fetch_loop(nas, naver, state, settings) -> None:
    interval = dt.timedelta(hours=settings.fetch_interval_hours)
    last_run: dt.datetime | None = None
    logger.info("naver-fetch loop 시작 interval=%.1fh 주간창 %s-%s",
                settings.fetch_interval_hours, settings.daytime_start, settings.daytime_end)
    while True:
        try:
            now = dt.datetime.now(KST)
            if in_daytime(now, settings.daytime_start, settings.daytime_end) and (
                    last_run is None or (now - last_run) >= interval):
                await run_cycle(nas, naver, state, settings)
                last_run = now
        except asyncio.CancelledError:
            logger.info("fetch_loop cancelled")
            raise
        except Exception:
            logger.exception("fetch_loop iteration 실패")
        await asyncio.sleep(300)


def build_heartbeat(state) -> str:
    return json.dumps({
        "name": "naver-fetch", "kind": "fetcher", "state": state.state,
        "ts": _epoch_now(), "last_fetch_at": state.last_fetch_at,
        "listings_pushed": state.listings_pushed, "errors": state.errors,
    })


async def heartbeat_loop(redis, state, interval: int = 15, ttl: int = 45) -> None:
    logger.info("naver-fetch heartbeat 시작 ttl=%ds", ttl)
    while True:
        try:
            await redis.set(HEARTBEAT_KEY, build_heartbeat(state), ex=ttl)
        except asyncio.CancelledError:
            raise
        except Exception:
            logger.exception("heartbeat 발신 실패")
        await asyncio.sleep(interval)
