"""Report generator for daily/weekly/monthly user activity reports.

Daily reports query Langfuse traces directly.
Weekly and monthly reports aggregate from child reports stored in DB.
Session activity summaries are generated via LLM for performance evaluation.
"""
from __future__ import annotations

import json
import logging
import os
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta, timezone
from statistics import mean
from typing import Any

from src.tools.helpers import _get
from src.tools.langfuse_fetch import _fetch_traces, _fetch_trace_observations
from src.tools.analysis import _summarize, _session_cohort_summary
from src.user_store import list_user_reports

log = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# LLM-powered session activity summarization
# ---------------------------------------------------------------------------

SESSION_SUMMARY_SYSTEM = (
    "당신은 업무 성과 분석 전문가입니다. "
    "AI 에이전트 사용 세션의 대화 턴(입력/출력)을 분석하여 "
    "해당 세션에서 사용자가 수행한 활동을 요약합니다.\n\n"
    "다음 관점에서 분석하세요:\n"
    "1. **주요 활동**: 사용자가 요청한 핵심 작업과 목적\n"
    "2. **작업 복잡도**: 단순 질의인지, 복잡한 분석·개발·기획 작업인지\n"
    "3. **활용 수준**: AI 도구를 얼마나 깊이 있게 활용했는지\n"
    "4. **성과**: 도출된 결과물, 해결된 문제, 의사결정 내용\n\n"
    "한국어로 3~5문장으로 요약하세요. 인사 평가(인사고과) 자료로 활용됩니다."
)

DAILY_ANALYSIS_SYSTEM = (
    "당신은 일간 활동 종합 분석가입니다. "
    "하루 동안의 AI 에이전트 사용 세션 요약들과 통계 데이터를 분석하여 "
    "종합적인 일간 활동 분석을 제공합니다.\n\n"
    "다음 관점에서 분석하세요:\n"
    "1. **주요 활동 흐름과 업무 패턴**: 하루 동안 어떤 업무를 수행했는지 흐름 파악\n"
    "2. **AI 활용 깊이와 효율성**: 단순 질의 vs 복잡한 분석·개발 작업 비율\n"
    "3. **에러/비용 이슈 진단**: 에러율이나 비용 이상치가 있다면 원인 추정\n"
    "4. **모델별 사용 패턴**: 어떤 모델을 주로 사용했고, 적절했는지 평가\n"
    "5. **개선 제안**: 더 효율적인 AI 활용을 위한 구체적 제안\n\n"
    "한국어로 5~10문장 서술형으로 분석하세요."
)

WEEKLY_ANALYSIS_SYSTEM = (
    "당신은 주간 트렌드 분석가입니다. "
    "7일간의 일간 분석 결과들과 집계 통계를 기반으로 "
    "주간 활동 트렌드를 분석합니다.\n\n"
    "다음 관점에서 분석하세요:\n"
    "1. **주간 업무 트렌드와 패턴 변화**: 요일별 활동 패턴, 업무 집중도 변화\n"
    "2. **일별 활동 편차와 특이사항**: 특별히 활발하거나 저조한 날의 원인 분석\n"
    "3. **AI 활용 성숙도 진전**: 주간 동안 활용 수준이 발전했는지 평가\n"
    "4. **비용/효율 추세**: 비용 대비 생산성 변화 추이\n"
    "5. **다음 주 개선 권고**: 구체적이고 실행 가능한 개선 사항\n\n"
    "한국어로 8~15문장 서술형으로 분석하세요."
)

MONTHLY_ANALYSIS_SYSTEM = (
    "당신은 월간 전략 분석가입니다. "
    "주간 분석 결과들과 월간 집계 통계를 기반으로 "
    "월간 AI 활용 전략을 평가합니다.\n\n"
    "다음 관점에서 분석하세요:\n"
    "1. **월간 AI 활용 전략 평가**: 전체적인 AI 도구 활용 방향성과 성과\n"
    "2. **주간별 성과 추이**: 주차별 성과 변화와 발전 양상\n"
    "3. **비용 대비 생산성 분석**: 투입 비용 대비 산출 효과 평가\n"
    "4. **장기 활용 패턴과 성숙도**: AI 활용 습관의 고도화 정도\n"
    "5. **다음 달 전략 제안**: 장기적 관점에서의 AI 활용 전략 권고\n\n"
    "한국어로 10~20문장 서술형으로 분석하세요."
)

