import httpx
import logging
from typing import Any, Dict, List, Optional

from .config import STOCK_URL, MUSIC_LAB_URL, INSTA_LAB_URL, REALESTATE_LAB_URL

logger = logging.getLogger(__name__)

_client = httpx.AsyncClient(timeout=30.0)

async def fetch_stock_news(limit: int = 10, category: str = None) -> List[Dict[str, Any]]:
    params = {"limit": limit}
    if category:
        params["category"] = category
    resp = await _client.get(f"{STOCK_URL}/api/stock/news", params=params)
    resp.raise_for_status()
    return resp.json()

async def fetch_stock_indices() -> Dict[str, Any]:
    resp = await _client.get(f"{STOCK_URL}/api/stock/indices")
    resp.raise_for_status()
    return resp.json()

async def summarize_stock_news(limit: int = 15) -> Dict[str, Any]:
    """stock의 AI 요약 엔드포인트 호출.
    반환: {"summary": str, "tokens": {...}, "model": str, "duration_ms": int, "article_count": int}
    """
    # stock 내부 Ollama 호출이 180s까지 가능하므로 여유있게 200s
    async with httpx.AsyncClient(timeout=200.0) as client:
        resp = await client.post(
            f"{STOCK_URL}/api/stock/news/summarize",
            json={"limit": limit},
        )
        resp.raise_for_status()
        return resp.json()


async def refresh_screener_snapshot() -> Dict[str, Any]:
    """stock의 KRX 일봉 스냅샷 갱신 (스크리너 실행 전 호출).

    네이버 금융 일괄 다운로드라 보통 30~120s, 여유있게 180s.
    """
    async with httpx.AsyncClient(timeout=180.0) as client:
        resp = await client.post(f"{STOCK_URL}/api/stock/screener/snapshot/refresh")
        resp.raise_for_status()
        return resp.json()


async def refresh_ai_news_sentiment() -> Dict[str, Any]:
    """stock의 AI 뉴스 sentiment 분석 트리거 (08:00 cron).

    네이버 100종목 스크래핑 + Claude Haiku 100콜 병렬 = 약 30-60초.
    여유있게 240s timeout.
    """
    async with httpx.AsyncClient(timeout=240.0) as client:
        resp = await client.post(
            f"{STOCK_URL}/api/stock/screener/snapshot/refresh-news-sentiment"
        )
        resp.raise_for_status()
        return resp.json()


async def run_stock_screener(mode: str = "auto") -> Dict[str, Any]:
    """stock의 스크리너 실행.

    반환 status:
      - 'skipped_holiday': 공휴일/주말 — telegram_payload 없음
      - 'success': telegram_payload 동봉
    엔진 자체는 수 초 내 끝나지만, 컨텍스트 로드+200종목 처리 여유 180s.
    """
    async with httpx.AsyncClient(timeout=180.0) as client:
        resp = await client.post(
            f"{STOCK_URL}/api/stock/screener/run",
            json={"mode": mode},
        )
        resp.raise_for_status()
        return resp.json()


async def scrape_stock_news() -> Dict[str, Any]:
    """stock의 수동 뉴스 스크랩 트리거 — DB에 최신 뉴스 저장.

    아침 브리핑 직전 호출하여 어제 데이터가 아닌 오늘 새벽 뉴스를 보장한다.
    네이버 금융 단일 요청이라 보통 수 초 내 완료, 여유있게 60s.
    """
    async with httpx.AsyncClient(timeout=60.0) as client:
        resp = await client.post(f"{STOCK_URL}/api/stock/scrap")
        resp.raise_for_status()
        return resp.json()


async def stock_holdings_run() -> Dict[str, Any]:
    """보유종목 시그널 계산 트리거 (EOD, use_llm=True).

    stock BackgroundTask 등록 후 즉시 {ok, queued} 반환.
    실제 계산은 stock 컨테이너 백그라운드에서 진행 — 여유있게 120s.
    """
    async with httpx.AsyncClient(timeout=120.0) as client:
        resp = await client.post(
            f"{STOCK_URL}/api/stock/holdings/intel/run",
            params={"use_llm": True},
        )
        resp.raise_for_status()
        return resp.json()


async def stock_holdings_brief() -> Dict[str, Any]:
    """보유종목 최신 브리핑 payload 조회 (GET, 모듈 레벨 _client 사용)."""
    resp = await _client.get(f"{STOCK_URL}/api/stock/holdings/intel")
    resp.raise_for_status()
    return resp.json()


