"""
SWE-bench Compression Evaluation
==================================
Measures litellm.compress() impact on SWE-bench Lite problems.

Each instance includes ~27k tokens of BM25-retrieved repo context — large
enough to meaningfully stress compression without requiring Docker or GitHub
API calls.

Usage:
    python tests/eval_swe_bench.py --model gpt-4o --problems 10
    python tests/eval_swe_bench.py --model claude-sonnet-4-20250514 --problems 25
    python tests/eval_swe_bench.py --model gpt-4o-mini --problems 50 --compression-trigger 8000

Requires:
    pip install datasets

Proxy eval metrics (no Docker / test runner required):
  - has_diff:          model produced a valid unified diff
  - file_overlap:      fraction of gold-patch files present in generated patch
  - exact_file_match:  generated patch touches exactly the same files as gold patch

Full SWE-bench pass rate (FAIL_TO_PASS) requires the official evaluation
harness with Docker — not in scope here. The proxy metrics are a lightweight
signal for whether compression degrades patch quality.
"""

import argparse
import json
import os
import re
import statistics
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Optional

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import litellm  # noqa: E402
from litellm.compression import compress as litellm_compress  # noqa: E402
from litellm.types.utils import CallTypes  # noqa: E402

# ---------------------------------------------------------------------------
# Prompts
# ---------------------------------------------------------------------------

SYSTEM_MSG = (
    "You are an expert software engineer resolving GitHub issues. "
    "You will be given an issue description and relevant source files. "
    "Produce a minimal unified diff patch that fixes the issue. "
    "Your response must contain ONLY the patch in unified diff format. "
    "Start with `diff --git a/path b/path`, then `---`, `+++`, and "
    "`@@` hunks. Do NOT include any explanation, commentary, or markdown "
    "fences — just the raw diff text."
)


# ---------------------------------------------------------------------------
# Dataset loading
# ---------------------------------------------------------------------------


def _load_via_datasets(n: int, split: str) -> list[dict]:
    """Load via the HuggingFace `datasets` library (preferred if available)."""
    from datasets import load_dataset

    ds = load_dataset("princeton-nlp/SWE-bench_Lite_bm25_27K", split=split)
    problems = []
    for i, item in enumerate(ds):
        if n > 0 and i >= n:
            break
        problems.append(dict(item))
    return problems


def _load_via_api(n: int, split: str) -> list[dict]:
    """Fallback: fetch rows directly from the HuggingFace dataset API (no deps).

    The API returns at most 100 rows per request, so we paginate.
    """
    import json
    import urllib.request

    # 0 means "all" — SWE-bench Lite has 300 test instances
    target = n if n > 0 else 300
    page_size = 100
    all_rows: list[dict] = []

    for offset in range(0, target, page_size):
        length = min(page_size, target - offset)
        url = (
            "https://datasets-server.huggingface.co/rows"
            "?dataset=princeton-nlp/SWE-bench_Lite_bm25_27K"
            f"&config=default&split={split}&offset={offset}&length={length}"
        )
        req = urllib.request.Request(url, headers={"User-Agent": "litellm-eval"})
        with urllib.request.urlopen(req, timeout=60) as resp:
            data = json.loads(resp.read().decode())
        rows = [row["row"] for row in data["rows"]]
        all_rows.extend(rows)
        if len(rows) < length:
            break  # no more data

    return all_rows


def load_problems(n: int = 10, split: str = "test") -> list[dict]:
    """Load n problems from princeton-nlp/SWE-bench_Lite_bm25_27K."""
    print("Loading SWE-bench_Lite_bm25_27K ...", flush=True)

    # Try the HuggingFace API first — it's pure HTTP with no native deps,
    # so it never triggers pyarrow/numpy binary incompatibilities that can
    # poison the process.  Fall back to the `datasets` library only if the
    # API call fails.
    try:
        problems = _load_via_api(n, split)
    except Exception:
        try:
            problems = _load_via_datasets(n, split)
        except Exception as e:
            print(f"ERROR: Could not load dataset ({type(e).__name__}: {e})")
            sys.exit(1)

    print(f"Loaded {len(problems)} problems.\n")
    return problems


# ---------------------------------------------------------------------------
# Message construction
# ---------------------------------------------------------------------------