_MAX_TURN_INPUT_CHARS = 500
_MAX_TURN_OUTPUT_CHARS = 500
_MAX_TURNS_PER_SESSION = 30


def _resolve_summary_llm() -> dict[str, str] | None:
    """Resolve LLM provider/model for session summary generation."""
    # 1) 전용 보고서 요약 모델 (최우선)
    report_model = os.getenv("AGENT_REPORT_SUMMARY_MODEL", "").strip()
    if report_model:
        model_str = report_model
    else:
        # 2) 기존 fallback 로직
        model_str = os.getenv("AGENT_DEEP_MODEL", "").strip()
    if not model_str:
        if os.getenv("OPENAI_API_KEY", "").strip():
            model_str = "openai:" + os.getenv("AGENT_OPENAI_MODEL", "gpt-4o-mini")
        elif os.getenv("ANTHROPIC_API_KEY", "").strip():
            model_str = "anthropic:" + os.getenv("AGENT_ANTHROPIC_MODEL", "claude-3-5-sonnet-latest")
        elif os.getenv("GOOGLE_API_KEY", "").strip() or os.getenv("GEMINI_API_KEY", "").strip():
            model_str = "google_genai:" + os.getenv("AGENT_GEMINI_MODEL", "gemini-2.0-flash")
        elif os.getenv("OPENROUTER_API_KEY", "").strip():
            model_str = "openrouter:" + os.getenv("OPENROUTER_MODEL", "openai/gpt-4o-mini")
    if not model_str:
        return None
    if ":" in model_str:
        provider, model = model_str.split(":", 1)
    else:
        provider, model = "openai", model_str
    return {"provider": provider, "model": model}


def _truncate(text: str, max_len: int) -> str:
    if len(text) <= max_len:
        return text
    return text[:max_len] + "..."


def _extract_session_turns(
    traces: list, session_id: str, max_turns: int = _MAX_TURNS_PER_SESSION,
) -> list[dict]:
    """Extract input/output turns from traces for a specific session."""
    session_traces = [
        t for t in traces
        if str(_get(t, "session_id") or "") == session_id
    ]
    session_traces.sort(key=lambda t: str(_get(t, "timestamp") or ""))

    turns: list[dict] = []
    for t in session_traces[:max_turns]:
        inp = _get(t, "input")
        out = _get(t, "output")
        inp_str = _truncate(json.dumps(inp, ensure_ascii=False), _MAX_TURN_INPUT_CHARS) if inp else ""
        out_str = _truncate(json.dumps(out, ensure_ascii=False), _MAX_TURN_OUTPUT_CHARS) if out else ""
        if inp_str or out_str:
            turns.append({
                "name": str(_get(t, "name") or ""),
                "input": inp_str,
                "output": out_str,
            })
    return turns


def _build_summary_llm():
    """Build a reusable LLM instance for session summary generation. Returns None if unavailable."""
    llm_spec = _resolve_summary_llm()
    if not llm_spec:
        return None

    try:
        from langchain.chat_models import init_chat_model
    except Exception as exc:
        log.warning("langchain import failed: %s", exc)
        return None

    provider = llm_spec["provider"]
    model = llm_spec["model"]
    model_provider = provider
    model_kwargs: dict[str, Any] = {
        "temperature": 0.3,
        "timeout": 60,
        "max_retries": 2,
    }

    if provider == "openrouter":
        model_provider = "openai"
        model_kwargs["api_key"] = os.getenv("OPENROUTER_API_KEY", "")
        model_kwargs["base_url"] = (
            os.getenv("OPENROUTER_BASE_URL", "").strip()
            or "https://openrouter.ai/api/v1"
        )
    elif provider == "google_genai":
        gkey = os.getenv("GOOGLE_API_KEY", "").strip() or os.getenv("GEMINI_API_KEY", "").strip()
        if gkey:
            model_kwargs["api_key"] = gkey

    try:
        return init_chat_model(model=model, model_provider=model_provider, **model_kwargs)
    except Exception as exc:
        log.warning("Failed to init summary LLM (%s:%s): %s", provider, model, exc)
        return None


