"""Context Strategy Experiments — Test context strategies + Reduce merging.

Tests on multiple codebases. Preserves results in experiments/results/.
"""

import json
import sys
import time
from collections import defaultdict
from pathlib import Path

from bex.tag_preprocessor.analyze import (
    _preprocess_files, _file_to_package, frequency_filter, scan_directory
)
from bex.twotinf import build_soa
from bex.rwr0 import rwr0
from bex.reduce import reduce_contexts, soa_distance, build_soa_with_support


RESULTS_DIR = Path(__file__).parent / "results"


CODEBASES = {
    "ragsak": {"name": "RAGSAK", "path": "/home/tobi/Desktop/kesai/RAGSAK", "ext": ".kt"},
    "flask": {"name": "Flask", "path": "/home/tobi/Desktop/dervish/external_refs/flask", "ext": ".py"},
}


# ---------------------------------------------------------------------------
# Data loading
# ---------------------------------------------------------------------------

def load_data(base, ext):
    groups = scan_directory(base)
    files = groups.get(ext, [])
    sequences, seq_files = _preprocess_files(files)
    symbol_seqs = [[text for _, text, _ in seq] for seq in sequences]
    packages = [_file_to_package(fp, base) for fp in seq_files]
    return symbol_seqs, packages, seq_files


# ---------------------------------------------------------------------------
# Context strategies
# ---------------------------------------------------------------------------

def context_file_path_k(symbol_seqs, packages, k):
    """Option A: Group by last k components of file path."""
    contexts = defaultdict(list)
    for i, seq in enumerate(symbol_seqs):
        pkg = packages[i]
        components = pkg.split("/")
        ctx = tuple(components[-k:]) if len(components) >= k else tuple(components)
        contexts[ctx].append(seq)
    return contexts


def context_first_k_symbols(symbol_seqs, k):
    """Option B: Group by first k symbols of call sequence."""
    contexts = defaultdict(list)
    for seq in symbol_seqs:
        if not seq:
            contexts[("ε",)].append(seq)
        else:
            ctx = tuple(seq[:k])
            contexts[ctx].append(seq)
    return contexts


def context_two_d(symbol_seqs, packages, path_k, sym_k):
    """Option C: Two-dimensional (path_k, first_k_symbols)."""
    contexts = defaultdict(list)
    for i, seq in enumerate(symbol_seqs):
        pkg = packages[i]
        components = pkg.split("/")
        path_ctx = tuple(components[-path_k:]) if len(components) >= path_k else tuple(components)
        if not seq:
            sym_ctx = ("ε",)
        else:
            sym_ctx = tuple(seq[:sym_k])
        ctx = path_ctx + sym_ctx
        contexts[ctx].append(seq)
    return contexts


def context_return_type_heuristic(symbol_seqs):
    """Option H: Group by return type heuristic (based on last symbol)."""
    contexts = defaultdict(list)
    for seq in symbol_seqs:
        if not seq:
            contexts[("ε",)].append(seq)
        else:
            last = seq[-1]
            if last.startswith("return"):
                ctx = ("RETURN_" + last.split()[1] if len(last.split()) > 1 else "RETURN_OTHER",)
            elif last in ("true", "false"):
                ctx = ("RETURN_BOOL",)
            elif last.startswith("set") or last.startswith("put"):
                ctx = ("SIDE_EFFECT",)
            else:
                ctx = ("RETURN_VALUE",)
            contexts[ctx].append(seq)
    return contexts


# ---------------------------------------------------------------------------
# Evaluation
# ---------------------------------------------------------------------------

