#!/usr/bin/env python3
"""
Apply Type Hints from suggestions JSON to source files.

Reads /tmp/type-hints-suggestions.json (generated by generate-type-hints.py),
applies typed signatures to source files using AST-based function lookup.

Safety: validates each file with py_compile after modification; reverts on failure.
"""

import ast
import json
import py_compile
import sys
from pathlib import Path

PROTECTED_PATTERNS = [
    ".github/workflows/",
    ".env",
    "ods/installers/",
    "ods/ods-cli",
    "ods/config/",
]


def is_protected(file_path: str) -> bool:
    """Check if a file path matches any protected patterns."""
    for pattern in PROTECTED_PATTERNS:
        if file_path.startswith(pattern) or f"/{pattern}" in file_path:
            return True
    return False


def find_function_line(source: str, function_name: str) -> int | None:
    """Use AST to find the actual line number of a function by name."""
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return None

    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if node.name == function_name:
                return node.lineno
    return None


def find_def_end_line(lines: list[str], start_idx: int) -> int:
    """Find the line index where a def statement ends (the line with the colon)."""
    depth = 0
    for i in range(start_idx, len(lines)):
        line = lines[i]
        depth += line.count("(") - line.count(")")
        if depth <= 0 and ":" in line:
            stripped = line.rstrip()
            if stripped.endswith(":"):
                return i
            colon_pos = stripped.rfind(":")
            if colon_pos >= 0:
                return i
    return start_idx


def get_existing_imports(source: str) -> set[str]:
    """Extract all existing import statements from source."""
    imports = set()
    for line in source.splitlines():
        stripped = line.strip()
        if stripped.startswith("import ") or stripped.startswith("from "):
            imports.add(stripped)
    return imports


def find_last_import_line(lines: list[str]) -> int:
    """Find the 0-based index of the last import line in the file."""
    last_import = -1
    for i, line in enumerate(lines):
        stripped = line.strip()
        if stripped.startswith("import ") or stripped.startswith("from "):
            last_import = i
    return last_import


def normalize_import(imp: str) -> list[str]:
    """Normalize an import statement."""
    return [imp.strip()]


def apply_type_hints(suggestions_path: str) -> dict:
    """Apply generated type hint suggestions to Python function signatures."""
    with open(suggestions_path, "r") as f:
        data = json.load(f)

    functions = data.get("functions_annotated", [])
    if not functions:
        print("No type hint suggestions to apply.")
        return {"files_modified": 0, "functions_applied": 0, "files_reverted": 0}

    by_file: dict[str, list] = {}
    for func in functions:
        file_path = func.get("file", "")
        if not file_path:
            continue
        if is_protected(file_path):
            print(f"  Skipping protected file: {file_path}")
            continue
        if not Path(file_path).exists():
            print(f"  Skipping missing file: {file_path}")
            continue
        by_file.setdefault(file_path, []).append(func)

    files_modified = 0
    functions_applied = 0
    files_reverted = 0

    for file_path, file_funcs in by_file.items():
        print(f"\nProcessing {file_path} ({len(file_funcs)} functions)...")

        original_content = Path(file_path).read_text()
        source = original_content
        lines = source.splitlines(keepends=True)

        located = []
        for func in file_funcs:
            actual_line = find_function_line(source, func["function"])
            if actual_line is None:
                print(
                    f"  Could not find function '{func['function']}' in AST, skipping"
                )
                continue
            located.append((actual_line, func))

        located.sort(key=lambda x: x[0], reverse=True)

        applied_in_file = 0
        for actual_line, func in located:
            typed_sig = func.get("typed_signature", "").strip()
            if not typed_sig:
                continue

            start_idx = actual_line - 1
            if start_idx >= len(lines) or start_idx < 0:
                continue

            end_idx = find_def_end_line(lines, start_idx)

            current_line = lines[start_idx]
            indent = current_line[: len(current_line) - len(current_line.lstrip())]

            current_stripped = current_line.lstrip()
            is_async = current_stripped.startswith("async ")

            typed_stripped = typed_sig.lstrip()
            if is_async and not typed_stripped.startswith("async "):
                typed_sig = "async " + typed_sig
            elif not is_async and typed_stripped.startswith("async "):
                typed_sig = typed_sig.replace("async ", "", 1)

            if not typed_sig.rstrip().endswith(":"):
                typed_sig = typed_sig.rstrip() + ":"

            new_line = indent + typed_sig.strip() + "\n"

            lines[start_idx : end_idx + 1] = [new_line]
            applied_in_file += 1
            print(f"  Applied: {func['function']} (line {actual_line})")

        if applied_in_file == 0:
            continue

        new_source = "".join(lines)
        existing_imports = get_existing_imports(new_source)

        imports_to_add = []
        for func in file_funcs:
            for imp in func.get("imports_needed", []):
                for normalized in normalize_import(imp):
                    if normalized not in existing_imports:
                        imports_to_add.append(normalized)
                        existing_imports.add(normalized)

        if imports_to_add:
            lines = new_source.splitlines(keepends=True)
            insert_idx = find_last_import_line(lines)
            if insert_idx >= 0:
                insert_point = insert_idx + 1
            else:
                insert_point = 0
                for i, line in enumerate(lines):
                    stripped = line.strip()
                    if (
                        stripped.startswith("#")
                        or stripped.startswith('"""')
                        or stripped.startswith("'''")
                        or not stripped
                    ):
                        insert_point = i + 1
                    else:
                        break

            import_lines = [imp + "\n" for imp in imports_to_add]
            lines[insert_point:insert_point] = import_lines
            new_source = "".join(lines)

        Path(file_path).write_text(new_source)

        try:
            py_compile.compile(file_path, doraise=True)
            files_modified += 1
            functions_applied += applied_in_file
            print(f"  Validated: {file_path} ({applied_in_file} functions)")
        except py_compile.PyCompileError as e:
            print(f"  REVERT: {file_path} failed compilation: {e}")
            Path(file_path).write_text(original_content)
            files_reverted += 1

    return {
        "files_modified": files_modified,
        "functions_applied": functions_applied,
        "files_reverted": files_reverted,
    }


def main() -> None:
    """Load and apply generated type hints."""
    suggestions_path = (
        sys.argv[1] if len(sys.argv) > 1 else "/tmp/type-hints-suggestions.json"
    )

    if not Path(suggestions_path).exists():
        print(f"Suggestions file not found: {suggestions_path}")
        print("No type hints to apply.")
        return

    print(f"Loading suggestions from {suggestions_path}...")
    result = apply_type_hints(suggestions_path)

    print("\n## Type Hints Application Summary\n")
    print(f"- **Files modified**: {result['files_modified']}")
    print(f"- **Functions updated**: {result['functions_applied']}")
    print(f"- **Files reverted** (compilation failed): {result['files_reverted']}")


if __name__ == "__main__":
    main()