def _summarize_session_activity(llm, turns: list[dict], session_id: str) -> str:
    """Generate LLM-powered activity summary for a single session using a pre-built LLM."""
    from langchain_core.messages import HumanMessage, SystemMessage

    lines: list[str] = [f"세션 ID: {session_id}", f"총 {len(turns)}개 턴", ""]
    for i, t in enumerate(turns, 1):
        lines.append(f"[턴 {i}] {t.get('name', '')}")
        if t.get("input"):
            lines.append(f"  입력: {t['input']}")
        if t.get("output"):
            lines.append(f"  출력: {t['output']}")
        lines.append("")

    user_message = "\n".join(lines)

    try:
        response = llm.invoke([
            SystemMessage(content=SESSION_SUMMARY_SYSTEM),
            HumanMessage(content=user_message),
        ])
        text = str(getattr(response, "content", "") or "").strip()
        return text if text else "(요약 생성 실패 — 빈 응답)"
    except Exception as exc:
        log.warning("Session summary LLM failed for %s: %s", session_id, exc)
        return "(요약 생성 실패)"


_FUTURE_TIMEOUT_SECS = 90


def _enrich_sessions_with_summaries(
    sessions: list[dict], traces: list, max_sessions: int = 20,
) -> list[dict]:
    """Enrich session entries with LLM-generated activity summaries (parallel)."""
    llm = _build_summary_llm()
    if not llm:
        for sess in sessions:
            sess.setdefault("activity_summary", "(LLM 미설정)")
        return sessions

    to_summarize: list[tuple[dict, list, str]] = []
    for sess in sessions[:max_sessions]:
        sid = sess.get("session_id", "")
        if not sid or sid == "unknown":
            sess["activity_summary"] = "(세션 ID 없음)"
            continue
        turns = _extract_session_turns(traces, sid)
        if not turns:
            sess["activity_summary"] = "(대화 턴 없음)"
            continue
        to_summarize.append((sess, turns, sid))

    # Mark sessions beyond max_sessions explicitly
    for sess in sessions[max_sessions:]:
        sess.setdefault("activity_summary", "")

    if not to_summarize:
        return sessions

    max_workers = min(len(to_summarize), 5)
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_map: dict[Any, dict] = {}
        for sess, turns, sid in to_summarize:
            future = executor.submit(_summarize_session_activity, llm, turns, sid)
            future_map[future] = sess

        for future in as_completed(future_map):
            sess = future_map[future]
            try:
                sess["activity_summary"] = future.result(timeout=_FUTURE_TIMEOUT_SECS)
            except TimeoutError:
                log.warning("Session summary timed out after %ds", _FUTURE_TIMEOUT_SECS)
                sess["activity_summary"] = "(요약 시간 초과)"
            except Exception as exc:
                log.warning("Session summary future failed: %s", exc)
                sess["activity_summary"] = "(요약 생성 실패)"

    return sessions


def _generate_daily_analysis(
    sessions: list[dict], summary: dict, model_usage: dict,
) -> str:
    """Generate LLM-powered daily comprehensive analysis."""
    llm = _build_summary_llm()
    if not llm:
        return ""

    from langchain_core.messages import HumanMessage, SystemMessage

    # Build context from session summaries + statistics
    lines: list[str] = ["=== 일간 통계 ==="]
    lines.append(f"총 트레이스: {summary.get('count', 0)}")
    lines.append(f"총 비용: ${summary.get('total_cost', 0.0):.4f}")
    lines.append(f"에러율: {(summary.get('error_rate', 0) * 100):.1f}%")
    avg_lat = summary.get("avg_latency_ms")
    if avg_lat is not None:
        lines.append(f"평균 지연: {avg_lat:.0f}ms")
    lines.append("")

    if model_usage:
        lines.append("=== 모델별 사용량 ===")
        for model, data in model_usage.items():
            if isinstance(data, dict):
                lines.append(
                    f"- {model}: {data.get('call_count', 0)}회 호출, "
                    f"토큰 {data.get('total_tokens', 0):,}, "
                    f"비용 ${data.get('total_cost', 0.0):.4f}"
                )
        lines.append("")

    lines.append("=== 세션별 활동 요약 ===")
    for sess in sessions:
        sid = sess.get("session_id", "unknown")
        act = sess.get("activity_summary", "")
        if act and act not in ("(LLM 미설정)", "(세션 ID 없음)", "(대화 턴 없음)"):
            lines.append(f"[{sid}] {act}")
            lines.append("")

    try:
        response = llm.invoke([
            SystemMessage(content=DAILY_ANALYSIS_SYSTEM),
            HumanMessage(content="\n".join(lines)),
        ])
        return str(getattr(response, "content", "") or "").strip()
    except Exception as exc:
        log.warning("Daily analysis LLM failed: %s", exc)
        return ""