def evaluate_context(contexts, label, min_methods=3, max_methods=50,
                     max_unique_ratio=0.85, max_alphabet=20, max_soa_edges=100):
    """Evaluate a context grouping: learn SOREs, collect metrics."""
    results = {
        "strategy": label,
        "total_contexts": len(contexts),
        "meaningful_contexts": 0,
        "total_methods": 0,
        "methods_in_good_groups": 0,
        "sore_successes": 0,
        "sore_failures": 0,
        "skip_reasons": {},
        "groups": [],
    }

    for ctx, seqs in sorted(contexts.items(), key=lambda x: -len(x[1])):
        n = len(seqs)
        results["total_methods"] += n
        if n < min_methods:
            continue
        results["meaningful_contexts"] += 1

        unique = len(set(tuple(s) for s in seqs))
        unique_ratio = unique / n if n else 1.0

        if n > max_methods:
            results["skip_reasons"]["too_large"] = results["skip_reasons"].get("too_large", 0) + 1
            results["groups"].append({
                "context": str(ctx), "methods": n, "unique": unique,
                "unique_ratio": round(unique_ratio, 3),
                "sore": "SKIP(too_large)", "sore_success": False,
            })
            continue

        if unique_ratio > max_unique_ratio:
            results["skip_reasons"]["too_diverse"] = results["skip_reasons"].get("too_diverse", 0) + 1
            results["groups"].append({
                "context": str(ctx), "methods": n, "unique": unique,
                "unique_ratio": round(unique_ratio, 3),
                "sore": "SKIP(too_diverse)", "sore_success": False,
            })
            continue

        alphabet = set()
        for seq in seqs:
            alphabet.update(seq)
        if len(alphabet) > max_alphabet:
            results["skip_reasons"]["large_alphabet"] = results["skip_reasons"].get("large_alphabet", 0) + 1
            results["groups"].append({
                "context": str(ctx), "methods": n, "unique": unique,
                "unique_ratio": round(unique_ratio, 3),
                "sore": "SKIP(large_alphabet)", "sore_success": False,
            })
            continue

        filtered = frequency_filter(
            [[(j, s, 0) for j, s in enumerate(seq)] for seq in seqs],
            min_coverage=0.0,
        )
        clean = [[text for _, text, _ in r] for r in filtered]
        clean = [s for s in clean if s]

        if len(clean) < 2:
            results["skip_reasons"]["empty_after_filter"] = results["skip_reasons"].get("empty_after_filter", 0) + 1
            continue

        soa = build_soa(clean)
        n_edges = sum(len(v) for v in soa._succ.values())
        if n_edges > max_soa_edges:
            results["skip_reasons"]["complex_soa"] = results["skip_reasons"].get("complex_soa", 0) + 1
            results["groups"].append({
                "context": str(ctx), "methods": n, "unique": unique,
                "unique_ratio": round(unique_ratio, 3),
                "sore": "SKIP(complex_soa)", "sore_success": False,
            })
            continue

        sore = rwr0(soa)

        group_info = {
            "context": str(ctx),
            "methods": n,
            "unique": unique,
            "unique_ratio": round(unique_ratio, 3),
            "sore": sore[:200] if sore not in ("∅",) else sore,
            "sore_success": sore != "∅",
        }

        if sore != "∅":
            results["sore_successes"] += 1
            results["methods_in_good_groups"] += n
        else:
            results["sore_failures"] += 1

        results["groups"].append(group_info)

    results["coverage"] = (
        round(results["methods_in_good_groups"] / results["total_methods"] * 100, 1)
        if results["total_methods"] > 0
        else 0
    )
    return results


def run_experiment(name, contexts, label, codebase_name, merge_info=None):
    """Run one experiment, print + save results."""
    print(f"\n  {label}")

    t0 = time.time()
    results = evaluate_context(contexts, label)
    elapsed = time.time() - t0
    results["elapsed_seconds"] = round(elapsed, 2)
    if merge_info:
        results["merge_info"] = merge_info

    print(f"    Contexts: {results['total_contexts']}  Meaningful: {results['meaningful_contexts']}  "
          f"SOREs: {results['sore_successes']}  Coverage: {results['coverage']}%  "
          f"Time: {elapsed:.2f}s")
    if merge_info:
        print(f"    Merges: {merge_info['merges']}  Threshold: {merge_info['threshold']}")

    out_path = RESULTS_DIR / f"{codebase_name}_{name}.json"
    with open(out_path, "w") as f:
        json.dump(results, f, indent=2)

    return results


# ---------------------------------------------------------------------------
# Run all experiments for one codebase
# ---------------------------------------------------------------------------

