import os
import shutil
import subprocess
import tempfile
from typing import List, Tuple

from pytest_cream.fetch import fetch_tests
from pytest_cream.install import install_from_git


def _run_pytest_collect_durations(tests_path: str, durations_file: str, exclude_patterns: list = None, python_executable: str = None) -> None:
    # Determine which Python executable to use
    if python_executable and python_executable.startswith("uv run"):
        # Special handling for uv run commands
        cmd = ["uv", "run", "python", "-m", "pytest"]
    else:
        # Handle version strings like "3.10" -> "python3.10"
        if python_executable and python_executable.replace(".", "").isdigit():
            python_cmd = f"python{python_executable}"
        else:
            python_cmd = python_executable if python_executable else "python"
        cmd = [python_cmd, "-m", "pytest"]
    
    cmd.extend([
        tests_path,
        "--durations=0",
        "--durations-min=0",
        "--tb=short",
        "--disable-warnings",
        "-q",
        "-m",
        "not slow",
    ])
    
    # Add exclusion patterns if provided
    if exclude_patterns:
        for pattern in exclude_patterns:
            cmd.extend(["--ignore", pattern])
    
    # Check if tests directory exists
    if not os.path.exists(tests_path):
        print(f"Warning: Tests directory '{tests_path}' not found. Creating empty durations file.")
        with open(durations_file, "w") as out:
            out.write("# No tests found\n")
        return
    
    try:
        with open(durations_file, "w") as out:
            subprocess.run(cmd, stdout=out, stderr=out, check=True)
    except subprocess.CalledProcessError as e:
        print(f"Warning: pytest failed to collect durations (exit code {e.returncode})")
        print(f"This might mean the project has no tests or tests are not compatible.")
        print(f"Creating empty durations file to continue workflow...")
        with open(durations_file, "w") as out:
            out.write("# pytest failed to collect durations\n")
            out.write(f"# Command: {' '.join(cmd)}\n")
            out.write(f"# Exit code: {e.returncode}\n")


def _parse_durations(durations_file: str) -> List[Tuple[float, str]]:
    results = []
    import re

    # Match lines like: '1.23s call path/to/test_file.py::test_name'
    # or '1.23s teardown path/to/test_file.py::test_name'
    float_re = re.compile(r"^\s*([0-9]*\.?[0-9]+)s\s+(?:\w+\s+)?(.+::.+)$")
    with open(durations_file, "r") as f:
        for line in f:
            line = line.strip()
            m = float_re.match(line)
            if m:
                try:
                    duration = float(m.group(1))
                    nodeid = m.group(2).strip()
                    results.append((duration, nodeid))
                except Exception:
                    continue
    return results


def _split_tests(parsed: List[Tuple[float, str]], threshold: float):
    long = [node for dur, node in parsed if dur >= threshold]
    short = [node for dur, node in parsed if dur < threshold]
    return long, short


def _filter_excluded_tests(tests: List[str], exclude_patterns: List[str]) -> List[str]:
    """Filter out tests matching any of the exclude patterns."""
    if not exclude_patterns:
        return tests
    
    filtered = []
    for test in tests:
        excluded = False
        for pattern in exclude_patterns:
            # Check if the pattern matches the test path
            # Support both file patterns (test_fetch.py) and path patterns (tests/test_fetch.py)
            if pattern in test or test.startswith(pattern):
                excluded = True
                break
        if not excluded:
            filtered.append(test)
    return filtered