def _generate_weekly_analysis(
    daily_analyses: list[str], merged_summary: dict, merged_usage: dict,
) -> str:
    """Generate LLM-powered weekly trend analysis from daily analyses."""
    llm = _build_summary_llm()
    if not llm:
        return ""

    from langchain_core.messages import HumanMessage, SystemMessage

    lines: list[str] = ["=== 주간 집계 통계 ==="]
    lines.append(f"총 트레이스: {merged_summary.get('count', 0)}")
    lines.append(f"총 비용: ${merged_summary.get('total_cost', 0.0):.4f}")
    lines.append(f"에러율: {(merged_summary.get('error_rate', 0) * 100):.1f}%")
    lines.append("")

    if merged_usage:
        lines.append("=== 주간 모델별 사용량 ===")
        for model, data in merged_usage.items():
            if isinstance(data, dict):
                lines.append(
                    f"- {model}: {data.get('call_count', 0)}회, "
                    f"비용 ${data.get('total_cost', 0.0):.4f}"
                )
        lines.append("")

    lines.append("=== 일간 분석 모음 ===")
    for i, analysis in enumerate(daily_analyses, 1):
        if analysis:
            lines.append(f"--- Day {i} ---")
            lines.append(analysis)
            lines.append("")

    try:
        response = llm.invoke([
            SystemMessage(content=WEEKLY_ANALYSIS_SYSTEM),
            HumanMessage(content="\n".join(lines)),
        ])
        return str(getattr(response, "content", "") or "").strip()
    except Exception as exc:
        log.warning("Weekly analysis LLM failed: %s", exc)
        return ""


def _generate_monthly_analysis(
    weekly_analyses: list[str], merged_summary: dict, merged_usage: dict,
) -> str:
    """Generate LLM-powered monthly strategic analysis from weekly analyses."""
    llm = _build_summary_llm()
    if not llm:
        return ""

    from langchain_core.messages import HumanMessage, SystemMessage

    lines: list[str] = ["=== 월간 집계 통계 ==="]
    lines.append(f"총 트레이스: {merged_summary.get('count', 0)}")
    lines.append(f"총 비용: ${merged_summary.get('total_cost', 0.0):.4f}")
    lines.append(f"에러율: {(merged_summary.get('error_rate', 0) * 100):.1f}%")
    lines.append("")

    if merged_usage:
        lines.append("=== 월간 모델별 사용량 ===")
        for model, data in merged_usage.items():
            if isinstance(data, dict):
                lines.append(
                    f"- {model}: {data.get('call_count', 0)}회, "
                    f"비용 ${data.get('total_cost', 0.0):.4f}"
                )
        lines.append("")

    lines.append("=== 주간 분석 모음 ===")
    for i, analysis in enumerate(weekly_analyses, 1):
        if analysis:
            lines.append(f"--- Week {i} ---")
            lines.append(analysis)
            lines.append("")

    try:
        response = llm.invoke([
            SystemMessage(content=MONTHLY_ANALYSIS_SYSTEM),
            HumanMessage(content="\n".join(lines)),
        ])
        return str(getattr(response, "content", "") or "").strip()
    except Exception as exc:
        log.warning("Monthly analysis LLM failed: %s", exc)
        return ""


def _parse_date(date_str: str) -> datetime:
    return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)


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


