"""GenerationContext — ambient lineage 컨텍스트 (Task A2).

파이프라인 스텝/서비스가 자기 작업을 ``with generation_context(...)`` 로 감싼다.
sink 가 ambient 로 읽어 lineage(stage/still_id/scene_index/...)를 채우고, 이 scope
전용 CaptureQueue 에 enqueue 한다. scope 종료 시 ``finally`` 에서 queue.flush()
(독립 non-fatal 세션)로 일괄 영속화한다.

worker thread 는 contextvars 가 자동 전파되지 않으므로 ``bind_context(ctx)`` 로
명시 전파한다(image_tracer.set_context / budget bind 패턴 정합).

★ 모든 동작 non-fatal — flush 실패는 로그만(생성 파이프라인 무영향).
"""

from __future__ import annotations

import logging
from contextlib import contextmanager
from contextvars import ContextVar, Token
from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterator, Optional

if TYPE_CHECKING:  # 순환 import 방지 — queue 는 런타임에 lazy import.
    from app.services.image_capture.queue import CaptureQueue

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class GenerationContext:
    """현재 capture scope 의 ambient 메타. frozen — scope 내 불변."""

    project_id: str
    episode_id: Optional[str]
    stage: str
    queue: "CaptureQueue"  # 이 scope 의 flush 큐
    still_id: Optional[str] = None
    scene_index: Optional[int] = None
    shot_index: Optional[int] = None
    entity_id: Optional[str] = None
    #: 이 scope 가 중간물을 자산으로 포착하는가 (2026-08-23).
    #: ★scope 를 여는 것은 그 자체로 capture 를 켜는 것이다(sink 의
    #:   default-capture-off 규약). 신원·trace 만 필요하고 자산은 늘리고
    #:   싶지 않은 자리를 위해 끌 수 있게 둔다.
    capture_enabled: bool = True


_gen_ctx: ContextVar[Optional[GenerationContext]] = ContextVar(
    "image_capture_gen_ctx", default=None
)


def current_context() -> Optional[GenerationContext]:
    """현재 capture scope 의 GenerationContext (없으면 None)."""
    return _gen_ctx.get()


def bind_context(ctx: GenerationContext) -> Token:
    """worker thread 명시 전파용 — ctx 를 현재 contextvar 에 set 하고 token 반환.

    호출자는 작업 종료 시 ``_gen_ctx.reset(token)`` 으로 복원한다.
    """
    return _gen_ctx.set(ctx)


def bind_current_generation_context(fn):
    """호출 시점의 current_context 를 캡처해 worker thread 에서 재설치하는
    래퍼 반환 (image_call_budget.bind_current_budget 패턴 정합).

    ThreadPool 병렬 롤 생성에서 capture 가 skipped_no_context 로 유실되는
    경로 차단 (2026-07-17 still-variants Codex BLOCKING-3). 캡처된
    컨텍스트가 없으면 no-op — 비 capture 경로 무영향. worker 는 호출
    종료 시 reset 으로 clean 상태를 복원한다.
    """
    captured = current_context()

    def _wrapped(*args, **kwargs):
        if captured is None:
            return fn(*args, **kwargs)
        token = bind_context(captured)
        try:
            return fn(*args, **kwargs)
        finally:
            _gen_ctx.reset(token)

    return _wrapped


@contextmanager
def generation_context(
    project_id: str,
    episode_id: Optional[str],
    stage: str,
    *,
    still_id: Optional[str] = None,
    scene_index: Optional[int] = None,
    shot_index: Optional[int] = None,
    entity_id: Optional[str] = None,
    capture: bool = True,
) -> Iterator["CaptureQueue"]:
    """capture scope 를 연다. with-블록 안에서 sink 가 ambient 로 enqueue.

    블록 종료 시(성공/예외 무관) ``finally`` 에서 queue.flush() 로 일괄 영속화.
    flush 는 독립 non-fatal 세션이라 business transaction rollback 에 말려도
    생성된 candidate 가 남는다(사용자 "거부 후보까지 전부" 요구). flush 실패도
    non-fatal — 로그만 남기고 삼킨다.

    yield 값은 이 scope 의 CaptureQueue (테스트/명시 enqueue 용).
    """
    from app.services.image_capture.queue import CaptureQueue

    # ctx 는 queue 를 참조하고, queue 도 ctx 를 참조한다(메타 lineage). 먼저
    # placeholder 로 ctx 를 만들고 queue 에 주입하는 대신, queue(ctx) 시그니처에
    # 맞춰 ctx 를 먼저 구성한다 — frozen 이라 후속 변경 불가하므로 queue 를
    # 먼저 만들고 ctx 에 넣는다. queue 는 생성 시 ctx 가 아직 없어도 동작하도록
    # set_context 로 받는다.
    queue = CaptureQueue.__new__(CaptureQueue)
    ctx = GenerationContext(
        project_id=project_id,
        episode_id=episode_id,
        stage=stage,
        queue=queue,
        still_id=still_id,
        scene_index=scene_index,
        shot_index=shot_index,
        entity_id=entity_id,
        capture_enabled=capture,
    )
    queue.__init__(ctx)  # type: ignore[misc]

    token = _gen_ctx.set(ctx)
    try:
        with _shot_trace_scope(ctx):
            yield queue
    finally:
        try:
            queue.flush()
        except Exception:  # pragma: no cover - flush 자체가 non-fatal raise 안 함
            logger.warning("generation_context: flush failed (non-fatal)", exc_info=True)
        _gen_ctx.reset(token)


@contextmanager
def _shot_trace_scope(ctx: GenerationContext) -> Iterator[None]:
    """이 capture scope 를 Opik trace 하나로 연다.

    ★새 경계를 만들지 않는다 — 이 scope 가 이미 샷 경계다.
    still_id·scene_index·shot_index 를 이미 들고 있고 worker 전파기도 있다.

    설정이 꺼져 있으면 아무것도 안 한다(바이트 동일).
    """
    from app.modules.llm.opik_trace import (
        build_axis_tags, current_trace, open_trace)

    if ctx.still_id:
        name = f"still:{str(ctx.still_id)[:8]} · {ctx.stage}"
    elif ctx.entity_id:
        name = f"entity:{str(ctx.entity_id)[:8]} · {ctx.stage}"
    else:
        name = f"stage:{ctx.stage}"

    meta = {
        k: v for k, v in (
            ("project_id", ctx.project_id),
            ("episode_id", ctx.episode_id),
            ("still_id", ctx.still_id),
            ("scene_index", ctx.scene_index),
            ("shot_index", ctx.shot_index),
            ("entity_id", ctx.entity_id),
            ("step", ctx.stage),
        ) if v is not None
    }
    # 주행 묶음은 부모(스텝 trace)에서 물려받는다. 부모가 없으면 없는 채로
    # 둔다 — 없는 값을 지어내면 묶음이 거짓말을 한다.
    parent = current_trace()
    thread_id = parent.thread_id if parent is not None else None

    with open_trace(name=name, tags=build_axis_tags(step=ctx.stage),
                    metadata=meta, thread_id=thread_id):
        yield
