from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
import hashlib
import json
import os
import re
import shlex
from statistics import mean
import time

from src.runtime_config import load_runtime_profile

_MODEL_RESPONSE_CACHE: dict[str, str] = {}
_KNOWN_MODEL_PROVIDERS = {
    "openai",
    "anthropic",
    "google_genai",
    "google_vertexai",
    "openrouter",
}


def _as_dict(value):
    if value is None:
        return {}
    if isinstance(value, dict):
        return value
    if hasattr(value, "dict"):
        try:
            return value.dict()
        except Exception:
            return {}
    if hasattr(value, "model_dump"):
        try:
            return value.model_dump()
        except Exception:
            return {}
    return {}


def _get(obj, key: str, default=None):
    if isinstance(obj, dict):
        return obj.get(key, default)
    return getattr(obj, key, default)


def _env_int(name: str, default: int, minimum: int = 1, maximum: int = 10) -> int:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        value = int(raw)
    except Exception:
        return default
    return max(minimum, min(value, maximum))


def _env_float(name: str, default: float, minimum: float = 0.1, maximum: float = 10.0) -> float:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        value = float(raw)
    except Exception:
        return default
    return max(minimum, min(value, maximum))


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name, "").strip().lower()
    if not raw:
        return default
    if raw in {"1", "true", "yes", "y", "on"}:
        return True
    if raw in {"0", "false", "no", "n", "off"}:
        return False
    return default


def _call_with_retry(label: str, fn, max_retries: int, base_delay_sec: float):
    errors: list[str] = []
    for attempt in range(1, max_retries + 1):
        try:
            return fn()
        except Exception as exc:
            errors.append(f"{attempt}회차: {exc}")
            if attempt == max_retries:
                break
            time.sleep(base_delay_sec * (2 ** (attempt - 1)))
    raise RuntimeError(f"{label} 실패 (재시도 {max_retries}회): {' | '.join(errors)}")


def _parse_dt(raw: str | None):
    if not raw:
        return None
    value = raw.strip()
    if not value:
        return None
    try:
        if len(value) == 10 and re.match(r"\d{4}-\d{2}-\d{2}$", value):
            dt = datetime.strptime(value, "%Y-%m-%d")
            return dt.replace(tzinfo=timezone.utc)
        normalized = value.replace("Z", "+00:00")
        dt = datetime.fromisoformat(normalized)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except Exception:
        return None


def _extract_json_block(message: str) -> dict:
    stripped = message.strip()
    if stripped.startswith("{") and stripped.endswith("}"):
        try:
            payload = json.loads(stripped)
            return payload if isinstance(payload, dict) else {}
        except Exception:
            return {}
    match = re.search(r"```json\s*(\{.*?\})\s*```", message, re.DOTALL)
    if not match:
        return {}
    try:
        payload = json.loads(match.group(1))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _parse_key_values(message: str) -> dict[str, str]:
    tokens: list[str] = []
    try:
        tokens = shlex.split(message)
    except Exception:
        tokens = message.split()

    pairs: dict[str, str] = {}
    for token in tokens:
        if "=" not in token:
            continue
        key, value = token.split("=", 1)
        key = key.strip()
        value = value.strip()
        if key:
            pairs[key] = value
    return pairs


def _parse_filters(message: str) -> dict:
    filters = {
        "mode": "overview",
        "from": None,
        "to": None,
        "limit": 20,
        "name": None,
        "user_id": None,
        "session_id": None,
        "trace_id": None,
        "tags": [],
        "environment": None,
        "metadata": {},
        "compare_models": False,
        "prompt": None,
    }

    payload = _extract_json_block(message)
    if payload:
        for key in ("mode", "from", "to", "name", "user_id", "session_id", "trace_id", "environment", "prompt"):
            if key in payload:
                filters[key] = payload.get(key)
        if "limit" in payload:
            try:
                filters["limit"] = int(payload.get("limit", 20))
            except Exception:
                filters["limit"] = 20
        tags = payload.get("tags", [])
        if isinstance(tags, str):
            tags = [t.strip() for t in tags.split(",") if t.strip()]
        if isinstance(tags, list):
            filters["tags"] = [str(t).strip() for t in tags if str(t).strip()]
        metadata = payload.get("metadata", {})
        if isinstance(metadata, dict):
            filters["metadata"] = {str(k): str(v) for k, v in metadata.items()}
        if "compare_models" in payload:
            filters["compare_models"] = bool(payload.get("compare_models"))
        return _normalize_filters(filters, message)

    kv = _parse_key_values(message)
    for key in ("mode", "from", "to", "name", "user_id", "session_id", "trace_id", "environment", "prompt"):
        if key in kv:
            filters[key] = kv[key]
    if "limit" in kv:
        try:
            filters["limit"] = int(kv["limit"])
        except Exception:
            filters["limit"] = 20
    if "tags" in kv:
        filters["tags"] = [v.strip() for v in kv["tags"].split(",") if v.strip()]
    for key, value in kv.items():
        if key.startswith("metadata."):
            filters["metadata"][key.split(".", 1)[1]] = value
    if "compare_models" in kv:
        filters["compare_models"] = kv["compare_models"].lower() in {"1", "true", "yes", "y"}
    return _normalize_filters(filters, message)


def _normalize_filters(filters: dict, message: str) -> dict:
    filters["limit"] = max(1, min(int(filters.get("limit", 20) or 20), 100))
    lowered = message.lower()
    mode = str(filters.get("mode", "") or "").strip().lower()
    if not mode:
        mode = _infer_mode_from_text(message)
    filters["mode"] = _normalize_mode(mode)
    if any(word in lowered for word in ("model compare", "compare models", "모델 비교", "모델별")):
        filters["compare_models"] = True
    return filters


def _normalize_mode(raw: str) -> str:
    aliases = {
        "overview": "overview",
        "summary": "overview",
        "observability": "observability",
        "debug": "observability",
        "root_cause": "observability",
        "prompt": "prompt_engineering",
        "prompt_engineering": "prompt_engineering",
        "eval": "evaluation",
        "evaluation": "evaluation",
    }
    return aliases.get(raw, "overview")


def _infer_mode_from_text(message: str) -> str:
    lowered = message.lower()
    if any(word in lowered for word in ("root cause", "debug", "관찰", "원인", "디버깅", "trace breakdown")):
        return "observability"
    if any(word in lowered for word in ("prompt", "프롬프트", "rewrite", "개선 문구")):
        return "prompt_engineering"
    if any(word in lowered for word in ("evaluation", "eval", "rubric", "평가", "채점")):
        return "evaluation"
    return "overview"