# ---------------------------------------------------------------------------
# Model usage collection from observations
# ---------------------------------------------------------------------------

def _collect_model_usage_from_observations(client, traces: list) -> dict[str, Any]:
    """Collect model usage (token counts, costs) per model from trace observations."""
    usage: dict[str, dict[str, Any]] = {}

    for trace in traces:
        trace_id = str(_get(trace, "id", ""))
        if not trace_id:
            continue

        # Try embedded observations first, fetch if absent
        observations = _get(trace, "observations", []) or []
        if not observations:
            try:
                observations = _fetch_trace_observations(client, trace_id, limit=200)
            except Exception:
                observations = []

        for obs in observations:
            model = str(
                _get(obs, "model", "") or _get(obs, "model_id", "") or ""
            ).strip()
            if not model:
                continue

            entry = usage.setdefault(model, {
                "model": model,
                "call_count": 0,
                "input_tokens": 0,
                "output_tokens": 0,
                "total_tokens": 0,
                "total_cost": 0.0,
            })
            entry["call_count"] += 1

            obs_usage = _get(obs, "usage", None)
            if isinstance(obs_usage, dict):
                entry["input_tokens"] += int(obs_usage.get("input", 0) or obs_usage.get("input_tokens", 0) or 0)
                entry["output_tokens"] += int(obs_usage.get("output", 0) or obs_usage.get("output_tokens", 0) or 0)
                entry["total_tokens"] += int(obs_usage.get("total", 0) or obs_usage.get("total_tokens", 0) or 0)
            elif hasattr(obs_usage, "input"):
                entry["input_tokens"] += int(getattr(obs_usage, "input", 0) or 0)
                entry["output_tokens"] += int(getattr(obs_usage, "output", 0) or 0)
                entry["total_tokens"] += int(getattr(obs_usage, "total", 0) or 0)

            obs_cost = _to_number(_get(obs, "total_cost") or _get(obs, "calculated_total_cost"))
            if obs_cost is not None:
                entry["total_cost"] += obs_cost

    return dict(sorted(usage.items(), key=lambda item: item[1]["call_count"], reverse=True))


# ---------------------------------------------------------------------------
# Timeline builder (hourly distribution for daily)
# ---------------------------------------------------------------------------

def _build_timeline(traces: list) -> list[dict[str, Any]]:
    """Build hourly distribution of traces for a single day."""
    buckets: dict[int, dict[str, Any]] = {}
    for hour in range(24):
        buckets[hour] = {"hour": hour, "trace_count": 0, "total_cost": 0.0}

    for trace in traces:
        ts = _get(trace, "timestamp")
        if ts is None:
            continue
        if isinstance(ts, datetime):
            dt = ts
        else:
            try:
                dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
            except Exception:
                continue
        hour = dt.hour
        buckets[hour]["trace_count"] += 1
        cost = _to_number(_get(trace, "total_cost"))
        if cost is not None:
            buckets[hour]["total_cost"] += cost

    return [buckets[h] for h in range(24)]


# ---------------------------------------------------------------------------
# Daily report — queries Langfuse directly
# ---------------------------------------------------------------------------

def generate_daily_report(
    client,
    target_user_id: str,
    date_str: str,
    project_id: int | None = None,
) -> dict[str, Any]:
    """Generate a daily report by fetching traces from Langfuse.

    Returns a dict with: summary, sessions, model_usage, timeline,
    trace_count, total_cost.
    """
    dt = _parse_date(date_str)
    from_ts = dt.isoformat()
    to_ts = (dt + timedelta(days=1)).isoformat()

    filters = {
        "user_id": target_user_id,
        "from": from_ts,
        "to": to_ts,
        "limit": 100,
    }

    traces = _fetch_traces(client, filters)

    summary = _summarize(traces)
    sessions = _session_cohort_summary(traces, limit=20)
    model_usage = _collect_model_usage_from_observations(client, traces)
    timeline = _build_timeline(traces)

    # Enrich sessions with LLM-generated activity summaries
    sessions = _enrich_sessions_with_summaries(sessions, traces)

    # Generate daily comprehensive LLM analysis
    llm_analysis = _generate_daily_analysis(sessions, summary, model_usage)

    return {
        "summary": summary,
        "sessions": sessions,
        "model_usage": model_usage,
        "timeline": timeline,
        "trace_count": summary.get("count", 0),
        "total_cost": summary.get("total_cost", 0.0),
        "llm_analysis": llm_analysis,
    }