def build_messages(instance: dict) -> list[dict]:
    """
    Build the message list for a SWE-bench instance.

    Structure:
      - system: instruction to produce a patch
      - user: problem statement + hints (the issue)
      - user: retrieved repo context (~27k tokens, the thing we compress)
      - user: final instruction
    """
    issue = instance["problem_statement"]
    hints = instance.get("hints_text", "").strip()
    context = instance["text"]  # BM25-retrieved file contents

    issue_content = f"## GitHub Issue\n\n{issue}"
    if hints:
        issue_content += f"\n\n## Hints\n\n{hints}"

    return [
        {"role": "system", "content": SYSTEM_MSG},
        {"role": "user", "content": issue_content},
        {
            "role": "user",
            "content": f"## Relevant source files\n\n{context}",
        },
        {
            "role": "user",
            "content": (
                "Based on the issue and source files above, produce a minimal "
                "unified diff patch. Output only the patch."
            ),
        },
    ]


# ---------------------------------------------------------------------------
# Patch helpers
# ---------------------------------------------------------------------------


def parse_patch_files(patch: str) -> set[str]:
    """Extract modified file paths from a unified diff.

    Tries `diff --git a/path b/path` first, then falls back to
    `--- a/path` lines for diffs that omit the git header.
    """
    files = set(re.findall(r"^diff --git a/(.*?) b/", patch, re.MULTILINE))
    if not files:
        # Fallback: extract from --- a/path lines
        files = set(re.findall(r"^--- a/(.+)", patch, re.MULTILINE))
    return files


def extract_patch(text: str) -> str:
    """Pull the diff out of an LLM response."""
    # Prefer fenced code block
    m = re.search(r"```(?:diff|patch)?\n(.*?)```", text, re.DOTALL)
    if m:
        return m.group(1).strip()
    # Fall back to first `diff --git` line
    idx = text.find("diff --git")
    if idx != -1:
        return text[idx:].strip()
    return text.strip()


def is_valid_diff(patch: str) -> bool:
    return bool(
        re.search(r"^@@.*@@", patch, re.MULTILINE) and "---" in patch and "+++" in patch
    )


# ---------------------------------------------------------------------------
# Proxy evaluation
# ---------------------------------------------------------------------------


def _parse_hunk_line_ranges(patch: str) -> dict[str, list[tuple[int, int]]]:
    """Parse a unified diff into {filepath: [(start, end), ...]} for modified line ranges."""
    current_file = None
    ranges: dict[str, list[tuple[int, int]]] = {}
    for line in patch.split("\n"):
        m = re.match(r"^diff --git a/(.*?) b/", line)
        if m:
            current_file = m.group(1)
            if current_file not in ranges:
                ranges[current_file] = []
            continue
        if not current_file:
            m2 = re.match(r"^--- a/(.+)", line)
            if m2:
                current_file = m2.group(1)
                if current_file not in ranges:
                    ranges[current_file] = []
                continue
        m3 = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line)
        if m3 and current_file:
            start = int(m3.group(1))
            length = int(m3.group(2) or "1")
            ranges[current_file].append((start, start + length))
    return ranges


def _extract_changed_lines(patch: str) -> set[str]:
    """Extract the actual added/removed lines (stripped) from a diff."""
    lines = set()
    for line in patch.split("\n"):
        if line.startswith(("+", "-")) and not line.startswith(("+++", "---")):
            stripped = line[1:].strip()
            if stripped:
                lines.add(stripped)
    return lines


def _line_range_overlap(
    ranges_a: dict[str, list[tuple[int, int]]],
    ranges_b: dict[str, list[tuple[int, int]]],
    tolerance: int = 10,
) -> float:
    """Compute fraction of gold hunk line ranges that overlap with generated ranges.

    Uses a tolerance window: a generated hunk counts as overlapping a gold hunk
    if their line ranges are within ``tolerance`` lines of each other.  This
    accounts for LLM-generated patches having slightly different line numbers
    than the gold patch (due to context window differences, reformatting, etc.)
    while still targeting the same logical code region.
    """
    shared_files = set(ranges_a.keys()) & set(ranges_b.keys())
    if not shared_files:
        return 0.0

    total_gold_hunks = 0
    overlapping_hunks = 0

    for f in shared_files:
        for g_start, g_end in ranges_a[f]:
            total_gold_hunks += 1
            for c_start, c_end in ranges_b[f]:
                # Ranges overlap (with tolerance) if they're within tolerance
                # lines of each other
                if (c_start - tolerance) <= g_end and (c_end + tolerance) >= g_start:
                    overlapping_hunks += 1
                    break  # count each gold hunk at most once

    if total_gold_hunks == 0:
        return 0.0
    return min(overlapping_hunks / total_gold_hunks, 1.0)