def _langfuse_config(profile: dict) -> dict:
    nested = profile.get("langfuse", {}) if isinstance(profile.get("langfuse"), dict) else {}
    host = (
        os.getenv("LANGFUSE_BASE_URL", "").strip()
        or os.getenv("AGENT_LANGFUSE_HOST", "").strip()
        or str(nested.get("host", "")).strip()
    )
    public_key = (
        os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
        or os.getenv("AGENT_LANGFUSE_PUBLIC_KEY", "").strip()
        or str(nested.get("public_key", "")).strip()
    )
    secret_key = (
        os.getenv("LANGFUSE_SECRET_KEY", "").strip()
        or os.getenv("AGENT_LANGFUSE_SECRET_KEY", "").strip()
        or str(nested.get("secret_key", "")).strip()
    )
    environment = (
        os.getenv("LANGFUSE_ENVIRONMENT", "").strip()
        or os.getenv("AGENT_LANGFUSE_ENVIRONMENT", "").strip()
        or str(nested.get("environment", "")).strip()
    )
    return {
        "host": host,
        "public_key": public_key,
        "secret_key": secret_key,
        "environment": environment,
    }


def _build_langfuse_client(profile: dict):
    cfg = _langfuse_config(profile)
    if not cfg["host"] or not cfg["public_key"] or not cfg["secret_key"]:
        return None, cfg, (
            "Langfuse 설정이 부족합니다. LANGFUSE_BASE_URL/LANGFUSE_PUBLIC_KEY/"
            "LANGFUSE_SECRET_KEY를 환경변수로 설정하세요."
        )
    try:
        from langfuse import Langfuse
    except Exception as exc:
        return None, cfg, f"langfuse SDK import 실패: {exc}"
    try:
        client = Langfuse(
            public_key=cfg["public_key"],
            secret_key=cfg["secret_key"],
            host=cfg["host"],
            environment=cfg["environment"] or None,
        )
    except Exception as exc:
        return None, cfg, f"Langfuse client 생성 실패: {exc}"
    return client, cfg, None


def _metadata_match(meta, required: dict[str, str]) -> bool:
    if not required:
        return True
    if not isinstance(meta, dict):
        return False
    for key, expected in required.items():
        current = meta
        for segment in key.split("."):
            if isinstance(current, dict) and segment in current:
                current = current[segment]
            else:
                return False
        if str(current) != str(expected):
            return False
    return True


def _langfuse_call(label: str, fn):
    retries = _env_int("AGENT_LANGFUSE_MAX_RETRIES", default=3, minimum=1, maximum=8)
    base_delay = _env_float("AGENT_LANGFUSE_RETRY_BASE_SEC", default=0.6, minimum=0.1, maximum=5.0)
    return _call_with_retry(label, fn, retries, base_delay)


def _fetch_trace_detail(client, trace_id: str):
    return _langfuse_call(f"langfuse.trace.get({trace_id})", lambda: client.api.trace.get(trace_id))


def _fetch_trace_observations(client, trace_id: str, limit: int = 200) -> list:
    def _call():
        return client.api.observations.get_many(trace_id=trace_id, limit=limit)

    response = _langfuse_call(f"langfuse.observations.get_many({trace_id})", _call)
    return list(getattr(response, "data", []) or [])


def _fetch_traces(client, filters: dict):
    target_limit = max(1, min(int(filters.get("limit", 20) or 20), 100))
    request_limit = max(20, target_limit)
    request_limit = min(request_limit, 100)

    base_kwargs = {"limit": request_limit}
    for key in ("name", "user_id", "session_id", "environment"):
        value = filters.get(key)
        if value:
            base_kwargs[key] = value
    if filters.get("tags"):
        base_kwargs["tags"] = filters["tags"]
    from_ts = _parse_dt(filters.get("from"))
    to_ts = _parse_dt(filters.get("to"))
    if from_ts:
        base_kwargs["from_timestamp"] = from_ts
    if to_ts:
        base_kwargs["to_timestamp"] = to_ts

    need_metadata_filter = bool(filters.get("metadata"))
    max_pages = _env_int("AGENT_LANGFUSE_MAX_PAGES", default=3, minimum=1, maximum=20)
    collected: list = []

    for page in range(1, max_pages + 1):
        kwargs = dict(base_kwargs)
        kwargs["page"] = page
        response = _langfuse_call("langfuse.trace.list", lambda kwargs=kwargs: client.api.trace.list(**kwargs))
        traces = list(getattr(response, "data", []) or [])
        if not traces:
            break

        if need_metadata_filter:
            meta_required = filters.get("metadata", {})
            traces = [trace for trace in traces if _metadata_match(_get(trace, "metadata", {}), meta_required)]

        collected.extend(traces)
        if len(collected) >= target_limit:
            break
        if len(traces) < request_limit:
            break

    traces = collected[:target_limit]

    trace_id = filters.get("trace_id")
    if trace_id:
        detail = _fetch_trace_detail(client, str(trace_id))
        traces = [detail]
    return traces


def _to_number(raw) -> float | None:
    try:
        value = float(raw)
    except Exception:
        return None
    if value != value:
        return None
    return value


def _parse_trace_timestamp(trace) -> datetime | None:
    raw = _get(trace, "timestamp")
    if isinstance(raw, datetime):
        return raw if raw.tzinfo else raw.replace(tzinfo=timezone.utc)
    if not raw:
        return None
    try:
        value = str(raw).replace("Z", "+00:00")
        parsed = datetime.fromisoformat(value)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed
    except Exception:
        return None


def _summarize(traces: list) -> dict:
    latencies = []
    costs = []
    users = set()
    sessions = set()
    trace_with_error = 0
    missing_user = 0
    missing_session = 0
    model_counts: dict[str, int] = {}
    score_values = []
    traces_with_observations = 0

    for trace in traces:
        user_id = _get(trace, "user_id")
        session_id = _get(trace, "session_id")
        if user_id:
            users.add(str(user_id))
        else:
            missing_user += 1
        if session_id:
            sessions.add(str(session_id))
        else:
            missing_session += 1

        latency = _to_number(_get(trace, "latency"))
        if latency is not None:
            latencies.append(latency)

        total_cost = _to_number(_get(trace, "total_cost"))
        if total_cost is not None:
            costs.append(total_cost)

        scores = _get(trace, "scores", []) or []
        for score in scores:
            value = _to_number(_get(score, "value"))
            if value is not None:
                score_values.append(value)

        has_error = False
        observations = _get(trace, "observations", []) or []
        if observations:
            traces_with_observations += 1
        for obs in observations:
            level = str(_get(obs, "level", "")).lower()
            status_message = str(_get(obs, "status_message", "") or "").strip()
            model = str(_get(obs, "model", "") or _get(obs, "model_id", "") or "").strip()
            if model:
                model_counts[model] = model_counts.get(model, 0) + 1
            if level in {"error", "fatal"} or status_message:
                has_error = True

        # observation이 빈 trace는 metadata 기반 모델 힌트를 사용
        if not observations:
            meta = _get(trace, "metadata", {}) if isinstance(_get(trace, "metadata", {}), dict) else {}
            hinted_model = str(meta.get("model") or meta.get("model_id") or "").strip()
            if hinted_model:
                model_counts[hinted_model] = model_counts.get(hinted_model, 0) + 1

        if has_error:
            trace_with_error += 1

    return {
        "count": len(traces),
        "unique_users": len(users),
        "unique_sessions": len(sessions),
        "missing_user": missing_user,
        "missing_session": missing_session,
        "avg_latency_ms": mean(latencies) if latencies else None,
        "max_latency_ms": max(latencies) if latencies else None,
        "total_cost": sum(costs) if costs else 0.0,
        "error_trace_count": trace_with_error,
        "error_rate": (trace_with_error / len(traces)) if traces else 0.0,
        "avg_score": mean(score_values) if score_values else None,
        "model_counts": dict(sorted(model_counts.items(), key=lambda item: item[1], reverse=True)[:8]),
        "traces_with_observations": traces_with_observations,
    }


