from __future__ import annotations

import argparse
import ast
from pathlib import Path


MARKER = "    # PRESERVATION_METHODS\n"


def method_span(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[int, int]:
    start = min((decorator.lineno for decorator in node.decorator_list), default=node.lineno)
    if node.end_lineno is None:
        raise ValueError(f"Missing end position for {node.name}")
    return start - 1, node.end_lineno


def move_methods(
    source_path: Path,
    class_name: str,
    target_path: Path,
    method_names: list[str],
) -> None:
    source = source_path.read_text(encoding="utf-8")
    tree = ast.parse(source, filename=str(source_path))
    owner = next(
        (
            node
            for node in tree.body
            if isinstance(node, ast.ClassDef) and node.name == class_name
        ),
        None,
    )
    if owner is None:
        raise ValueError(f"Class not found: {class_name}")

    methods = {
        node.name: node
        for node in owner.body
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    }
    missing = [name for name in method_names if name not in methods]
    if missing:
        raise ValueError(f"Methods not found in {class_name}: {', '.join(missing)}")

    lines = source.splitlines(keepends=True)
    ordered = sorted((methods[name] for name in method_names), key=lambda node: node.lineno)
    blocks = ["".join(lines[start:end]).rstrip() for start, end in map(method_span, ordered)]

    for start, end in sorted(map(method_span, ordered), reverse=True):
        del lines[start:end]
        while start < len(lines) - 1 and lines[start] == "\n" and lines[start + 1] == "\n":
            del lines[start]

    target = target_path.read_text(encoding="utf-8")
    if target.count(MARKER) != 1:
        raise ValueError(f"Target must contain exactly one method marker: {target_path}")
    target = target.replace(MARKER, "\n\n".join(blocks) + "\n")

    source_path.write_text("".join(lines), encoding="utf-8")
    target_path.write_text(target, encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description="Mechanically move class methods between modules")
    parser.add_argument("--source", type=Path, required=True)
    parser.add_argument("--class-name", required=True)
    parser.add_argument("--target", type=Path, required=True)
    parser.add_argument("methods", nargs="+")
    args = parser.parse_args()
    move_methods(args.source, args.class_name, args.target, args.methods)


if __name__ == "__main__":
    main()

