"""
实验：用 LLM + Dataset 构造 Lambda 演算原语（本地模拟版）

通过 subprocess 调用 claude CLI 逐个执行 LDS 测试。
每个测试将 dataset (few-shot examples) + 新输入组成 prompt，
然后让 LLM 输出结果，对比期望值。

使用方式：
    python run_experiment.py
"""

import subprocess
import json
import sys
import time
from dataclasses import dataclass, field


@dataclass
class ExperimentResult:
    name: str
    tests: list[dict] = field(default_factory=list)

    def add(self, input_val: str, expected: str, actual: str):
        # 宽松匹配：去空格、去引号、大小写
        def normalize(s):
            return s.strip().strip("'\"").strip().lower()
        passed = normalize(actual) == normalize(expected)
        self.tests.append({
            "input": input_val,
            "expected": expected,
            "actual": actual,
            "passed": passed,
        })

    @property
    def passed_count(self):
        return sum(1 for t in self.tests if t["passed"])

    @property
    def total_count(self):
        return len(self.tests)

    def summary(self) -> str:
        lines = [
            f"\n{'='*60}",
            f"实验: {self.name}",
            f"通过: {self.passed_count}/{self.total_count}",
            f"{'='*60}",
        ]
        for t in self.tests:
            status = "✓" if t["passed"] else "✗"
            lines.append(
                f"  {status} input={t['input']!r:40s} "
                f"expected={t['expected']!r:12s} actual={t['actual']!r}"
            )
        return "\n".join(lines)