def _heuristic_opinions(metrics: dict) -> list[str]:
    if metrics["count"] <= 0:
        return ["조건에 맞는 trace가 없어 분석 의견을 생성할 수 없습니다."]

    sample_size = int(metrics.get("count", 0))
    opinions = []
    error_rate = metrics.get("error_rate", 0.0)
    if sample_size < 5:
        opinions.append(f"- 주의: 샘플 수가 {sample_size}건으로 적어 통계 신뢰도가 낮습니다.")

    if error_rate >= 0.2 and sample_size >= 5:
        opinions.append(
            f"- 안정성 경고: 오류성 trace 비율이 {error_rate * 100:.1f}%로 높습니다. 실패 trace의 공통 프롬프트/툴 콜 패턴을 우선 분리해 보세요."
        )
    elif error_rate > 0:
        opinions.append(f"- 안정성: 오류성 trace 비율은 {error_rate * 100:.1f}%이며, 추세 모니터링 대상입니다.")
    else:
        opinions.append("- 안정성: 오류성 trace가 관측되지 않았습니다.")

    avg_latency = metrics.get("avg_latency_ms")
    if avg_latency is not None:
        if avg_latency >= 5000:
            opinions.append(f"- 성능: 평균 latency가 {avg_latency:.1f}ms로 높습니다. 모델 라우팅/컨텍스트 길이 최적화를 권장합니다.")
        else:
            opinions.append(f"- 성능: 평균 latency는 {avg_latency:.1f}ms 수준입니다.")

    if metrics.get("missing_user", 0) > 0 or metrics.get("missing_session", 0) > 0:
        opinions.append(
            f"- 계측 품질: user_id 누락 {metrics.get('missing_user', 0)}건, session_id 누락 {metrics.get('missing_session', 0)}건이 있습니다."
        )

    if metrics.get("model_counts"):
        top_model, top_count = next(iter(metrics["model_counts"].items()))
        opinions.append(f"- 모델 사용 패턴: `{top_model}` 모델 호출이 가장 많습니다({top_count}건).")

    obs_count = int(metrics.get("traces_with_observations", 0))
    if sample_size > 0 and obs_count < sample_size:
        opinions.append(
            f"- 계측 가시성: observation 상세 포함 trace가 {obs_count}/{sample_size}건입니다. 상세 분석 모드에서 trace.get/observations 보강을 권장합니다."
        )

    avg_score = metrics.get("avg_score")
    if avg_score is not None:
        opinions.append(f"- 품질 스코어: 평균 score는 {avg_score:.3f}입니다.")
    return opinions