def proxy_eval(generated_text: str, instance: dict) -> dict:
    """
    Evaluate a generated patch without running the test suite.

    Returns:
        has_diff:            bool  — model produced a valid unified diff
        file_overlap:        float — fraction of gold files present in patch
        exact_file_match:    bool  — generated patch touches exactly the right files
        hunk_overlap:        float — fraction of gold line ranges covered by generated hunks
        content_similarity:  float — Jaccard similarity of changed lines (added/removed)
    """
    generated_patch = extract_patch(generated_text)
    gold_patch = instance["patch"]
    gold_files = parse_patch_files(gold_patch)
    generated_files = parse_patch_files(generated_patch)

    has_diff = is_valid_diff(generated_patch)

    file_overlap = (
        len(gold_files & generated_files) / len(gold_files) if gold_files else 0.0
    )
    exact_file_match = (gold_files == generated_files) and bool(gold_files)

    # Hunk-level: do they modify the same line ranges?
    gold_ranges = _parse_hunk_line_ranges(gold_patch)
    gen_ranges = _parse_hunk_line_ranges(generated_patch)
    hunk_overlap = _line_range_overlap(gold_ranges, gen_ranges)

    # Content-level: Jaccard similarity of the actual changed lines
    gold_lines = _extract_changed_lines(gold_patch)
    gen_lines = _extract_changed_lines(generated_patch)
    if gold_lines or gen_lines:
        content_similarity = len(gold_lines & gen_lines) / len(gold_lines | gen_lines)
    else:
        content_similarity = 0.0

    return {
        "has_diff": has_diff,
        "file_overlap": round(file_overlap, 3),
        "exact_file_match": exact_file_match,
        "hunk_overlap": round(hunk_overlap, 3),
        "content_similarity": round(content_similarity, 3),
        "gold_files": sorted(gold_files),
        "generated_files": sorted(generated_files),
    }


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class SWERunResult:
    instance_id: str
    mode: str  # "baseline" or "compressed"
    has_diff: bool
    file_overlap: float
    exact_file_match: bool
    hunk_overlap: float
    content_similarity: float
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    latency_ms: float
    cost_usd: float = 0.0
    compression_ratio: float = 0.0
    error: str = ""


# ---------------------------------------------------------------------------
# Single instance evaluation
# ---------------------------------------------------------------------------


def _run_with_retrieval_loop(
    model: str,
    messages: list[dict],
    tools: list[dict],
    cache: dict[str, str],
    max_retrievals: int = 5,
) -> tuple[str, object, float, float]:
    """
    Call the model, and if it invokes litellm_content_retrieve, fulfill
    the tool call from the cache and re-call until the model produces a
    final text response (or we hit max_retrievals).

    Returns (generated_text, final_usage, total_latency_ms, total_cost).
    """
    total_latency = 0.0
    total_cost = 0.0
    total_usage = None
    kwargs: dict = {
        "model": model,
        "messages": list(messages),
        "temperature": 0.0,
        "max_tokens": 4096,
    }
    if tools:
        kwargs["tools"] = tools

    for _ in range(max_retrievals + 1):
        t0 = time.time()
        resp = litellm.completion(**kwargs)
        total_latency += (time.time() - t0) * 1000
        total_cost += resp._hidden_params.get("response_cost", 0) or 0
        total_usage = resp.usage

        choice = resp.choices[0]

        # If the model produced tool calls, fulfill them and loop
        tool_calls = getattr(choice.message, "tool_calls", None)
        if tool_calls:
            # Append the assistant message with tool calls
            kwargs["messages"].append(choice.message.model_dump())

            for tc in tool_calls:
                if tc.function.name == "litellm_content_retrieve":
                    import json as _json

                    args = _json.loads(tc.function.arguments)
                    key = args.get("key", "")
                    content = cache.get(key, f"[key {key!r} not found in cache]")
                    kwargs["messages"].append(
                        {
                            "role": "tool",
                            "tool_call_id": tc.id,
                            "content": content,
                        }
                    )
                else:
                    kwargs["messages"].append(
                        {
                            "role": "tool",
                            "tool_call_id": tc.id,
                            "content": "[unknown tool]",
                        }
                    )
            continue

        # No tool calls — model produced a final text response
        return choice.message.content or "", total_usage, total_latency, total_cost

    # Exhausted retries — return whatever we have
    return resp.choices[0].message.content or "", total_usage, total_latency, total_cost