# ---------------------------------------------------------------------------
# Merge helpers for weekly / monthly aggregation
# ---------------------------------------------------------------------------

def _merge_summaries(summaries: list[dict]) -> dict[str, Any]:
    if not summaries:
        return {}

    total_count = sum(s.get("count", 0) for s in summaries)
    all_users: set[str] = set()
    all_sessions: set[str] = set()
    total_cost = 0.0
    error_count = 0
    latencies: list[float] = []

    model_counts: dict[str, int] = defaultdict(int)

    for s in summaries:
        total_cost += float(s.get("total_cost", 0.0) or 0)
        error_count += int(s.get("error_trace_count", 0) or 0)
        if s.get("avg_latency_ms") is not None and s.get("count", 0) > 0:
            latencies.extend([float(s["avg_latency_ms"])] * int(s["count"]))
        for model, cnt in (s.get("model_counts") or {}).items():
            model_counts[model] += int(cnt)

    return {
        "count": total_count,
        "total_cost": total_cost,
        "error_trace_count": error_count,
        "error_rate": (error_count / total_count) if total_count else 0.0,
        "avg_latency_ms": mean(latencies) if latencies else None,
        "model_counts": dict(sorted(model_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
    }


def _merge_sessions(sessions_list: list[list]) -> list[dict[str, Any]]:
    merged: dict[str, dict[str, Any]] = {}
    for sessions in sessions_list:
        for sess in sessions:
            sid = str(sess.get("session_id", "unknown"))
            entry = merged.setdefault(sid, {
                "session_id": sid,
                "trace_count": 0,
                "error_count": 0,
                "total_cost": 0.0,
                "latencies": [],
                "_activity_summaries": [],
            })
            entry["trace_count"] += int(sess.get("trace_count", 0) or 0)
            entry["error_count"] += int(sess.get("error_count", 0) or 0)
            entry["total_cost"] += float(sess.get("total_cost", 0.0) or 0)
            if sess.get("avg_latency_ms") is not None:
                entry["latencies"].append(float(sess["avg_latency_ms"]))
            if sess.get("activity_summary"):
                entry["_activity_summaries"].append(str(sess["activity_summary"]))

    rows: list[dict[str, Any]] = []
    for payload in merged.values():
        lat = payload.pop("latencies", [])
        summaries = payload.pop("_activity_summaries", [])
        payload["avg_latency_ms"] = mean(lat) if lat else None
        payload["error_rate"] = (
            payload["error_count"] / payload["trace_count"]
            if payload["trace_count"]
            else 0.0
        )
        payload["activity_summary"] = "\n---\n".join(summaries) if summaries else None
        rows.append(payload)

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


def _merge_model_usage(usages: list[dict]) -> dict[str, Any]:
    merged: dict[str, dict[str, Any]] = {}
    for usage in usages:
        for model, data in usage.items():
            if not isinstance(data, dict):
                continue
            entry = merged.setdefault(model, {
                "model": model,
                "call_count": 0,
                "input_tokens": 0,
                "output_tokens": 0,
                "total_tokens": 0,
                "total_cost": 0.0,
            })
            entry["call_count"] += int(data.get("call_count", 0) or 0)
            entry["input_tokens"] += int(data.get("input_tokens", 0) or 0)
            entry["output_tokens"] += int(data.get("output_tokens", 0) or 0)
            entry["total_tokens"] += int(data.get("total_tokens", 0) or 0)
            entry["total_cost"] += float(data.get("total_cost", 0.0) or 0)

    return dict(sorted(merged.items(), key=lambda x: x[1]["call_count"], reverse=True))


def _merge_timeline(timelines: list[list]) -> list[dict[str, Any]]:
    """Merge timelines into per-date summary entries."""
    daily: list[dict[str, Any]] = []
    for idx, tl in enumerate(timelines):
        total_traces = sum(e.get("trace_count", 0) for e in tl)
        total_cost = sum(e.get("total_cost", 0.0) for e in tl)
        daily.append({
            "day_index": idx,
            "trace_count": total_traces,
            "total_cost": total_cost,
        })
    return daily


# ---------------------------------------------------------------------------
# Weekly report — aggregates daily reports from DB
# ---------------------------------------------------------------------------

def generate_weekly_report(
    owner_id: str,
    project_id: int | None,
    target_user_id: str,
    week_start: str,
) -> dict[str, Any]:
    """Generate a weekly report by aggregating 7 daily reports from DB."""
    dt_start = _parse_date(week_start)
    dt_end = dt_start + timedelta(days=7)

    filtered = list_user_reports(
        owner_id=owner_id,
        target_user_id=target_user_id,
        period_type="daily",
        project_id=project_id,
        limit=7,
        period_start_gte=week_start,
        period_start_lt=dt_end.strftime("%Y-%m-%d"),
    )

    summaries = [r["summary"] for r in filtered if isinstance(r.get("summary"), dict)]
    sessions_list = [r["sessions"] for r in filtered if isinstance(r.get("sessions"), list)]
    usages = [r["model_usage"] for r in filtered if isinstance(r.get("model_usage"), dict)]
    timelines = [r["timeline"] for r in filtered if isinstance(r.get("timeline"), list)]

    merged_summary = _merge_summaries(summaries)
    merged_sessions = _merge_sessions(sessions_list)
    merged_usage = _merge_model_usage(usages)
    merged_timeline = _merge_timeline(timelines)

    # Collect daily LLM analyses for weekly synthesis
    daily_analyses = [r.get("llm_analysis", "") for r in filtered]
    llm_analysis = _generate_weekly_analysis(daily_analyses, merged_summary, merged_usage)

    return {
        "summary": merged_summary,
        "sessions": merged_sessions,
        "model_usage": merged_usage,
        "timeline": merged_timeline,
        "trace_count": merged_summary.get("count", 0),
        "total_cost": merged_summary.get("total_cost", 0.0),
        "llm_analysis": llm_analysis,
    }


# ---------------------------------------------------------------------------
# Monthly report — aggregates weekly reports from DB
# ---------------------------------------------------------------------------

def generate_monthly_report(
    owner_id: str,
    project_id: int | None,
    target_user_id: str,
    month_start: str,
) -> dict[str, Any]:
    """Generate a monthly report by aggregating weekly reports from DB."""
    dt_start = _parse_date(month_start)
    # Compute month end
    if dt_start.month == 12:
        dt_end = dt_start.replace(year=dt_start.year + 1, month=1)
    else:
        dt_end = dt_start.replace(month=dt_start.month + 1)

    filtered = list_user_reports(
        owner_id=owner_id,
        target_user_id=target_user_id,
        period_type="weekly",
        project_id=project_id,
        limit=5,
        period_start_gte=month_start,
        period_start_lt=dt_end.strftime("%Y-%m-%d"),
    )

    summaries = [r["summary"] for r in filtered if isinstance(r.get("summary"), dict)]
    sessions_list = [r["sessions"] for r in filtered if isinstance(r.get("sessions"), list)]
    usages = [r["model_usage"] for r in filtered if isinstance(r.get("model_usage"), dict)]
    timelines = [r["timeline"] for r in filtered if isinstance(r.get("timeline"), list)]

    merged_summary = _merge_summaries(summaries)
    merged_sessions = _merge_sessions(sessions_list)
    merged_usage = _merge_model_usage(usages)
    merged_timeline = _merge_timeline(timelines)

    # Collect weekly LLM analyses for monthly synthesis
    weekly_analyses = [r.get("llm_analysis", "") for r in filtered]
    llm_analysis = _generate_monthly_analysis(weekly_analyses, merged_summary, merged_usage)

    return {
        "summary": merged_summary,
        "sessions": merged_sessions,
        "model_usage": merged_usage,
        "timeline": merged_timeline,
        "trace_count": merged_summary.get("count", 0),
        "total_cost": merged_summary.get("total_cost", 0.0),
        "llm_analysis": llm_analysis,
    }