def _confidence_level(metrics: dict) -> str:
    sample_size = int(metrics.get("count", 0))
    obs_count = int(metrics.get("traces_with_observations", 0))
    if sample_size < 5:
        return "low"
    if sample_size < 20:
        return "medium" if obs_count >= max(1, sample_size // 2) else "low"
    if obs_count >= int(sample_size * 0.7):
        return "high"
    return "medium"


def _session_cohort_summary(traces: list, limit: int = 10) -> list[dict]:
    buckets: dict[str, dict] = {}
    for trace in traces:
        session_id = str(_get(trace, "session_id") or "unknown")
        bucket = buckets.setdefault(
            session_id,
            {
                "session_id": session_id,
                "trace_count": 0,
                "error_count": 0,
                "latencies": [],
                "total_cost": 0.0,
            },
        )
        bucket["trace_count"] += 1
        latency = _to_number(_get(trace, "latency"))
        if latency is not None:
            bucket["latencies"].append(latency)
        cost = _to_number(_get(trace, "total_cost"))
        if cost is not None:
            bucket["total_cost"] += cost

        has_error = False
        for obs in (_get(trace, "observations", []) or []):
            level = str(_get(obs, "level", "")).lower()
            status_message = str(_get(obs, "status_message", "") or "").strip()
            if level in {"error", "fatal"} or status_message:
                has_error = True
                break
        if has_error:
            bucket["error_count"] += 1

    rows: list[dict] = []
    for session_id, payload in buckets.items():
        latencies = payload.pop("latencies", [])
        payload["avg_latency_ms"] = mean(latencies) if latencies else None
        payload["error_rate"] = (
            payload["error_count"] / payload["trace_count"] if payload["trace_count"] else 0.0
        )
        rows.append(payload)

    rows.sort(key=lambda x: (x.get("trace_count", 0), x.get("total_cost", 0.0)), reverse=True)
    return rows[:limit]


def _window_metrics(traces: list) -> dict:
    if not traces:
        return {
            "count": 0,
            "avg_latency_ms": None,
            "total_cost": 0.0,
            "error_rate": 0.0,
        }

    latencies = []
    costs = []
    error_count = 0
    for trace in traces:
        latency = _to_number(_get(trace, "latency"))
        if latency is not None:
            latencies.append(latency)
        cost = _to_number(_get(trace, "total_cost"))
        if cost is not None:
            costs.append(cost)
        for obs in (_get(trace, "observations", []) or []):
            level = str(_get(obs, "level", "")).lower()
            status_message = str(_get(obs, "status_message", "") or "").strip()
            if level in {"error", "fatal"} or status_message:
                error_count += 1
                break
    return {
        "count": len(traces),
        "avg_latency_ms": mean(latencies) if latencies else None,
        "total_cost": sum(costs) if costs else 0.0,
        "error_rate": (error_count / len(traces)) if traces else 0.0,
    }


def _trend_delta(prev: float | None, curr: float | None) -> float | None:
    if prev is None or curr is None:
        return None
    if prev == 0:
        return None if curr == 0 else 100.0
    return ((curr - prev) / prev) * 100.0


def _time_regression_summary(traces: list) -> dict:
    ordered = sorted(
        traces,
        key=lambda trace: _parse_trace_timestamp(trace) or datetime.min.replace(tzinfo=timezone.utc),
    )
    if len(ordered) < 2:
        return {
            "previous_window": _window_metrics(ordered),
            "current_window": _window_metrics(ordered),
            "deltas_pct": {"avg_latency_ms": None, "error_rate": None, "total_cost": None},
            "regression_flags": [],
        }

    mid = len(ordered) // 2
    older = ordered[:mid]
    newer = ordered[mid:]
    prev_metrics = _window_metrics(older)
    curr_metrics = _window_metrics(newer)

    deltas = {
        "avg_latency_ms": _trend_delta(prev_metrics.get("avg_latency_ms"), curr_metrics.get("avg_latency_ms")),
        "error_rate": _trend_delta(prev_metrics.get("error_rate"), curr_metrics.get("error_rate")),
        "total_cost": _trend_delta(prev_metrics.get("total_cost"), curr_metrics.get("total_cost")),
    }
    flags = []
    if deltas["avg_latency_ms"] is not None and deltas["avg_latency_ms"] >= 30:
        flags.append("latency_regression")
    if deltas["error_rate"] is not None and deltas["error_rate"] >= 30:
        flags.append("error_rate_regression")
    if deltas["total_cost"] is not None and deltas["total_cost"] >= 30:
        flags.append("cost_regression")

    return {
        "previous_window": prev_metrics,
        "current_window": curr_metrics,
        "deltas_pct": deltas,
        "regression_flags": flags,
    }


def _compact_metadata(meta):
    if not isinstance(meta, dict):
        return {}
    keep_keys = [
        "source",
        "event",
        "model",
        "provider_id",
        "model_id",
        "thread_id",
        "turn_id",
        "cwd",
    ]
    compact = {key: meta.get(key) for key in keep_keys if key in meta}
    usage = meta.get("usage")
    if isinstance(usage, dict):
        compact["usage"] = {
            "input_tokens": usage.get("input_tokens"),
            "output_tokens": usage.get("output_tokens"),
            "total_tokens": usage.get("total_tokens"),
        }
    return compact


def _trace_preview(traces: list, limit: int = 5) -> list[dict]:
    rows = []
    for trace in traces[:limit]:
        rows.append(
            {
                "id": str(_get(trace, "id", "")),
                "name": str(_get(trace, "name", "")),
                "timestamp": str(_get(trace, "timestamp", "")),
                "user_id": _get(trace, "user_id"),
                "session_id": _get(trace, "session_id"),
                "latency": _get(trace, "latency"),
                "total_cost": _get(trace, "total_cost"),
                "tags": _get(trace, "tags", []),
                "metadata": _compact_metadata(_get(trace, "metadata", {})),
            }
        )
    return rows


def _profile_llm_config(profile: dict) -> dict:
    llm = profile.get("llm", {}) if isinstance(profile.get("llm"), dict) else {}
    temperature = _to_number(llm.get("temperature"))
    timeout_sec = _to_number(llm.get("timeout_sec"))
    max_workers = _to_number(llm.get("max_workers"))
    max_retries = _to_number(llm.get("max_retries"))
    return {
        "comparison_models": llm.get("comparison_models", []),
        "prompt_review_model": str(llm.get("prompt_review_model", "") or "").strip(),
        "temperature": temperature if temperature is not None else 0.2,
        "max_workers": max(1, int(max_workers if max_workers is not None else 3)),
        "timeout_sec": timeout_sec if timeout_sec is not None else 25.0,
        "max_retries": max(1, int(max_retries if max_retries is not None else 2)),
        "openrouter_base_url": str(llm.get("openrouter_base_url", "https://openrouter.ai/api/v1")).strip(),
    }


def _normalize_provider(provider: str | None) -> str | None:
    if not provider:
        return None
    lowered = provider.strip().lower()
    aliases = {
        "openai": "openai",
        "anthropic": "anthropic",
        "claude": "anthropic",
        "google": "google_genai",
        "google_genai": "google_genai",
        "gemini": "google_genai",
        "google_vertexai": "google_vertexai",
        "openrouter": "openrouter",
    }
    return aliases.get(lowered, lowered)


def _parse_model_spec(raw_spec, forced_provider: str | None = None) -> dict | None:
    if isinstance(raw_spec, dict):
        provider = _normalize_provider(str(raw_spec.get("provider", "")).strip() or forced_provider)
        model = str(raw_spec.get("model", "")).strip()
        alias = str(raw_spec.get("alias", "")).strip()
        if not model:
            return None
        resolved_alias = alias or (f"{provider}:{model}" if provider else model)
        return {
            "provider": provider,
            "model": model,
            "alias": resolved_alias,
        }

    raw = str(raw_spec or "").strip()
    if not raw:
        return None

    provider = _normalize_provider(forced_provider)
    model = raw
    if ":" in raw:
        prefix, remainder = raw.split(":", 1)
        normalized_prefix = _normalize_provider(prefix)
        if normalized_prefix in _KNOWN_MODEL_PROVIDERS:
            provider = normalized_prefix
            model = remainder.strip()

    if not model:
        return None
    return {
        "provider": provider,
        "model": model,
        "alias": f"{provider}:{model}" if provider else model,
    }


def _csv_values(raw: str) -> list[str]:
    return [item.strip() for item in str(raw or "").split(",") if item.strip()]


def _comparison_model_specs(profile: dict) -> list[dict]:
    llm_cfg = _profile_llm_config(profile)
    specs: list[dict] = []

    explicit_specs = os.getenv("AGENT_LLM_COMPARISON_MODELS", "").strip() or os.getenv("AGENT_MODEL_SPECS", "").strip()
    if explicit_specs:
        for value in _csv_values(explicit_specs):
            parsed = _parse_model_spec(value)
            if parsed:
                specs.append(parsed)
    else:
        legacy_openrouter_specs = (
            os.getenv("AGENT_OPENROUTER_MODELS", "").strip()
            or os.getenv("OPENROUTER_MODELS", "").strip()
        )
        if legacy_openrouter_specs:
            for value in _csv_values(legacy_openrouter_specs):
                parsed = _parse_model_spec(value, forced_provider="openrouter")
                if parsed:
                    specs.append(parsed)
        else:
            configured = llm_cfg.get("comparison_models", [])
            if isinstance(configured, list):
                for item in configured:
                    parsed = _parse_model_spec(item)
                    if parsed:
                        specs.append(parsed)

    if not specs:
        if os.getenv("OPENAI_API_KEY", "").strip():
            specs.append(_parse_model_spec(f"openai:{os.getenv('AGENT_OPENAI_MODEL', 'gpt-4o-mini')}"))
        if os.getenv("ANTHROPIC_API_KEY", "").strip():
            specs.append(
                _parse_model_spec(f"anthropic:{os.getenv('AGENT_ANTHROPIC_MODEL', 'claude-3-5-sonnet-latest')}")
            )
        if os.getenv("GOOGLE_API_KEY", "").strip() or os.getenv("GEMINI_API_KEY", "").strip():
            specs.append(_parse_model_spec(f"google_genai:{os.getenv('AGENT_GEMINI_MODEL', 'gemini-2.0-flash')}"))
        if os.getenv("OPENROUTER_API_KEY", "").strip():
            fallback_model = os.getenv("OPENROUTER_MODEL", "").strip() or "openai/gpt-4o-mini"
            specs.append(_parse_model_spec(f"openrouter:{fallback_model}"))

    deduped: list[dict] = []
    seen: set[str] = set()
    for spec in specs:
        if not spec:
            continue
        alias = str(spec.get("alias", "")).strip()
        if not alias or alias in seen:
            continue
        seen.add(alias)
        deduped.append(spec)
    return deduped


def _extract_text_from_model_output(content) -> str:
    if isinstance(content, str):
        return content.strip()
    if isinstance(content, list):
        texts = []
        for item in content:
            if isinstance(item, str) and item.strip():
                texts.append(item.strip())
                continue
            if isinstance(item, dict):
                text = str(item.get("text", "")).strip()
                if text:
                    texts.append(text)
        return "\n".join(texts).strip()
    return str(content or "").strip()


def _invoke_model_with_langchain(profile: dict, spec: dict, context_payload: dict) -> str:
    provider = _normalize_provider(spec.get("provider"))
    model = str(spec.get("model", "")).strip()
    alias = str(spec.get("alias", model)).strip() or model
    if not model:
        return "(모델 이름 누락)"

    cache_key = hashlib.sha256(
        (alias + "::" + json.dumps(context_payload, ensure_ascii=False, sort_keys=True)).encode("utf-8")
    ).hexdigest()
    if cache_key in _MODEL_RESPONSE_CACHE:
        return _MODEL_RESPONSE_CACHE[cache_key]

    try:
        from langchain.chat_models import init_chat_model
        from langchain_core.messages import HumanMessage, SystemMessage
    except Exception as exc:
        return f"(langchain init_chat_model import 실패: {exc})"

    llm_cfg = _profile_llm_config(profile)
    timeout_sec = _env_float(
        "AGENT_LLM_TIMEOUT_SEC",
        default=float(llm_cfg.get("timeout_sec", 25.0)),
        minimum=5.0,
        maximum=120.0,
    )
    retries = _env_int(
        "AGENT_LLM_MAX_RETRIES",
        default=int(llm_cfg.get("max_retries", 2)),
        minimum=1,
        maximum=8,
    )
    retry_delay = _env_float("AGENT_LLM_RETRY_BASE_SEC", default=0.8, minimum=0.1, maximum=5.0)
    temperature = _env_float(
        "AGENT_LLM_TEMPERATURE",
        default=float(llm_cfg.get("temperature", 0.2)),
        minimum=0.0,
        maximum=2.0,
    )

    model_provider = provider
    model_kwargs = {
        "temperature": temperature,
        "timeout": timeout_sec,
        "max_retries": retries,
    }
    if provider == "openrouter":
        api_key = os.getenv("OPENROUTER_API_KEY", "").strip()
        if not api_key:
            return "(OPENROUTER_API_KEY 미설정)"
        model_provider = "openai"
        model_kwargs["api_key"] = api_key
        model_kwargs["base_url"] = (
            os.getenv("AGENT_OPENROUTER_BASE_URL", "").strip()
            or os.getenv("OPENROUTER_BASE_URL", "").strip()
            or str(llm_cfg.get("openrouter_base_url", "")).strip()
            or "https://openrouter.ai/api/v1"
        )
    elif provider == "google_genai":
        google_key = os.getenv("GOOGLE_API_KEY", "").strip() or os.getenv("GEMINI_API_KEY", "").strip()
        if google_key:
            model_kwargs["api_key"] = google_key

    def _invoke():
        llm = init_chat_model(model=model, model_provider=model_provider, **model_kwargs)
        response = llm.invoke(
            [
                SystemMessage(
                    content=(
                        "You are a Langfuse trace analyst. Return concise Korean bullets only. "
                        "Focus on reliability, latency, cost, and instrumentation quality."
                    )
                ),
                HumanMessage(content=json.dumps(context_payload, ensure_ascii=False)),
            ]
        )
        text = _extract_text_from_model_output(_get(response, "content", ""))
        return text if text else "(content 비어있음)"

    try:
        output = _call_with_retry(f"model({alias})", _invoke, retries, retry_delay)
        _MODEL_RESPONSE_CACHE[cache_key] = output
        return output
    except Exception as exc:
        return f"(실패: {exc})"


def _model_comparison_section(profile: dict, metrics: dict, traces: list, filters: dict) -> str:
    model_specs = _comparison_model_specs(profile)
    if not model_specs:
        return "## Model comparison\n- 비교할 모델이 설정되지 않았습니다. (AGENT_LLM_COMPARISON_MODELS 또는 llm.comparison_models)"

    context_payload = {
        "filters": filters,
        "metrics": metrics,
        "trace_preview": _trace_preview(traces, limit=5),
    }
    llm_cfg = _profile_llm_config(profile)
    max_workers = min(
        len(model_specs),
        _env_int("AGENT_LLM_MAX_WORKERS", default=int(llm_cfg.get("max_workers", 3)), minimum=1, maximum=8),
    )
    outputs: dict[str, str] = {}
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_map = {
            executor.submit(_invoke_model_with_langchain, profile, spec, context_payload): spec for spec in model_specs
        }
        for future in as_completed(future_map):
            spec = future_map[future]
            label = str(spec.get("alias", spec.get("model", "unknown")))
            try:
                outputs[label] = future.result()
            except Exception as exc:
                outputs[label] = f"(비교 실패: {exc})"

    lines = ["## Model comparison (LangChain init_chat_model)"]
    for spec in model_specs:
        label = str(spec.get("alias", spec.get("model", "unknown")))
        output = outputs.get(label, "(결과 없음)")
        lines.append(f"### {label}\n{output}")
    return "\n\n".join(lines)


def _format_filters(filters: dict) -> str:
    payload = dict(filters)
    payload["metadata"] = filters.get("metadata", {})
    return json.dumps(payload, ensure_ascii=False, indent=2)


def _select_trace_for_deep_dive(traces: list, trace_id: str | None):
    if trace_id:
        for trace in traces:
            if str(_get(trace, "id", "")) == trace_id:
                return trace
        return traces[0] if traces else None
    if not traces:
        return None
    return max(
        traces,
        key=lambda t: (
            1 if _to_number(_get(t, "latency")) is not None else 0,
            _to_number(_get(t, "latency")) or 0.0,
            _to_number(_get(t, "total_cost")) or 0.0,
        ),
    )


def _trace_diagnostics(trace, observations: list | None = None) -> dict:
    if observations is None:
        observations = _get(trace, "observations", []) or []
    obs_count = len(observations)
    by_type: dict[str, int] = {}
    by_level: dict[str, int] = {}
    with_status = []
    model_counts: dict[str, int] = {}

    for obs in observations:
        obs_type = str(_get(obs, "type", "unknown") or "unknown").lower()
        by_type[obs_type] = by_type.get(obs_type, 0) + 1

        level = str(_get(obs, "level", "default") or "default").lower()
        by_level[level] = by_level.get(level, 0) + 1

        status_message = str(_get(obs, "status_message", "") or "").strip()
        if status_message:
            with_status.append(
                {
                    "id": str(_get(obs, "id", "")),
                    "name": str(_get(obs, "name", "")),
                    "level": level,
                    "status_message": status_message[:280],
                }
            )

        model = str(_get(obs, "model", "") or _get(obs, "model_id", "") or "").strip()
        if model:
            model_counts[model] = model_counts.get(model, 0) + 1

    return {
        "obs_count": obs_count,
        "by_type": dict(sorted(by_type.items(), key=lambda item: item[1], reverse=True)),
        "by_level": dict(sorted(by_level.items(), key=lambda item: item[1], reverse=True)),
        "error_observations": with_status[:5],
        "model_counts": dict(sorted(model_counts.items(), key=lambda item: item[1], reverse=True)),
    }


def _rubric_scores(metrics: dict) -> dict:
    reliability = max(0.0, 5.0 - (metrics.get("error_rate", 0.0) * 10.0))
    latency = metrics.get("avg_latency_ms")
    if latency is None:
        efficiency = 3.0
    elif latency <= 1000:
        efficiency = 5.0
    elif latency <= 3000:
        efficiency = 4.0
    elif latency <= 5000:
        efficiency = 3.0
    elif latency <= 8000:
        efficiency = 2.0
    else:
        efficiency = 1.0

    instrumentation = 5.0
    instrumentation -= min(2.0, metrics.get("missing_user", 0) * 0.2)
    instrumentation -= min(2.0, metrics.get("missing_session", 0) * 0.2)
    instrumentation = max(1.0, instrumentation)
    overall = round((reliability + efficiency + instrumentation) / 3.0, 2)
    return {
        "reliability": round(reliability, 2),
        "efficiency": round(efficiency, 2),
        "instrumentation": round(instrumentation, 2),
        "overall": overall,
    }


def _first_non_empty(*values) -> str | None:
    for value in values:
        if value is None:
            continue
        text = str(value).strip()
        if text:
            return text
    return None


def _extract_prompt_from_trace(trace) -> str | None:
    if trace is None:
        return None
    metadata = _get(trace, "metadata", {})
    if not isinstance(metadata, dict):
        metadata = {}

    candidate = _first_non_empty(
        metadata.get("system_prompt"),
        metadata.get("prompt"),
        metadata.get("instruction"),
        metadata.get("input_prompt"),
        _get(trace, "input"),
    )
    if candidate:
        return candidate

    messages = metadata.get("messages")
    if isinstance(messages, list):
        chunks = []
        for msg in messages[:3]:
            role = str(_get(msg, "role", "")).strip()
            content = str(_get(msg, "content", "")).strip()
            if content:
                chunks.append(f"[{role or 'msg'}] {content}")
        if chunks:
            return "\n".join(chunks)
    return None


def _resolve_baseline_prompt(filters: dict, trace, metrics: dict) -> str:
    explicit = _first_non_empty(filters.get("prompt"))
    if explicit:
        return explicit
    extracted = _extract_prompt_from_trace(trace)
    if extracted:
        return extracted

    hints = []
    if metrics.get("error_rate", 0.0) >= 0.1:
        hints.append("정확성/검증")
    if (metrics.get("avg_latency_ms") or 0) >= 3000:
        hints.append("응답 지연")
    if metrics.get("missing_user", 0) or metrics.get("missing_session", 0):
        hints.append("계측 누락")
    hint_text = ", ".join(hints) if hints else "일반 품질"
    return (
        "당신은 Langfuse 운영 분석 어시스턴트입니다. 사용자 요청을 해결하고, "
        f"{hint_text} 개선 관점에서 실행 가능한 제안을 3개 이내로 제공합니다."
    )


def _rewrite_prompt_candidates(baseline_prompt: str, metrics: dict, trace) -> list[dict]:
    trace_name = str(_get(trace, "name", "") or "target-trace").strip()
    high_error = metrics.get("error_rate", 0.0) >= 0.1
    slow_latency = (metrics.get("avg_latency_ms") or 0) >= 3000
    has_instrumentation_gap = metrics.get("missing_user", 0) > 0 or metrics.get("missing_session", 0) > 0

    verification_line = "- 불확실한 정보는 추측하지 말고 `확인 필요`로 표기한다." if high_error else "- 답변 핵심 근거를 2개 이내로 제시한다."
    latency_line = "- 도구 호출은 최대 2회, 결과가 충분하면 즉시 종료한다." if slow_latency else "- 필요한 경우에만 도구를 호출하고 중복 호출을 피한다."
    instrumentation_line = (
        "- 결과에 user_id/session_id/trace_id 기반 관측 포인트를 1줄 포함한다."
        if has_instrumentation_gap
        else "- 결과에 추적 가능한 근거(trace 또는 observation)를 1줄 포함한다."
    )

    candidate_a = "\n".join(
        [
            "[역할]",
            "당신은 Langfuse 품질 분석가입니다.",
            "",
            "[목표]",
            baseline_prompt,
            "",
            "[제약]",
            verification_line,
            latency_line,
            instrumentation_line,
            "",
            "[출력 형식]",
            "1) 요약(2문장 이내)",
            "2) 원인 가설(최대 3개)",
            "3) 즉시 실행 액션(우선순위 순)",
        ]
    )

    candidate_b = "\n".join(
        [
            "작업 대상 trace:",
            f"- {trace_name}",
            "",
            "아래 절차를 반드시 지켜 답변하세요.",
            "1) 요청 의도와 성공 기준을 3줄 이내로 재정의",
            "2) trace/observation 근거를 확인하고 신뢰도(high/medium/low) 표시",
            "3) 개선안을 성능/안정성/비용 축으로 나눠 제시",
            "4) 다음 실험(A/B 테스트) 2개 제안",
            "",
            "원문 요청:",
            baseline_prompt,
        ]
    )

    candidate_c = "\n".join(
        [
            "You are a bilingual Langfuse diagnostics copilot.",
            "Return Korean output with lightweight English labels.",
            "",
            "Policy:",
            "- Facts first, assumptions separated.",
            "- If evidence is weak, mark as `확인 필요`.",
            "- Include a tiny scorecard: reliability / latency / instrumentation (1-5).",
            "",
            "User request:",
            baseline_prompt,
        ]
    )

    return [
        {"id": "A", "strategy": "구조화 템플릿 + 제약 명시", "prompt": candidate_a},
        {"id": "B", "strategy": "절차 중심 추론 + 실험 설계", "prompt": candidate_b},
        {"id": "C", "strategy": "간결 이중언어 정책 + 스코어카드", "prompt": candidate_c},
    ]


def _score_prompt_text(prompt_text: str, metrics: dict) -> dict:
    text = str(prompt_text or "")
    lowered = text.lower()
    length = len(text)

    clarity = 2.2
    if any(tag in text for tag in ("[목표]", "[제약]", "[출력 형식]", "절차", "1)", "2)", "3)")):
        clarity += 1.6
    if "요약" in text and ("액션" in text or "실험" in text):
        clarity += 0.6

    safety = 2.0
    if any(keyword in text for keyword in ("확인 필요", "추측하지", "근거", "신뢰도")):
        safety += 1.8
    if "가설" in text:
        safety += 0.4

    observability = 2.0
    if any(keyword in lowered for keyword in ("trace", "observation", "session_id", "user_id", "instrumentation", "scorecard")):
        observability += 1.8
    if "metadata" in lowered or "log" in lowered:
        observability += 0.4

    if 180 <= length <= 900:
        efficiency = 4.6
    elif 120 <= length < 180 or 900 < length <= 1200:
        efficiency = 3.8
    elif 80 <= length < 120:
        efficiency = 3.2
    else:
        efficiency = 2.4

    if metrics.get("error_rate", 0.0) >= 0.1 and "확인 필요" in text:
        safety += 0.3
    if (metrics.get("avg_latency_ms") or 0) >= 3000 and ("최대 2회" in text or "중복 호출" in text):
        efficiency += 0.3
    if (metrics.get("missing_user", 0) > 0 or metrics.get("missing_session", 0) > 0) and (
        "session_id" in lowered or "user_id" in lowered
    ):
        observability += 0.3

    clarity = round(min(5.0, clarity), 2)
    safety = round(min(5.0, safety), 2)
    observability = round(min(5.0, observability), 2)
    efficiency = round(min(5.0, efficiency), 2)
    total = round((clarity + safety + observability + efficiency) / 4.0, 2)

    return {
        "clarity": clarity,
        "safety": safety,
        "observability": observability,
        "efficiency": efficiency,
        "total": total,
    }


def _score_prompt_candidates(candidates: list[dict], metrics: dict) -> list[dict]:
    rows = []
    for candidate in candidates:
        scores = _score_prompt_text(candidate.get("prompt", ""), metrics)
        rows.append({**candidate, "scores": scores})
    rows.sort(key=lambda item: item.get("scores", {}).get("total", 0.0), reverse=True)
    return rows


def _prompt_delta(base_scores: dict, best_scores: dict) -> dict:
    def _delta(key: str):
        return round((best_scores.get(key, 0.0) - base_scores.get(key, 0.0)), 2)

    return {
        "clarity": _delta("clarity"),
        "safety": _delta("safety"),
        "observability": _delta("observability"),
        "efficiency": _delta("efficiency"),
        "total": _delta("total"),
    }


def _select_prompt_review_model(profile: dict, model_specs: list[dict]) -> dict | None:
    llm_cfg = _profile_llm_config(profile)
    requested = (
        os.getenv("AGENT_PROMPT_REVIEW_MODEL", "").strip()
        or str(llm_cfg.get("prompt_review_model", "")).strip()
    )
    if requested:
        parsed = _parse_model_spec(requested)
        if parsed:
            return parsed
    return model_specs[0] if model_specs else None


def _prompt_review_with_model(profile: dict, baseline_prompt: str, ranked_candidates: list[dict], metrics: dict) -> str | None:
    if not _env_bool("AGENT_PROMPT_REVIEW_USE_MODEL", default=True):
        return None
    model_specs = _comparison_model_specs(profile)
    selected = _select_prompt_review_model(profile, model_specs)
    if not selected:
        return "### Model critique\n- 사용할 모델 설정이 없어 모델 피드백은 생략했습니다."
    label = str(selected.get("alias", selected.get("model", "unknown")))

    compact_candidates = []
    for candidate in ranked_candidates[:3]:
        compact_candidates.append(
            {
                "id": candidate.get("id"),
                "strategy": candidate.get("strategy"),
                "score": candidate.get("scores", {}).get("total"),
                "prompt": str(candidate.get("prompt", ""))[:700],
            }
        )

    payload = {
        "task": "Rank prompt variants for Langfuse analysis assistant. Return concise Korean bullets.",
        "metrics": metrics,
        "baseline_prompt": baseline_prompt[:700],
        "candidates": compact_candidates,
    }
    critique = _invoke_model_with_langchain(profile, selected, payload)
    return "\n".join(
        [
            f"### Model critique ({label})",
            critique,
        ]
    )


def _polly_observability_section(client, trace, metrics: dict) -> str:
    if trace is None:
        return "## Observability deep dive\n- 분석할 trace가 없습니다."

    trace_id = str(_get(trace, "id", ""))
    detailed_trace = trace
    try:
        detailed_trace = _fetch_trace_detail(client, trace_id)
    except Exception:
        detailed_trace = trace

    observations = _get(detailed_trace, "observations", []) or []
    if not observations:
        try:
            observations = _fetch_trace_observations(client, trace_id, limit=200)
        except Exception:
            observations = []

    diag = _trace_diagnostics(detailed_trace, observations=observations)
    recommendations = []
    if diag["error_observations"]:
        recommendations.append("- 오류 status_message가 있는 observation을 우선 재현해 root cause를 고정하세요.")
    if not diag["model_counts"]:
        recommendations.append("- model/model_id 계측을 observation 단위로 남기면 모델별 병목 분석이 쉬워집니다.")
    if metrics.get("avg_latency_ms") and metrics["avg_latency_ms"] > 3000:
        recommendations.append("- 평균 latency가 높습니다. 프롬프트 길이와 tool fan-out을 줄여보세요.")
    if not recommendations:
        recommendations.append("- 현재 trace에서는 명확한 실패 패턴이 없으므로, 고비용 trace 중심으로 샘플링을 확대하세요.")

    payload = {
        "trace_id": str(_get(detailed_trace, "id", "")),
        "trace_name": str(_get(detailed_trace, "name", "")),
        "latency": _get(detailed_trace, "latency"),
        "total_cost": _get(detailed_trace, "total_cost"),
        "diagnostics": diag,
        "recommendations": recommendations,
    }
    return "\n".join(
        [
            "## Observability deep dive (Polly-inspired)",
            "```json",
            json.dumps(payload, ensure_ascii=False, indent=2),
            "```",
        ]
    )


def _polly_prompt_section(profile: dict, trace, metrics: dict, filters: dict, traces: list) -> str:
    baseline_prompt = _resolve_baseline_prompt(filters, trace, metrics)
    baseline_scores = _score_prompt_text(baseline_prompt, metrics)
    ranked = _score_prompt_candidates(_rewrite_prompt_candidates(baseline_prompt, metrics, trace), metrics)
    best = ranked[0] if ranked else None
    if not best:
        return "## Prompt engineering lab\n- 후보 프롬프트를 생성하지 못했습니다."

    delta = _prompt_delta(baseline_scores, best.get("scores", {}))
    score_payload = {
        "baseline": baseline_scores,
        "candidates": [
            {
                "id": candidate.get("id"),
                "strategy": candidate.get("strategy"),
                "scores": candidate.get("scores"),
            }
            for candidate in ranked
        ],
        "recommended": best.get("id"),
        "improvement_delta": delta,
        "sample_size": len(traces),
    }

    lines = [
        "## Prompt engineering lab (Lens, Polly-inspired)",
        f"- baseline_total: {baseline_scores.get('total')}/5",
        f"- recommended_variant: {best.get('id')} ({best.get('strategy')})",
        f"- expected_total_delta: {delta.get('total', 0.0):+}",
        "",
        "### Baseline prompt",
        "```text",
        baseline_prompt,
        "```",
        "",
        "### Candidate scorecard",
        "```json",
        json.dumps(score_payload, ensure_ascii=False, indent=2),
        "```",
        "",
        "### Recommended prompt",
        "```text",
        str(best.get("prompt", "")),
        "```",
        "",
        "### Expected impact (before → after)",
        f"- clarity: {baseline_scores.get('clarity')} → {best.get('scores', {}).get('clarity')}",
        f"- safety: {baseline_scores.get('safety')} → {best.get('scores', {}).get('safety')}",
        f"- observability: {baseline_scores.get('observability')} → {best.get('scores', {}).get('observability')}",
        f"- efficiency: {baseline_scores.get('efficiency')} → {best.get('scores', {}).get('efficiency')}",
    ]

    if metrics.get("total_cost", 0.0) > 1.0:
        lines.append("- 참고: 비용이 높아 max_tokens/컨텍스트 길이 축소 실험을 함께 권장합니다.")

    model_review = _prompt_review_with_model(profile, baseline_prompt, ranked, metrics)
    if model_review:
        lines.extend(["", model_review])

    return "\n".join(lines)


def _polly_evaluation_section(metrics: dict, traces: list) -> str:
    rubric = _rubric_scores(metrics)
    confidence = _confidence_level(metrics)
    cohort = _session_cohort_summary(traces, limit=8)
    regression = _time_regression_summary(traces)
    payload = {
        "rubric_1_to_5": rubric,
        "sample_size": len(traces),
        "confidence": confidence,
        "policy": {
            "pass_overall_min": 3.5,
            "reliability_min": 4.0,
            "instrumentation_min": 4.0,
        },
        "session_cohort_top": cohort,
        "time_regression": regression,
        "next_actions": [
            "실패 trace를 태그(`incident`, `retry`)로 분리해 재현 우선순위 큐 구성",
            "session_id 단위로 사용자 여정 평가셋을 만들고 주간 회귀 점검",
            "score(정확성/만족도) 입력 자동화를 통해 품질 대시보드 고도화",
        ],
    }
    return "\n".join(
        [
            "## Evaluation workflow (Polly-inspired)",
            "```json",
            json.dumps(payload, ensure_ascii=False, indent=2),
            "```",
        ]
    )


def run_tool_pipeline(message: str) -> str:
    profile = load_runtime_profile()
    filters = _parse_filters(message)
    client, cfg, err = _build_langfuse_client(profile)
    if err:
        return (
            "Langfuse trace 분석기를 실행할 수 없습니다.\n"
            f"- reason: {err}\n\n"
            "예시 요청:\n"
            "from=2026-02-01 to=2026-02-25 user_id=demo-user limit=20 metadata.channel=prod compare_models=true"
        )

    try:
        traces = _fetch_traces(client, filters)
    except Exception as exc:
        return f"Langfuse trace 조회 실패: {exc}"

    metrics = _summarize(traces)
    confidence = _confidence_level(metrics)
    opinions = _heuristic_opinions(metrics)
    selected_trace = _select_trace_for_deep_dive(traces, filters.get("trace_id"))

    lines = [
        "## Langfuse Lens query",
        f"- host: {cfg.get('host')}",
        f"- mode: {filters.get('mode')}",
        f"- fetched_traces: {metrics['count']}",
        "",
        "## Filters",
        "```json",
        _format_filters(filters),
        "```",
        "",
        "## Metrics summary",
        f"- unique_users: {metrics['unique_users']}",
        f"- unique_sessions: {metrics['unique_sessions']}",
        f"- error_trace_count: {metrics['error_trace_count']}",
        f"- error_rate: {metrics['error_rate'] * 100:.2f}%",
        f"- avg_latency_ms: {metrics['avg_latency_ms']}",
        f"- max_latency_ms: {metrics['max_latency_ms']}",
        f"- total_cost: {metrics['total_cost']:.6f}",
        f"- avg_score: {metrics['avg_score']}",
        f"- model_counts: {metrics['model_counts']}",
        f"- traces_with_observations: {metrics.get('traces_with_observations', 0)}/{metrics['count']}",
        f"- confidence: {confidence}",
        "",
        "## Heuristic opinions",
        *opinions,
        "",
        "## Trace preview",
        "```json",
        json.dumps(_trace_preview(traces, limit=min(5, metrics['count'] or 5)), ensure_ascii=False, indent=2),
        "```",
    ]

    mode = filters.get("mode", "overview")
    if mode == "observability":
        lines.extend(["", _polly_observability_section(client, selected_trace, metrics)])
    elif mode == "prompt_engineering":
        lines.extend(["", _polly_prompt_section(profile, selected_trace, metrics, filters, traces)])
    elif mode == "evaluation":
        lines.extend(["", _polly_evaluation_section(metrics, traces)])

    if filters.get("compare_models"):
        lines.extend(["", _model_comparison_section(profile, metrics, traces, filters)])

    return "\n".join(lines)