def run_codebase(codebase_key):
    cfg = CODEBASES[codebase_key]
    name = cfg["name"]
    base = cfg["path"]
    ext = cfg["ext"]

    print(f"\n{'#' * 70}")
    print(f"#  {name}  ({base})")
    print(f"{'#' * 70}")

    print(f"Loading {name} data...")
    symbol_seqs, packages, seq_files = load_data(base, ext)
    print(f"Loaded {len(symbol_seqs)} methods from {len(set(packages))} packages")

    all_results = []

    # --- Baselines ---
    contexts = defaultdict(list)
    for i, seq in enumerate(symbol_seqs):
        contexts[packages[i]].append(seq)
    all_results.append(run_experiment("baseline_package", contexts,
                                      "Baseline: Package grouping", codebase_key))

    for k in [1, 2, 3]:
        contexts = context_file_path_k(symbol_seqs, packages, k)
        all_results.append(run_experiment(f"file_path_k{k}", contexts,
                                          f"Option A: File path k={k}", codebase_key))

    for k in [1, 2, 3]:
        contexts = context_first_k_symbols(symbol_seqs, k)
        all_results.append(run_experiment(f"first_k_sym_{k}", contexts,
                                          f"Option B: First {k} symbols", codebase_key))

    for path_k in [1, 2]:
        for sym_k in [1, 2]:
            contexts = context_two_d(symbol_seqs, packages, path_k, sym_k)
            all_results.append(run_experiment(
                f"two_d_p{path_k}_s{sym_k}", contexts,
                f"Option C: Path k={path_k} + Symbol k={sym_k}", codebase_key))

    contexts = context_return_type_heuristic(symbol_seqs)
    all_results.append(run_experiment("return_type_heuristic", contexts,
                                      "Option H: Return type heuristic", codebase_key))

    # --- Reduce experiments ---
    # Best base: first k symbols, k=1,2,3
    # Test Reduce on each with thresholds
    reduce_thresholds = [0.05, 0.10, 0.15, 0.20, 0.30, 0.40]

    print(f"\n--- Reduce experiments ---")
    for base_k in [1, 2, 3]:
        base_contexts = context_first_k_symbols(symbol_seqs, base_k)
        base_eval = evaluate_context(base_contexts, f"First {base_k} symbols (pre-reduce)")

        for threshold in reduce_thresholds:
            merged, merge_count = reduce_contexts(base_contexts, threshold)
            merge_info = {"merges": merge_count, "threshold": threshold,
                          "contexts_before": len(base_contexts),
                          "contexts_after": len(merged)}
            label = f"Reduce k={base_k} ε={threshold}"
            all_results.append(run_experiment(
                f"reduce_k{base_k}_e{str(threshold).replace('.', '')}",
                merged, label, codebase_key, merge_info))

    # --- Summary ---
    print(f"\n{'=' * 70}")
    print(f"  {name} SUMMARY")
    print(f"{'=' * 70}")
    print(f"{'Strategy':<42} {'Ctx':>5} {'SORE':>5} {'Cov%':>6} {'Merge':>5}")
    print("-" * 70)
    for r in all_results:
        merge_str = ""
        if r.get("merge_info"):
            merge_str = str(r["merge_info"]["merges"])
        print(f"{r['strategy']:<42} {r['meaningful_contexts']:>5} "
              f"{r['sore_successes']:>5} {r['coverage']:>5.1f}% {merge_str:>5}")

    summary = [
        {k: v for k, v in r.items() if k != "groups"}
        for r in all_results
    ]
    summary_path = RESULTS_DIR / f"{codebase_key}_summary.json"
    with open(summary_path, "w") as f:
        json.dump(summary, f, indent=2)
    print(f"\nSummary: {summary_path}")

    return all_results


def main():
    RESULTS_DIR.mkdir(parents=True, exist_ok=True)

    targets = sys.argv[1:] if len(sys.argv) > 1 else list(CODEBASES.keys())

    all_results = {}
    for key in targets:
        if key not in CODEBASES:
            print(f"Unknown codebase: {key}. Available: {list(CODEBASES.keys())}")
            continue
        all_results[key] = run_codebase(key)

    # Cross-codebase comparison
    print(f"\n{'#' * 70}")
    print(f"#  CROSS-CODEBASE COMPARISON")
    print(f"{'#' * 70}")
    for key, results in all_results.items():
        cfg = CODEBASES[key]
        best = max(results, key=lambda r: r["coverage"])
        print(f"\n  {cfg['name']}: best = {best['strategy']} ({best['coverage']}%)")


if __name__ == "__main__":
    main()