# --- stock watchlist (실시간 매매 알람) ---

async def watchlist_add(ticker: str) -> Dict[str, Any]:
    """stock의 관심종목 추가 (POST, 이미 존재하면 멱등하게 갱신)."""
    resp = await _client.post(f"{STOCK_URL}/api/stock/watchlist", json={"ticker": ticker})
    resp.raise_for_status()
    return resp.json()


async def watchlist_remove(ticker: str) -> Dict[str, Any]:
    """stock의 관심종목 삭제."""
    resp = await _client.delete(f"{STOCK_URL}/api/stock/watchlist/{ticker}")
    resp.raise_for_status()
    return resp.json()


async def watchlist_list() -> Dict[str, Any]:
    """stock의 관심종목 목록 조회 → {"watchlist": [...]}."""
    resp = await _client.get(f"{STOCK_URL}/api/stock/watchlist")
    resp.raise_for_status()
    return resp.json()


async def generate_music(payload: dict) -> Dict[str, Any]:
    resp = await _client.post(f"{MUSIC_LAB_URL}/api/music/generate", json=payload)
    resp.raise_for_status()
    return resp.json()

async def get_music_status(task_id: str) -> Dict[str, Any]:
    resp = await _client.get(f"{MUSIC_LAB_URL}/api/music/status/{task_id}")
    resp.raise_for_status()
    return resp.json()

async def get_music_credits() -> Dict[str, Any]:
    resp = await _client.get(f"{MUSIC_LAB_URL}/api/music/credits")
    resp.raise_for_status()
    return resp.json()


# --- insta-lab ---

async def insta_collect(categories: Optional[list] = None) -> Dict[str, Any]:
    """뉴스 수집 트리거 → task_id 반환."""
    payload = {"categories": categories} if categories else {}
    resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/news/collect", json=payload)
    resp.raise_for_status()
    return resp.json()


async def insta_extract(categories: Optional[list] = None) -> Dict[str, Any]:
    payload = {"categories": categories} if categories else {}
    resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/keywords/extract", json=payload)
    resp.raise_for_status()
    return resp.json()


async def insta_list_keywords(category: Optional[str] = None,
                              used: Optional[bool] = None) -> List[Dict[str, Any]]:
    params: Dict[str, Any] = {}
    if category:
        params["category"] = category
    if used is not None:
        params["used"] = "true" if used else "false"
    resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/keywords", params=params)
    resp.raise_for_status()
    return resp.json().get("items", [])


async def insta_get_keyword(keyword_id: int) -> Optional[Dict[str, Any]]:
    items = await insta_list_keywords()
    for it in items:
        if it["id"] == keyword_id:
            return it
    return None


async def insta_create_slate(keyword: str, category: str, keyword_id: Optional[int] = None) -> Dict[str, Any]:
    resp = await _client.post(
        f"{INSTA_LAB_URL}/api/insta/slates",
        json={"keyword": keyword, "category": category, "keyword_id": keyword_id},
    )
    resp.raise_for_status()
    return resp.json()


async def insta_task_status(task_id: str) -> Dict[str, Any]:
    resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/tasks/{task_id}")
    resp.raise_for_status()
    return resp.json()


async def insta_get_slate(slate_id: int) -> Dict[str, Any]:
    resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/slates/{slate_id}")
    resp.raise_for_status()
    return resp.json()


async def insta_get_asset_bytes(slate_id: int, page: int) -> bytes:
    """카드 PNG 바이트를 가져와 텔레그램 미디어 그룹에 첨부."""
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.get(f"{INSTA_LAB_URL}/api/insta/slates/{slate_id}/assets/{page}")
        resp.raise_for_status()
        return resp.content


async def insta_collect_trends(categories: Optional[list] = None) -> Dict[str, Any]:
    payload = {"categories": categories} if categories else {}
    resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/trends/collect", json=payload)
    resp.raise_for_status()
    return resp.json()


async def insta_list_trends(source: Optional[str] = None,
                            category: Optional[str] = None,
                            days: int = 1) -> List[Dict[str, Any]]:
    params: Dict[str, Any] = {"days": days}
    if source:
        params["source"] = source
    if category:
        params["category"] = category
    resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/trends", params=params)
    resp.raise_for_status()
    return resp.json().get("items", [])