def call_llm(prompt: str, max_retries: int = 2) -> str:
    """调用 claude CLI 执行 LDS prompt"""
    for attempt in range(max_retries + 1):
        try:
            result = subprocess.run(
                ["claude", "-p", prompt, "--model", "sonnet"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                return result.stdout.strip()
            else:
                if attempt < max_retries:
                    time.sleep(1)
                    continue
                return f"ERROR: {result.stderr.strip()}"
        except subprocess.TimeoutExpired:
            if attempt < max_retries:
                continue
            return "ERROR: timeout"
        except FileNotFoundError:
            return "ERROR: claude CLI not found"
    return "ERROR: max retries exceeded"


def build_prompt(description: str, examples: list[tuple[str, str]], test_input: str) -> str:
    """构造 LDS prompt: system instruction + dataset + test input"""
    lines = [
        "You are a precise function executor. Given the examples below, "
        "learn the pattern and apply it to the new input. "
        "Output ONLY the result, nothing else. No explanation, no quotes, no extra text.",
        "",
        f"Function: {description}",
        "",
        "Examples:",
    ]
    for inp, out in examples:
        lines.append(f"  Input: {inp}")
        lines.append(f"  Output: {out}")
        lines.append("")
    lines.append(f"Input: {test_input}")
    lines.append("Output:")
    return "\n".join(lines)


# ============================================================
# 实验定义
# ============================================================

def experiment_succ() -> ExperimentResult:
    """实验 1: SUCC 后继函数 ≡ λn.λf.λx. f(n f x)"""
    result = ExperimentResult("SUCC (后继函数)")
    dataset = [
        ("0", "1"), ("1", "2"), ("2", "3"),
        ("5", "6"), ("9", "10"), ("13", "14"), ("99", "100"),
    ]
    tests = [3, 4, 7, 10, 15, 42, 127, 255]
    for n in tests:
        prompt = build_prompt(
            "Given a number n, output n+1 (the successor).",
            dataset, str(n)
        )
        actual = call_llm(prompt)
        result.add(str(n), str(n + 1), actual)
    return result


def experiment_booleans() -> ExperimentResult:
    """实验 2: TRUE/FALSE ≡ λa.λb.a / λa.λb.b"""
    result = ExperimentResult("TRUE/FALSE (Church 布尔值)")

    true_dataset = [
        ("A='apple', B='banana'", "apple"),
        ("A='red', B='blue'", "red"),
        ("A='1', B='0'", "1"),
        ("A='yes', B='no'", "yes"),
        ("A='cat', B='dog'", "cat"),
    ]
    false_dataset = [
        ("A='apple', B='banana'", "banana"),
        ("A='red', B='blue'", "blue"),
        ("A='1', B='0'", "0"),
        ("A='yes', B='no'", "no"),
        ("A='cat', B='dog'", "dog"),
    ]

    test_pairs = [("sun", "moon"), ("hello", "world"), ("42", "0"), ("alpha", "omega")]
    for a, b in test_pairs:
        inp = f"A='{a}', B='{b}'"
        # TRUE
        prompt_t = build_prompt("Given two options A and B, always select A (the first one).", true_dataset, inp)
        actual_t = call_llm(prompt_t)
        result.add(f"TRUE({a},{b})", a, actual_t)
        # FALSE
        prompt_f = build_prompt("Given two options A and B, always select B (the second one).", false_dataset, inp)
        actual_f = call_llm(prompt_f)
        result.add(f"FALSE({a},{b})", b, actual_f)
    return result


def experiment_logic() -> ExperimentResult:
    """实验 3: AND/OR/NOT"""
    result = ExperimentResult("AND/OR/NOT (逻辑运算)")

    and_ds = [("TRUE, TRUE", "TRUE"), ("TRUE, FALSE", "FALSE"),
              ("FALSE, TRUE", "FALSE"), ("FALSE, FALSE", "FALSE")]
    or_ds = [("TRUE, TRUE", "TRUE"), ("TRUE, FALSE", "TRUE"),
             ("FALSE, TRUE", "TRUE"), ("FALSE, FALSE", "FALSE")]
    not_ds = [("TRUE", "FALSE"), ("FALSE", "TRUE")]

    # AND
    for inp, exp in and_ds:
        prompt = build_prompt("Logical AND of two boolean values.", and_ds, inp)
        actual = call_llm(prompt)
        result.add(f"AND({inp})", exp, actual)
    # OR
    for inp, exp in or_ds:
        prompt = build_prompt("Logical OR of two boolean values.", or_ds, inp)
        actual = call_llm(prompt)
        result.add(f"OR({inp})", exp, actual)
    # NOT
    for inp, exp in not_ds:
        prompt = build_prompt("Logical NOT of a boolean value.", not_ds, inp)
        actual = call_llm(prompt)
        result.add(f"NOT({inp})", exp, actual)
    return result


def experiment_if() -> ExperimentResult:
    """实验 4: IF-THEN-ELSE ≡ λcond.λthen.λelse. cond then else"""
    result = ExperimentResult("IF-THEN-ELSE (条件分支)")

    dataset = [
        ("condition=TRUE, then='yes', else='no'", "yes"),
        ("condition=FALSE, then='yes', else='no'", "no"),
        ("condition=TRUE, then='42', else='0'", "42"),
        ("condition=FALSE, then='42', else='0'", "0"),
        ("condition=TRUE, then='go', else='stop'", "go"),
        ("condition=FALSE, then='go', else='stop'", "stop"),
    ]
    tests = [
        ("condition=TRUE, then='accept', else='reject'", "accept"),
        ("condition=FALSE, then='accept', else='reject'", "reject"),
        ("condition=TRUE, then='100', else='-1'", "100"),
        ("condition=FALSE, then='100', else='-1'", "-1"),
        ("condition=TRUE, then='open', else='close'", "open"),
        ("condition=FALSE, then='open', else='close'", "close"),
    ]
    for inp, exp in tests:
        prompt = build_prompt(
            "IF-THEN-ELSE: If condition is TRUE, output the THEN value. If FALSE, output the ELSE value.",
            dataset, inp
        )
        actual = call_llm(prompt)
        result.add(inp, exp, actual)
    return result


def experiment_pairs() -> ExperimentResult:
    """实验 5: PAIR/FST/SND"""
    result = ExperimentResult("PAIR/FST/SND (有序对)")

    pair_ds = [
        ("a=3, b=7", "(3, 7)"),
        ("a=hello, b=world", "(hello, world)"),
        ("a=0, b=1", "(0, 1)"),
    ]
    fst_ds = [
        ("(3, 7)", "3"), ("(hello, world)", "hello"),
        ("(0, 1)", "0"), ("(42, 99)", "42"),
    ]
    snd_ds = [
        ("(3, 7)", "7"), ("(hello, world)", "world"),
        ("(0, 1)", "1"), ("(42, 99)", "99"),
    ]

    # PAIR
    for inp, exp in [("a=10, b=20", "(10, 20)"), ("a=foo, b=bar", "(foo, bar)")]:
        prompt = build_prompt("Construct a pair from a and b.", pair_ds, inp)
        actual = call_llm(prompt)
        result.add(f"PAIR({inp})", exp, actual)

    # FST
    for inp, exp in [("(5, 8)", "5"), ("(alpha, beta)", "alpha"), ("(100, 200)", "100")]:
        prompt = build_prompt("Extract the first element of a pair.", fst_ds, inp)
        actual = call_llm(prompt)
        result.add(f"FST({inp})", exp, actual)

    # SND
    for inp, exp in [("(5, 8)", "8"), ("(alpha, beta)", "beta"), ("(100, 200)", "200")]:
        prompt = build_prompt("Extract the second element of a pair.", snd_ds, inp)
        actual = call_llm(prompt)
        result.add(f"SND({inp})", exp, actual)

    return result


def experiment_composition() -> ExperimentResult:
    """实验 6: 函数组合 DOUBLE ∘ SUCC ≡ (g ∘ f)(x) = g(f(x))"""
    result = ExperimentResult("DOUBLE ∘ SUCC (函数组合)")

    succ_ds = [("0", "1"), ("1", "2"), ("2", "3"), ("5", "6"), ("9", "10")]
    double_ds = [("0", "0"), ("1", "2"), ("2", "4"), ("3", "6"), ("5", "10"), ("10", "20")]

    for n in [0, 1, 3, 5, 7, 10]:
        # Step 1: SUCC(n)
        p1 = build_prompt("Given a number n, output n+1.", succ_ds, str(n))
        succ_result = call_llm(p1)

        # Step 2: DOUBLE(SUCC(n))
        p2 = build_prompt("Given a number n, output 2*n.", double_ds, succ_result)
        double_result = call_llm(p2)

        expected = str(2 * (n + 1))
        result.add(f"DOUBLE(SUCC({n}))", expected, double_result)
    return result


def experiment_factorial() -> ExperimentResult:
    """实验 7: FACTORIAL —— 递归通过 CoT 展开，等价于 Y 组合子"""
    result = ExperimentResult("FACTORIAL (递归/Y 组合子)")

    dataset = [
        ("0", "0! = 1\nResult: 1"),
        ("1", "1! = 1 × 0! = 1 × 1 = 1\nResult: 1"),
        ("3", "3! = 3 × 2!\n2! = 2 × 1!\n1! = 1 × 0!\n0! = 1\nSo: 1 × 1 × 2 × 3 = 6\nResult: 6"),
        ("5", "5! = 5 × 4!\n4! = 4 × 3!\n3! = 3 × 2!\n2! = 2 × 1!\n1! = 1 × 0!\n0! = 1\nSo: 1 × 1 × 2 × 3 × 4 × 5 = 120\nResult: 120"),
    ]

    import math
    for n in [2, 4, 6, 7, 8, 10]:
        prompt = build_prompt(
            "Compute n! (factorial) step by step. Show recursive expansion. End with 'Result: <number>'.",
            dataset, str(n)
        )
        actual_raw = call_llm(prompt)
        # 提取 Result 行
        actual = actual_raw
        for line in reversed(actual_raw.split("\n")):
            if "Result:" in line:
                actual = line.split("Result:")[-1].strip()
                break
        expected = str(math.factorial(n))
        result.add(f"{n}!", expected, actual)
    return result


def experiment_church_numerals() -> ExperimentResult:
    """实验 8: Church 数 c_n ≡ λf.λx. f^n(x)"""
    result = ExperimentResult("CHURCH 数 (f^n(x))")

    dataset = [
        ("n=0, f='add1', x='0'", "0"),
        ("n=1, f='add1', x='0'", "1"),
        ("n=2, f='add1', x='0'", "2"),
        ("n=3, f='add1', x='0'", "3"),
        ("n=0, f='double', x='1'", "1"),
        ("n=1, f='double', x='1'", "2"),
        ("n=2, f='double', x='1'", "4"),
        ("n=3, f='double', x='1'", "8"),
        ("n=4, f='double', x='1'", "16"),
    ]

    # add1 tests
    for n in [4, 5, 7, 10]:
        prompt = build_prompt(
            "Apply function f to value x exactly n times. "
            "add1 adds 1 each time. double multiplies by 2 each time.",
            dataset, f"n={n}, f='add1', x='0'"
        )
        actual = call_llm(prompt)
        result.add(f"add1^{n}(0)", str(n), actual)

    # double tests: double^n(1) = 2^n
    for n in [5, 6, 7, 8]:
        prompt = build_prompt(
            "Apply function f to value x exactly n times. "
            "add1 adds 1 each time. double multiplies by 2 each time.",
            dataset, f"n={n}, f='double', x='1'"
        )
        actual = call_llm(prompt)
        result.add(f"double^{n}(1)", str(2**n), actual)

    return result


# ============================================================
# 主程序
# ============================================================

def main():
    print("=" * 60)
    print("LDS ≡ Lambda Calculus 实验验证")
    print("模式: 本地 Claude CLI 模拟")
    print("=" * 60)

    # 检查 claude CLI
    try:
        r = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=5)
        print(f"Claude CLI: {r.stdout.strip()}")
    except Exception as e:
        print(f"错误: 无法找到 claude CLI: {e}")
        sys.exit(1)

    experiments = [
        ("1. 后继函数 SUCC", experiment_succ),
        ("2. Church 布尔值 TRUE/FALSE", experiment_booleans),
        ("3. 逻辑运算 AND/OR/NOT", experiment_logic),
        ("4. 条件分支 IF-THEN-ELSE", experiment_if),
        ("5. 有序对 PAIR/FST/SND", experiment_pairs),
        ("6. 函数组合 DOUBLE∘SUCC", experiment_composition),
        ("7. 递归/阶乘 (Y 组合子)", experiment_factorial),
        ("8. Church 数 f^n(x)", experiment_church_numerals),
    ]

    all_results = []
    for name, fn in experiments:
        print(f"\n>>> 运行实验 {name}...")
        r = fn()
        all_results.append(r)
        print(r.summary())

    # 汇总
    total = sum(r.total_count for r in all_results)
    passed = sum(r.passed_count for r in all_results)

    print("\n" + "=" * 60)
    print("汇总结果")
    print("=" * 60)
    for r in all_results:
        rate = r.passed_count / r.total_count * 100 if r.total_count else 0
        bar = "█" * int(rate / 5) + "░" * (20 - int(rate / 5))
        print(f"  {r.name:40s} {r.passed_count:2d}/{r.total_count:2d}  {bar} {rate:.0f}%")
    print(f"\n  {'总计':40s} {passed:2d}/{total:2d}  {passed/total*100:.1f}%")
    print("=" * 60)

    # 保存 JSON
    output = {
        "model": "claude-sonnet (via CLI)",
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        "experiments": [
            {
                "name": r.name,
                "passed": r.passed_count,
                "total": r.total_count,
                "tests": r.tests,
            }
            for r in all_results
        ],
        "total_passed": passed,
        "total_tests": total,
    }
    with open("experiment_results.json", "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)
    print("\n结果已保存到 experiment_results.json")


if __name__ == "__main__":
    main()