def quick_run(
    repo_url: str, 
    branch: str, 
    workspace: str, 
    install_mode: str = "pip", 
    install_cmd: str = None,
    install_fallback: bool = False,
    install_repo: str = None,
    install_branch: str = None,
    exclude_tests: list = None,
    threshold: float = 1.0,
    workers: int = 4,
    python_executable: str = None
) -> None:
    """Full workflow:
    - fetch branch into a temporary folder
    - run pytest to collect durations
    - parse durations and run long tests sequentially
    - run remaining (short) tests in parallel
    """
    # Extract repo name from URL for display
    repo = repo_url.split("/")[-1].replace(".git", "")
    
    # Determine install_uv based on install_mode
    install_uv = install_mode == "uv"
    
    # Determine working directory: either a provided workspace (with branch-stamped subdir)
    # or a temporary directory.
    if workspace:
        # Validate workspace path is creatable/writable. If not, fall back to tempdir.
        try:
            os.makedirs(workspace, exist_ok=True)
            from datetime import datetime

            stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            working_dir = os.path.join(workspace, f"quicktester_{branch}_{stamp}")
            # ensure uniqueness
            counter = 0
            base = working_dir
            while os.path.exists(working_dir):
                counter += 1
                working_dir = f"{base}_{counter}"
            os.makedirs(working_dir, exist_ok=True)
        except OSError as e:
            # Likely a permissions/read-only filesystem error; fall back to tempdir
            print(
                f"Warning: cannot create workspace at '{workspace}' ({e}). Falling back to temporary directory."
            )
            workspace = None
            working_dir = tempfile.mkdtemp(prefix=f"quicktester_{branch}_")
    else:
        working_dir = tempfile.mkdtemp(prefix=f"quicktester_{branch}_")

    try:
        # Optionally install code from another repo/branch before running tests
        if install_repo:
            if install_cmd:
                print(
                    f"Installing code from {install_repo}@{install_branch or 'default'} using custom command: '{install_cmd}'"
                )
            else:
                print(
                    f"Installing code from {install_repo}@{install_branch or 'default'} using mode='{install_mode}'"
                )
            res = install_from_git(
                repo=install_repo,
                branch=install_branch,
                mode=install_mode,
                workspace=working_dir,
                use_uv=install_uv,
                install_cmd=install_cmd,
                fallback_on_error=install_fallback,
            )
            if res.get("status") != "ok":
                print(f"Error installing from {install_repo}: {res.get('message')}")
                raise SystemExit(3)
            else:
                print(
                    f"Install succeeded: {res.get('message')}; path={res.get('path')}"
                )

        print(f"Fetching {repo}@{branch} into {working_dir}")
        extracted = fetch_tests(branch=branch, output_dir=working_dir, repo=repo_url)

        tests_path = os.path.join(extracted, "tests")
        if not os.path.exists(tests_path):
            # fallback: maybe repo root contains test files
            tests_path = extracted

        durations_file = os.path.join(extracted, "durations.log")
        print("Collecting test durations (initial run)...")
        _run_pytest_collect_durations(
            tests_path=tests_path, 
            durations_file=durations_file,
            exclude_patterns=exclude_tests,
            python_executable=python_executable
        )

        parsed = _parse_durations(durations_file)
        if not parsed:
            print("No durations parsed; attempting to run full test suite in parallel")
            try:
                if python_executable and python_executable.startswith("uv run"):
                    # Special handling for uv run commands
                    cmd = ["uv", "run", "python", "-m", "pytest", tests_path]
                    # Check if pytest-xdist is available for parallel execution
                    result = subprocess.run(["uv", "run", "python", "-c", "import pytest_xdist"], 
                                          capture_output=True, cwd=extracted)
                    if result.returncode == 0:
                        cmd.extend(["-n", str(workers)])
                    else:
                        print("pytest-xdist not available, running tests sequentially")
                else:
                    # Handle version strings like "3.10" -> "python3.10"
                    if python_executable and python_executable.replace(".", "").isdigit():
                        python_cmd = f"python{python_executable}"
                    else:
                        python_cmd = python_executable if python_executable else "python"
                    # Check if pytest-xdist is available for parallel execution
                    result = subprocess.run([python_cmd, "-c", "import pytest_xdist"], 
                                          capture_output=True, cwd=extracted)
                    
                    if result.returncode == 0:
                        # pytest-xdist is available, use parallel execution
                        cmd = [python_cmd, "-m", "pytest", tests_path, "-n", str(workers)]
                    else:
                        # pytest-xdist not available, run sequentially
                        print("pytest-xdist not available, running tests sequentially")
                        cmd = [python_cmd, "-m", "pytest", tests_path]
                
                if exclude_tests:
                    for pattern in exclude_tests:
                        cmd.extend(["--ignore", pattern])
                subprocess.run(cmd, check=False, cwd=extracted)
            except Exception as e:
                print(f"Warning: Could not run tests: {e}")
                print("This might be normal if the project doesn't have tests or uses a different test framework.")
            return

        long_tests, short_tests = _split_tests(parsed, threshold)
        
        # Filter out excluded tests
        if exclude_tests:
            long_tests = _filter_excluded_tests(long_tests, exclude_tests)
            short_tests = _filter_excluded_tests(short_tests, exclude_tests)

        print(
            f"Found {len(long_tests)} long tests and {len(short_tests)} short tests (threshold={threshold})"
        )

        # Run long tests sequentially
        if long_tests:
            print("Running long tests sequentially to warm caches...")
            if python_executable and python_executable.startswith("uv run"):
                for node in long_tests:
                    try:
                        subprocess.run(["uv", "run", "python", "-m", "pytest", node], cwd=extracted, check=False)
                    except Exception as e:
                        print(f"Warning: Long test {node} failed: {e}")
            else:
                # Handle version strings like "3.10" -> "python3.10"
                if python_executable and python_executable.replace(".", "").isdigit():
                    python_cmd = f"python{python_executable}"
                else:
                    python_cmd = python_executable if python_executable else "python"
                for node in long_tests:
                    try:
                        subprocess.run([python_cmd, "-m", "pytest", node], cwd=extracted, check=False)
                    except Exception as e:
                        print(f"Warning: Long test {node} failed: {e}")

        # Run short tests in parallel
        if short_tests:
            if python_executable and python_executable.startswith("uv run"):
                # Check if pytest-xdist is available for parallel execution
                result = subprocess.run(["uv", "run", "python", "-c", "import pytest_xdist"], 
                                      capture_output=True, cwd=extracted)
                
                if result.returncode == 0:
                    print(
                        f"Running {len(short_tests)} short tests in parallel (workers={workers})"
                    )
                    cmd = ["uv", "run", "python", "-m", "pytest", "-n", str(workers)] + short_tests
                else:
                    print(
                        f"Running {len(short_tests)} short tests sequentially (pytest-xdist not available)"
                    )
                    cmd = ["uv", "run", "python", "-m", "pytest"] + short_tests
            else:
                # Handle version strings like "3.10" -> "python3.10"
                if python_executable and python_executable.replace(".", "").isdigit():
                    python_cmd = f"python{python_executable}"
                else:
                    python_cmd = python_executable if python_executable else "python"
                
                # Check if pytest-xdist is available for parallel execution
                result = subprocess.run([python_cmd, "-c", "import pytest_xdist"], 
                                      capture_output=True, cwd=extracted)
                
                if result.returncode == 0:
                    print(
                        f"Running {len(short_tests)} short tests in parallel (workers={workers})"
                    )
                    cmd = [python_cmd, "-m", "pytest", "-n", str(workers)] + short_tests
                else:
                    print(
                        f"Running {len(short_tests)} short tests sequentially (pytest-xdist not available)"
                    )
                    cmd = [python_cmd, "-m", "pytest"] + short_tests
            
            try:
                subprocess.run(cmd, cwd=extracted, check=False)
            except Exception as e:
                print(f"Warning: Short tests failed: {e}")
        else:
            print("No short tests to run")

    finally:
        # keep the workspace/temporary folder so the user can inspect results; print location
        print(f"Workflow complete. Tests and logs are in: {working_dir}")