def eval_instance(
    instance: dict,
    model: str,
    use_compression: bool,
    compression_trigger: int,
    compression_target: Optional[int] = None,
    embedding_model: Optional[str] = None,
) -> SWERunResult:
    mode = "compressed" if use_compression else "baseline"
    messages = build_messages(instance)
    compression_ratio = 0.0
    tools: list[dict] = []
    cache: dict[str, str] = {}

    if use_compression:
        compress_kwargs: dict = {
            "messages": messages,
            "model": model,
            "call_type": CallTypes.completion,
            "compression_trigger": compression_trigger,
            "embedding_model": embedding_model,
        }
        if compression_target is not None:
            compress_kwargs["compression_target"] = compression_target
        result = litellm_compress(**compress_kwargs)
        messages = result["messages"]
        tools = result["tools"]
        cache = result["cache"]
        compression_ratio = result["compression_ratio"]

    try:
        generated_text, usage, latency_ms, cost = _run_with_retrieval_loop(
            model=model,
            messages=messages,
            tools=tools,
            cache=cache,
        )
        ev = proxy_eval(generated_text, instance)

        return SWERunResult(
            instance_id=instance["instance_id"],
            mode=mode,
            has_diff=ev["has_diff"],
            file_overlap=ev["file_overlap"],
            exact_file_match=ev["exact_file_match"],
            hunk_overlap=ev["hunk_overlap"],
            content_similarity=ev["content_similarity"],
            prompt_tokens=usage.prompt_tokens,
            completion_tokens=usage.completion_tokens,
            total_tokens=usage.total_tokens,
            latency_ms=latency_ms,
            cost_usd=cost,
            compression_ratio=compression_ratio,
        )
    except Exception as e:
        return SWERunResult(
            instance_id=instance["instance_id"],
            mode=mode,
            has_diff=False,
            file_overlap=0.0,
            exact_file_match=False,
            hunk_overlap=0.0,
            content_similarity=0.0,
            prompt_tokens=0,
            completion_tokens=0,
            total_tokens=0,
            latency_ms=0.0,
            compression_ratio=0.0,
            error=str(e)[:500],
        )


# ---------------------------------------------------------------------------
# Aggregation
# ---------------------------------------------------------------------------


def aggregate(results: list[SWERunResult]) -> dict:
    if not results:
        return {}
    valid = [r for r in results if not r.error]
    errors = len(results) - len(valid)
    return {
        "total": len(results),
        "errors": errors,
        "has_diff_rate": round(
            sum(r.has_diff for r in results) / len(results) * 100, 1
        ),
        "avg_file_overlap": round(statistics.mean(r.file_overlap for r in results), 3),
        "exact_file_match_rate": round(
            sum(r.exact_file_match for r in results) / len(results) * 100, 1
        ),
        "avg_hunk_overlap": round(statistics.mean(r.hunk_overlap for r in results), 3),
        "avg_content_similarity": round(
            statistics.mean(r.content_similarity for r in results), 3
        ),
        "avg_prompt_tokens": round(statistics.mean(r.prompt_tokens for r in results)),
        "avg_total_tokens": round(statistics.mean(r.total_tokens for r in results)),
        "avg_latency_ms": round(statistics.mean(r.latency_ms for r in results), 1),
        "avg_compression_ratio": round(
            statistics.mean(r.compression_ratio for r in results), 4
        ),
        "total_cost_usd": round(sum(r.cost_usd for r in results), 6),
        "avg_cost_usd": round(statistics.mean(r.cost_usd for r in results), 6),
    }


