import json
import logging
import os
import subprocess
import time
from typing import Any

from py_agent.infrastructure.pytest_runner import get_pytest_command

logger = logging.getLogger(__name__)


def find_test_files(project_path: str) -> list[str]:
    test_files = []
    for root, _, files in os.walk(project_path):
        for file in files:
            if (file.startswith("test_") and file.endswith(".py")) or file.endswith("_test.py"):
                test_files.append(os.path.join(root, file))
    return test_files


def get_pytest_command_with_coverage(
    project_path: str, coverage_report_path: str
) -> list[str] | None:
    command = get_pytest_command(project_path)
    if not command:
        return None
    command.extend(
        [
            "--cov=" + project_path,
            "--cov-report=json:" + coverage_report_path,
        ]
    )
    return command


def wait_for_valid_coverage_file(
    coverage_report_path: str, command_result: subprocess.CompletedProcess[str]
) -> dict[str, Any]:
    max_attempts = 5
    attempt = 0
    delay = 0.5

    while attempt < max_attempts:
        attempt += 1
        time.sleep(delay)
        try:
            if not os.path.exists(coverage_report_path):
                logger.info(f"Coverage file not yet created, attempt {attempt}")
            else:
                with open(coverage_report_path) as f:
                    content = f.read()
                if not content.strip():
                    logger.info(f"Coverage file is empty, attempt {attempt}")
                else:
                    json.loads(content)
                    logger.info("Successfully validated JSON, file is ready")
                    return {"success": True}
        except json.JSONDecodeError as e:
            logger.info(f"Invalid JSON in coverage file, attempt {attempt}: {e}")
        except Exception as e:
            logger.info(f"Error reading coverage file, attempt {attempt}: {e}")

    stderr_snippet = (command_result.stderr or "").strip()[:200]
    error_message = f"Failed to generate valid coverage JSON after {attempt} attempts"
    if stderr_snippet:
        error_message += f": {stderr_snippet}"
    logger.error(error_message)

    return {
        "success": False,
        "error": error_message,
    }


def run_coverage(project_path: str, coverage_report_path: str) -> dict[str, Any]:
    try:
        os.makedirs(os.path.dirname(coverage_report_path), exist_ok=True)

        pytest_command = get_pytest_command(project_path)
        if pytest_command is None:
            return {"success": False, "error": "Could not find pytest in the environment"}

        command = get_pytest_command_with_coverage(project_path, coverage_report_path)
        if not command:
            raise RuntimeError("Unable to locate pytest or pytest-cov")

        test_files = find_test_files(project_path)
        if not test_files:
            return {
                "success": False,
                "error": "No test files found in the project. Coverage requires test files to generate reports.",
            }

        logger.info(f"pytest coverage command: {' '.join(command)}")
        result = subprocess.run(
            command,
            cwd=project_path,
            capture_output=True,
            text=True,
            env=os.environ.copy(),
        )

        return wait_for_valid_coverage_file(coverage_report_path, result)

    except Exception as e:
        logger.exception("Error generating coverage report")
        return {"success": False, "error": str(e)}


def run_targeted_coverage(
    project_path: str,
    coverage_report_path: str,
    source_files: list[str],
    test_files: list[str],
) -> dict[str, Any]:
    """Run coverage for specific source files using only the given test files."""
    try:
        os.makedirs(os.path.dirname(coverage_report_path), exist_ok=True)

        command = get_pytest_command(project_path)
        if not command:
            return {"success": False, "error": "Could not find pytest in the environment"}

        # Use parent directories of source files for --cov (not individual files,
        # because coverage.py needs importable package paths, not file paths).
        source_dirs = {os.path.relpath(os.path.dirname(src), project_path) for src in source_files}
        for d in sorted(source_dirs):
            command.append(f"--cov={d}")

        command.append(f"--cov-report=json:{coverage_report_path}")
        command.append("--no-header")
        command.append("-q")

        # Run only the specified test files
        for tf in test_files:
            command.append(os.path.relpath(tf, project_path))

        logger.info(f"targeted coverage command: {' '.join(command)}")
        result = subprocess.run(
            command,
            cwd=project_path,
            capture_output=True,
            text=True,
            env=os.environ.copy(),
        )

        # Detect missing pytest-cov early instead of waiting for a file that won't exist
        if result.returncode != 0 and "no module named pytest_cov" in (result.stderr or "").lower():
            return {
                "success": False,
                "error": "pytest-cov is not installed. Install it with: pip install pytest-cov",
            }

        return wait_for_valid_coverage_file(coverage_report_path, result)

    except Exception as e:
        logger.exception("Error generating targeted coverage report")
        return {"success": False, "error": str(e)}


def parse_coverage_report(
    coverage_report_path: str,
    project_path: str,
    source_files: list[str] | None = None,
) -> dict[str, Any]:
    """Parse coverage JSON into the CoverageReport format used by ts-agent/early-cli.

    Returns a dict keyed by relative file path (plus a ``"/"`` root entry)
    where each value has ``percentage``, ``totalStatements``, and
    ``coveredStatements``.

    When *source_files* is provided, only those files are included and the
    ``"/"`` totals are recomputed from them.
    """
    with open(coverage_report_path, encoding="utf-8") as f:
        report: dict[str, Any] = json.load(f)

    # Build a set of relative paths to filter on
    target_rels: set[str] | None = None
    if source_files:
        target_rels = {
            os.path.relpath(sf, project_path) if os.path.isabs(sf) else sf for sf in source_files
        }

    coverage_tree: dict[str, Any] = {}
    total_statements = 0
    total_covered = 0

    for file_path, data in report.get("files", {}).items():
        summary = data.get("summary", {})
        rel_path = (
            os.path.relpath(file_path, project_path) if os.path.isabs(file_path) else file_path
        )

        if target_rels is not None and rel_path not in target_rels:
            continue

        stmts = summary.get("num_statements", 0)
        covered = summary.get("covered_lines", 0)
        coverage_tree[rel_path] = {
            "percentage": summary.get("percent_covered", 0),
            "totalStatements": stmts,
            "coveredStatements": covered,
        }
        total_statements += stmts
        total_covered += covered

    coverage_tree["/"] = {
        "percentage": (total_covered / total_statements * 100) if total_statements > 0 else 0,
        "totalStatements": total_statements,
        "coveredStatements": total_covered,
    }

    return coverage_tree


def convert_coverage_to_json(coverage_db_path: str, coverage_report_path: str) -> dict[str, Any]:
    try:
        os.makedirs(os.path.dirname(coverage_report_path), exist_ok=True)

        command = get_pytest_command(os.path.dirname(coverage_db_path))
        if not command:
            return {
                "success": False,
                "error": "Missing required package: coverage",
                "data": {"missing_packages": ["coverage"]},
            }

        command = [command[0], "-m", "coverage", "json", "-o", coverage_report_path]

        result = subprocess.run(
            command,
            cwd=os.path.dirname(coverage_db_path),
            capture_output=True,
            text=True,
        )

        if os.path.exists(coverage_report_path):
            return {"success": True}
        else:
            return {
                "success": False,
                "error": "Failed to convert .coverage to JSON format",
                "data": {
                    "exit_code": result.returncode,
                    "stdout": result.stdout,
                    "stderr": result.stderr,
                },
            }

    except Exception as e:
        logger.exception("Error converting coverage to JSON")
        return {"success": False, "error": str(e)}