async def insta_get_preferences() -> Dict[str, float]:
    resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/preferences")
    resp.raise_for_status()
    return {p["category"]: p["weight"] for p in resp.json().get("categories", [])}


async def insta_put_preferences(weights: Dict[str, float]) -> Dict[str, Any]:
    resp = await _client.put(
        f"{INSTA_LAB_URL}/api/insta/preferences",
        json={"categories": weights},
    )
    resp.raise_for_status()
    return resp.json()


async def insta_ranked(threshold: float = 0.6, limit: int = 20, dedup_window_days: int = 14) -> list:
    async with httpx.AsyncClient(timeout=120) as client:
        r = await client.get(
            f"{INSTA_LAB_URL}/api/insta/keywords/ranked",
            params={"threshold": threshold, "limit": limit, "dedup_window_days": dedup_window_days},
        )
        r.raise_for_status()
        return r.json()["items"]


async def insta_decision(slate_id: int, decision: str) -> dict:
    async with httpx.AsyncClient(timeout=30) as client:
        r = await client.post(
            f"{INSTA_LAB_URL}/api/insta/slates/{slate_id}/decision",
            json={"decision": decision},
        )
        r.raise_for_status()
        return r.json()


# --- realestate-lab ---

async def realestate_collect() -> Dict[str, Any]:
    """청약 공고 수동 수집 트리거"""
    async with httpx.AsyncClient(timeout=120.0) as client:
        resp = await client.post(f"{REALESTATE_LAB_URL}/api/realestate/collect")
        resp.raise_for_status()
        return resp.json()


async def realestate_matches(limit: int = 20) -> List[Dict[str, Any]]:
    """realestate-lab의 GET /api/realestate/matches 호출."""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(
            f"{REALESTATE_LAB_URL}/api/realestate/matches",
            params={"size": limit},
        )
        resp.raise_for_status()
        data = resp.json()
        return data.get("items", [])


async def realestate_dashboard() -> Dict[str, Any]:
    resp = await _client.get(f"{REALESTATE_LAB_URL}/api/realestate/dashboard")
    resp.raise_for_status()
    return resp.json()


async def realestate_mark_read(match_id: int) -> Dict[str, Any]:
    resp = await _client.patch(f"{REALESTATE_LAB_URL}/api/realestate/matches/{match_id}/read")
    resp.raise_for_status()
    return resp.json()


async def realestate_bookmark_toggle(announcement_id: int) -> Dict[str, Any]:
    """realestate-lab의 PATCH /api/realestate/announcements/{id}/bookmark 호출."""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.patch(
            f"{REALESTATE_LAB_URL}/api/realestate/announcements/{announcement_id}/bookmark"
        )
        resp.raise_for_status()
        return resp.json()


# --- lotto-backend ---

async def lotto_candidates(n: int = 20) -> Dict[str, Any]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/curator/candidates", params={"n": n})
    resp.raise_for_status()
    return resp.json()


async def lotto_context() -> Dict[str, Any]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/curator/context")
    resp.raise_for_status()
    return resp.json()


async def lotto_save_briefing(payload: dict) -> Dict[str, Any]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.post(f"{LOTTO_BACKEND_URL}/api/lotto/briefing", json=payload)
    resp.raise_for_status()
    return resp.json()


async def lotto_review_latest() -> Optional[Dict[str, Any]]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/review/latest")
    if resp.status_code == 404:
        return None
    resp.raise_for_status()
    return resp.json()


async def lotto_review_by_draw(draw_no: int) -> Optional[Dict[str, Any]]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/review/{draw_no}")
    if resp.status_code == 404:
        return None
    resp.raise_for_status()
    return resp.json()


async def lotto_reviews_history(limit: int = 10) -> List[Dict[str, Any]]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(
        f"{LOTTO_BACKEND_URL}/api/lotto/review/history",
        params={"limit": limit},
    )
    resp.raise_for_status()
    return resp.json().get("reviews", [])


# --- music-lab pipeline (YouTube publisher orchestration) ---

async def list_active_pipelines() -> list[dict]:
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline?status=active")
        resp.raise_for_status()
        return resp.json().get("pipelines", [])


async def list_failed_pipelines() -> list[dict]:
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline?status=failed")
        resp.raise_for_status()
        data = resp.json()
        return data if isinstance(data, list) else data.get("items", data.get("pipelines", []))


