import subprocess
import time
import re
import os
import datetime
import pytest
import urllib.request
import urllib.error

# Configuration
# Use a session-scope timestamp or generate per test?
# Per-test ensures no collisions even if run in parallel (though these should satisfy sequential)
TIMESTAMP = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")

AGENTS_TO_TEST = [
    {"name": "smolagents", "script": "examples/low_code/smolagents_demo.py", "project_prefix": "verify-smolagents"},
    {"name": "openai_agents", "script": "examples/low_code/openai_agents_demo.py", "project_prefix": "verify-openai"},
    {"name": "manual_phoenix", "script": "examples/low_code/manual_phoenix_demo.py", "project_prefix": "verify-manual"},
    {"name": "simple_openai", "script": "examples/low_code/simple_openai.py", "project_prefix": "verify-simple-openai"},
]


@pytest.fixture(scope="session", autouse=True)
def phoenix_server():
    """Ensure a Phoenix server is running before executing E2E tests, and shut it down afterward."""
    # 1. Check if it's already running locally
    otlp_ready = False
    for _ in range(2):
        try:
            req = urllib.request.Request("http://localhost:6006/v1/traces", method="POST")
            urllib.request.urlopen(req, timeout=1)
            otlp_ready = True
            break
        except urllib.error.HTTPError:
            # A valid HTTPError (405, 400) means the receiver is bound and responding
            otlp_ready = True
            break
        except (urllib.error.URLError, ConnectionError):
            time.sleep(1)

    if otlp_ready:
        print("\nPhoenix is already running on port 6006.")
        yield "http://localhost:6006"
        return

    import sys

    print("\nStarting local Phoenix server for E2E tests...")

    env = os.environ.copy()
    env["PHOENIX_PORT"] = "6006"

    # Start it using the current python executable to avoid 'uv run' overhead
    # We use run_in_thread=True and a sleepy while loop because run_in_thread=False
    # can crash the fastAPI uvicorn startup in some MacOS environments.
    script = "import phoenix as px; import time; px.launch_app(run_in_thread=True); import sys; sys.stdout.flush(); time.sleep(86400)"

    proc = subprocess.Popen([sys.executable, "-c", script], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)

    # Poll until the server is responsive
    max_retries = 30
    for _ in range(max_retries):
        try:
            # specifically poll the OTLP endpoint
            req = urllib.request.Request("http://localhost:6006/v1/traces", method="POST")
            urllib.request.urlopen(req, timeout=1)
            print("Phoenix server is up and running.")
            break
        except urllib.error.HTTPError:
            # A valid HTTPError (405, 400) means the endpoint is bound and responding
            print("Phoenix server is up and running.")
            break
        except Exception:
            # Also check if process crashed early
            if proc.poll() is not None:
                stderr_output = proc.stderr.read() if proc.stderr else "Unknown error"
                pytest.fail(f"Phoenix server process crashed unexpectedly: {stderr_output}")
            time.sleep(1)
    else:
        proc.terminate()
        stderr_output = proc.stderr.read() if proc.stderr else "Unknown error"
        pytest.fail(f"Failed to start local Phoenix server within 30 seconds. Stderr: {stderr_output}")

    yield "http://localhost:6006"

    # Cleanup: shut down Phoenix when tests are done
    print("\nShutting down local Phoenix server...")
    proc.terminate()
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()