# ---------------------------------------------------------------------------
# Main benchmark
# ---------------------------------------------------------------------------


def run_benchmark(
    model: str,
    num_problems: int = 10,
    compression_trigger: int = 10_000,
    compression_target: Optional[int] = None,
    embedding_model: Optional[str] = None,
) -> dict:
    """
    Run baseline vs compressed evaluation on SWE-bench Lite problems.

    Parameters:
        model:               LLM model name (litellm format).
        num_problems:        How many SWE-bench Lite problems to run.
        compression_trigger: Token count above which compression activates.
                             The bm25_27K dataset has ~27k tokens of context
                             per problem, so a trigger of 10k–20k is sensible.
        embedding_model:     Optional embedding model for semantic scoring.
    """
    problems = load_problems(n=num_problems)

    print(f"{'=' * 60}")
    print("SWE-bench Compression Eval")
    print(f"{'=' * 60}")
    print(f"Model:               {model}")
    print(f"Problems:            {len(problems)}")
    effective_target = (
        compression_target
        if compression_target is not None
        else compression_trigger * 7 // 10
    )
    print(f"Compression trigger: {compression_trigger} tokens")
    print(f"Compression target:  {effective_target} tokens")
    print(f"Embedding model:     {embedding_model or 'None (BM25 only)'}")
    print(f"{'=' * 60}\n")

    baseline_results: list[SWERunResult] = []
    compressed_results: list[SWERunResult] = []

    for i, instance in enumerate(problems):
        iid = instance["instance_id"]

        print(f"[{i+1}/{len(problems)}] {iid}")

        print(f"  baseline   ...", end=" ", flush=True)
        r_base = eval_instance(
            instance,
            model,
            use_compression=False,
            compression_trigger=compression_trigger,
            compression_target=compression_target,
        )
        baseline_results.append(r_base)
        if r_base.error:
            print(f"ERROR: {r_base.error[:80]}")
        else:
            print(
                f"{'✓' if r_base.has_diff else '✗'} diff  "
                f"file_overlap={r_base.file_overlap:.2f}  "
                f"{r_base.prompt_tokens} tok  "
                f"${r_base.cost_usd:.4f}"
            )

        print(f"  compressed ...", end=" ", flush=True)
        r_comp = eval_instance(
            instance,
            model,
            use_compression=True,
            compression_trigger=compression_trigger,
            compression_target=compression_target,
            embedding_model=embedding_model,
        )
        compressed_results.append(r_comp)
        if r_comp.error:
            print(f"ERROR: {r_comp.error[:80]}")
        else:
            print(
                f"{'✓' if r_comp.has_diff else '✗'} diff  "
                f"file_overlap={r_comp.file_overlap:.2f}  "
                f"{r_comp.prompt_tokens} tok  "
                f"${r_comp.cost_usd:.4f}  "
                f"(ratio: {r_comp.compression_ratio:.2%})"
            )

    base_agg = aggregate(baseline_results)
    comp_agg = aggregate(compressed_results)

    print(f"\n{'=' * 60}")
    print("RESULTS")
    print(f"{'=' * 60}")
    print(f"\n  Baseline:")
    print(f"    Has-diff rate:       {base_agg['has_diff_rate']}%")
    print(f"    Avg file overlap:    {base_agg['avg_file_overlap']:.3f}")
    print(f"    Exact file match:    {base_agg['exact_file_match_rate']}%")
    print(f"    Avg hunk overlap:    {base_agg['avg_hunk_overlap']:.3f}")
    print(f"    Avg content sim:     {base_agg['avg_content_similarity']:.3f}")
    print(f"    Avg prompt tokens:   {base_agg['avg_prompt_tokens']}")
    print(f"    Avg latency:         {base_agg['avg_latency_ms']}ms")
    print(f"    Total cost:          ${base_agg['total_cost_usd']:.4f}")
    print(f"    Avg cost/problem:    ${base_agg['avg_cost_usd']:.6f}")

    print(f"\n  Compressed:")
    print(f"    Has-diff rate:       {comp_agg['has_diff_rate']}%")
    print(f"    Avg file overlap:    {comp_agg['avg_file_overlap']:.3f}")
    print(f"    Exact file match:    {comp_agg['exact_file_match_rate']}%")
    print(f"    Avg hunk overlap:    {comp_agg['avg_hunk_overlap']:.3f}")
    print(f"    Avg content sim:     {comp_agg['avg_content_similarity']:.3f}")
    print(f"    Avg prompt tokens:   {comp_agg['avg_prompt_tokens']}")
    print(f"    Avg latency:         {comp_agg['avg_latency_ms']}ms")
    print(f"    Total cost:          ${comp_agg['total_cost_usd']:.4f}")
    print(f"    Avg cost/problem:    ${comp_agg['avg_cost_usd']:.6f}")
    print(f"    Avg compression:     {comp_agg['avg_compression_ratio']:.2%}")

    token_savings = base_agg["avg_prompt_tokens"] - comp_agg["avg_prompt_tokens"]
    token_pct = (
        round(token_savings / base_agg["avg_prompt_tokens"] * 100, 1)
        if base_agg["avg_prompt_tokens"]
        else 0
    )
    print(f"\n  Delta (compressed vs baseline):")
    print(f"    Token savings:       {token_savings} ({token_pct}%)")
    print(
        f"    Latency delta:       {base_agg['avg_latency_ms'] - comp_agg['avg_latency_ms']:+.1f}ms"
    )
    print(
        f"    Has-diff delta:      {comp_agg['has_diff_rate'] - base_agg['has_diff_rate']:+.1f}%"
    )
    print(
        f"    File overlap delta:  {comp_agg['avg_file_overlap'] - base_agg['avg_file_overlap']:+.3f}"
    )
    print(
        f"    Exact match delta:   {comp_agg['exact_file_match_rate'] - base_agg['exact_file_match_rate']:+.1f}%"
    )
    print(
        f"    Hunk overlap delta:  {comp_agg['avg_hunk_overlap'] - base_agg['avg_hunk_overlap']:+.3f}"
    )
    print(
        f"    Content sim delta:   {comp_agg['avg_content_similarity'] - base_agg['avg_content_similarity']:+.3f}"
    )
    cost_savings = base_agg["total_cost_usd"] - comp_agg["total_cost_usd"]
    cost_pct = (
        round(cost_savings / base_agg["total_cost_usd"] * 100, 1)
        if base_agg["total_cost_usd"]
        else 0
    )
    print(f"    Cost savings:        ${cost_savings:.4f} ({cost_pct}%)")

    ts = time.strftime("%Y-%m-%d_%H-%M-%S")
    report_path = f"eval_swe_bench_report_{ts}.json"
    report = {
        "model": model,
        "timestamp": ts,
        "num_problems": len(problems),
        "compression_trigger": compression_trigger,
        "embedding_model": embedding_model,
        "baseline": base_agg,
        "compressed": comp_agg,
        "baseline_results": [asdict(r) for r in baseline_results],
        "compressed_results": [asdict(r) for r in compressed_results],
    }
    with open(report_path, "w") as f:
        json.dump(report, f, indent=2)
    print(f"\nFull report saved to: {report_path}")

    return report


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SWE-bench Compression Evaluation")
    parser.add_argument(
        "--model", default="gpt-4o-mini", help="Model name (litellm format)"
    )
    parser.add_argument(
        "--problems",
        type=int,
        default=10,
        help="Number of SWE-bench Lite problems to run (default: 10)",
    )
    parser.add_argument(
        "--compression-trigger",
        type=int,
        default=10_000,
        help="Token threshold to activate compression (default: 10000). "
        "The bm25_27K dataset has ~27k tokens of context per problem.",
    )
    parser.add_argument(
        "--compression-target",
        type=int,
        default=None,
        help="Target token count after compression (default: 70%% of trigger). "
        "Higher values preserve more context at the cost of less compression.",
    )
    parser.add_argument(
        "--embedding-model",
        type=str,
        default=None,
        help="Embedding model for semantic scoring (e.g. text-embedding-3-small)",
    )
    args = parser.parse_args()

    run_benchmark(
        model=args.model,
        num_problems=args.problems,
        compression_trigger=args.compression_trigger,
        compression_target=args.compression_target,
        embedding_model=args.embedding_model,
    )