async def pipeline_retry(pid: int) -> dict:
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.post(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/retry")
        out = {"status_code": resp.status_code}
        try:
            out.update(resp.json())
        except Exception:
            pass
        return out


async def get_pipeline(pid: int) -> dict:
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}")
        resp.raise_for_status()
        return resp.json()


async def post_pipeline_feedback(pid: int, step: str, intent: str,
                                 feedback_text: Optional[str] = None) -> dict:
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.post(
            f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/feedback",
            json={"step": step, "intent": intent, "feedback_text": feedback_text},
        )
        resp.raise_for_status()
        return resp.json()


async def save_pipeline_telegram_msg(pid: int, step: str, msg_id: int) -> None:
    async with httpx.AsyncClient(timeout=10) as client:
        await client.patch(
            f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/telegram-msg",
            json={"step": step, "message_id": msg_id},
        )


async def lookup_pipeline_by_msg(msg_id: int) -> Optional[dict]:
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/lookup-by-msg/{msg_id}")
        if resp.status_code == 200:
            return resp.json()
        return None


async def lotto_best() -> List[Dict[str, Any]]:
    """GET /api/lotto/best — best_picks 20개 (numbers + scores 5종)."""
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/best")
    resp.raise_for_status()
    data = resp.json()
    items = data.get("items") if isinstance(data, dict) else data
    return items or []


async def lotto_strategy_weights() -> Dict[str, float]:
    """GET /api/lotto/strategy/weights — 전략별 가중치 dict."""
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/strategy/weights")
    resp.raise_for_status()
    data = resp.json()
    weights = data.get("weights") if isinstance(data, dict) else data
    if isinstance(weights, list):
        return {item["strategy"]: float(item["weight"]) for item in weights}
    return {k: float(v) for k, v in (weights or {}).items()}


async def lotto_latest_draw() -> Optional[int]:
    """GET /api/lotto/latest — 최신 회차 번호만 반환."""
    from .config import LOTTO_BACKEND_URL
    try:
        resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/latest")
        resp.raise_for_status()
        data = resp.json()
        # /api/lotto/latest 응답 키: {"drawNo": N, ...}
        # 하위 호환을 위해 drawNo, draw_no, drwNo, draw 순서로 시도
        for key in ("drawNo", "draw_no", "drwNo", "draw"):
            if isinstance(data, dict) and data.get(key):
                return int(data[key])
        return None
    except Exception:
        return None


async def lotto_evolver_status() -> Dict[str, Any]:
    """GET /api/lotto/evolver/status — 이번주 trials + 다음주 base 정보."""
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/evolver/status")
    resp.raise_for_status()
    return resp.json()


async def lotto_evolver_evaluate() -> Dict[str, Any]:
    """POST /api/lotto/evolver/evaluate-now — 회고 트리거 (텔레그램 리포트용)."""
    from .config import LOTTO_BACKEND_URL
    async with httpx.AsyncClient(timeout=60.0) as client:
        resp = await client.post(f"{LOTTO_BACKEND_URL}/api/lotto/evolver/evaluate-now")
        resp.raise_for_status()
        return resp.json()


async def lotto_backtest_review(draw_no: int) -> Dict[str, Any]:
    from .config import LOTTO_BACKEND_URL
    resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/backtest/review/{draw_no}")
    resp.raise_for_status()
    return resp.json()



from .config import AGENT_CONTAINER_MAP


async def fetch_service_logs(
    agent_id: str,
    since: Optional[str] = None,
    limit: int = 200,
) -> List[Dict[str, Any]]:
    """해당 에이전트가 가리키는 컨테이너의 /logs/recent 를 호출해서
    path_prefix 정규식으로 필터한 결과를 반환.

    네트워크 실패 시 빈 리스트를 반환하고 warning 만 남김 (LogTab 이 죽지 않게).
    """
    mapping = AGENT_CONTAINER_MAP.get(agent_id)
    if not mapping:
        return []
    host, port, path_re = mapping
    url = f"http://{host}:{port}/logs/recent"
    params: Dict[str, Any] = {"limit": limit}
    if since:
        params["since"] = since
    try:
        async with httpx.AsyncClient(timeout=3.0) as client:
            resp = await client.get(url, params=params)
            data = resp.json().get("logs", [])
    except Exception as e:
        logger.warning("fetch_service_logs(%s) 실패: %s", agent_id, e)
        return []
    return [
        x for x in data
        if x.get("source") == "log"
        or path_re.match(x.get("path", "") or "")
    ]