@pytest.mark.e2e
@pytest.mark.parametrize("agent_config", AGENTS_TO_TEST, ids=[a["name"] for a in AGENTS_TO_TEST])
def test_e2e_pipeline_agent(agent_config, phoenix_server):
    """
    Runs the full E2E pipeline for a specific agent configuration:
    1. Executing the agent script
    2. Verifying traces in Phoenix associated with a unique project name
    3. Running Evolve Sync to verify guideline generation
    """
    agent_name = agent_config["name"]
    script_path = agent_config["script"]

    # Generate unique project name for this run
    # Using a fresh timestamp per run to avoid collisions if tests run slowly
    current_timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    project_name = f"{agent_config['project_prefix']}-{current_timestamp}"

    print("\n==================================================")
    print(f" TESTING AGENT: {agent_name}")
    print(f" Script: {script_path}")
    print(f" Project: {project_name}")
    print("==================================================")

    # --- Step 1: Run Agent ---
    print("\n--- Step 1: Running Agent ---")
    start_time = time.time()
    env = os.environ.copy()
    env["EVOLVE_AUTO_ENABLED"] = "true"
    # Important: Set project name for auto-instrumentation
    # evolve.auto prioritizes EVOLVE_TRACING_PROJECT over PHOENIX_PROJECT_NAME
    env["EVOLVE_TRACING_PROJECT"] = project_name
    env["PHOENIX_PROJECT_NAME"] = project_name

    # Ensure script exists
    if not os.path.exists(script_path):
        pytest.fail(f"Script not found: {script_path}")

    try:
        result = subprocess.run(["uv", "run", "python", script_path], env=env, capture_output=True, text=True, timeout=90)
    except subprocess.TimeoutExpired as e:
        print("❌ Agent execution timed out after 90s")
        # Still try to capture what we can from stdout/stderr if possible
        stdout = e.stdout if e.stdout else ""
        stderr = e.stderr if e.stderr else ""
        print("STDOUT:", stdout)
        print("STDERR:", stderr)
        pytest.fail(f"Agent execution timed out for {agent_name}")

    if result.returncode != 0:
        print(f"❌ Agent failed with exit code {result.returncode}")
        print("STDERR:", result.stderr)
        print("STDOUT:", result.stdout)
        pytest.fail(f"Agent execution failed: {result.stderr}")

    print(f"✅ Agent finished in {time.time() - start_time:.2f}s")

    # --- Step 2: Verify Traces ---
    print(f"\n--- Step 2: Verifying Phoenix Traces ({project_name}) ---")

    # Wait briefly for traces to be flushed/indexed
    time.sleep(2)

    check_script = f"""
import phoenix as px
import sys
try:
    c = px.Client(endpoint='{phoenix_server}')
    df = c.get_spans_dataframe(project_name='{project_name}')
    if df is not None and not df.empty:
        print(f"FOUND_TRACES:{{len(df)}}")
    else:
        print("NO_TRACES")
except Exception as e:
    print(f"ERROR:{{e}}")
"""
    try:
        result = subprocess.run(["uv", "run", "python", "-c", check_script], capture_output=True, text=True, timeout=30)
    except subprocess.TimeoutExpired:
        print("❌ Phoenix trace verification script timed out")
        pytest.fail(f"Trace verification timed out for {project_name}")

    output = result.stdout + result.stderr
    if "FOUND_TRACES" in output:
        count = output.split("FOUND_TRACES:")[1].split()[0]
        print(f"✅ Found {count} traces in project '{project_name}'")
    else:
        print(f"❌ No traces found in project '{project_name}'")
        print(f"Debug Output: {output}")
        pytest.fail(f"No traces found in Phoenix project {project_name}. Debug: {output}")

    # --- Step 3: Sync & Generate Guidelines ---
    print("\n--- Step 3: Running Evolve Sync & Monitoring ---")
    sync_command = [
        "uv",
        "run",
        "evolve",
        "sync",
        "phoenix",
        "--project",
        project_name,
        "--include-errors",
        "--limit",
        "500",
    ]
    print(f"Command: {' '.join(sync_command)}")

    process = subprocess.Popen(
        sync_command,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,  # Merge stderr to monitor everything
        text=True,
        bufsize=1,
        universal_newlines=True,
    )

    guidelines_found = False
    sync_start = time.time()
    timeout = 120  # 2 minute timeout for sync
    output_lines = []

    try:
        while True:
            if time.time() - sync_start > timeout:
                print(f"❌ Timeout waiting for guidelines generation ({timeout}s)")
                break

            line = process.stdout.readline()
            if not line:
                if process.poll() is not None:
                    break
                time.sleep(0.1)  # Avoid tight loop if no output but process alive
                continue

            output_lines.append(line)
            line_stripped = line.strip()
            # print(f"[Sync] {line_stripped}") # Optional: verbose logging

            # Check target log pattern
            match = re.search(r"generated (\d+) guidelines", line_stripped)
            if match:
                count = int(match.group(1))
                if count > 0:
                    print(f"\n✅ SUCCESS: Generated {count} guidelines!")
                    guidelines_found = True
                    break
    finally:
        if process.poll() is None:
            print("Stopping sync process...")
            process.terminate()
            try:
                process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                process.kill()

    if not guidelines_found:
        full_output = "".join(output_lines)
        print(f"Final Sync Output:\n{full_output}")
        pytest.fail(f"Failed to detect guideline generation for {agent_name} within {timeout}s.")
