import argparse
import re
import sys
from pathlib import Path

# --- Custom Version Parser ---

class PLXVersion:
    """Parses <MAJOR>.<MINOR>.<PATCH> strings for comparison."""
    def __init__(self, version_str):
        self.orig = version_str
        try:
            # Splits "1.2.3" into [1, 2, 3]
            self.parts = [int(p) for p in version_str.split('.')]
            while len(self.parts) < 3:
                self.parts.append(0)
        except ValueError:
            print(f"Error: Invalid version format '{version_str}'. Use MAJOR.MINOR.PATCH")
            sys.exit(1)

    def __lt__(self, other):
        return self.parts < other.parts

    def __le__(self, other):
        return self.parts <= other.parts

    def __gt__(self, other):
        return self.parts > other.parts

    def __ge__(self, other):
        return self.parts >= other.parts

    def __eq__(self, other):
        return self.parts == other.parts

    def __repr__(self):
        return self.orig

# --- Migration Registry ---

MIGRATIONS = []

REDIRECTED_TRAIT = "Physics3D.Interactions.ConnectorProperties.Redirected"
REDIRECTED_CONNECTOR = "Physics3D.Interactions.MateConnector"
OLD_REDIRECTED_CONNECTOR = re.compile(
    r"Physics3D\.Interactions\.RedirectedMateConnector\b"
)


def migrate_redirected_connectors(content):
    """Replace the former connector model with a MateConnector trait."""
    code = _mask_openplx_comments_and_strings(content)
    newline = "\r\n" if "\r\n" in content else "\n"
    declaration = re.compile(
        r"^(?P<name>[A-Za-z_]\w*)[ \t]+is[ \t]+"
        r"Physics3D\.Interactions\.RedirectedMateConnector"
        r"(?:[ \t]+with[ \t]+(?P<traits>[^:\r\n]+))?[ \t]*:?[ \t]*$",
        re.MULTILINE,
    )

    def replace_declaration(match):
        traits = REDIRECTED_TRAIT
        if match.group("traits"):
            traits += ", " + match.group("traits").strip()
        original = content[match.start():match.end()]
        comment = " " + original[original.index("#"):].strip() if "#" in original else ""
        return f"{match.group('name')} is {REDIRECTED_CONNECTOR}:{comment}{newline}    with {traits}"

    edits = []
    named_declarations = list(declaration.finditer(code))
    for match in named_declarations:
        edits.append((match.start(), match.end(), replace_declaration(match)))

    instance = re.compile(
        r"\b(?:is|becomes)[ \t]+(?P<type>" + OLD_REDIRECTED_CONNECTOR.pattern +
        r")(?P<with>[ \t]+with[ \t]+)?"
    )
    for match in instance.finditer(code):
        if any(start <= match.start() < end for start, end, _ in edits):
            continue
        line_end = code.find("\n", match.end())
        if line_end < 0:
            line_end = len(code)
        tail = code[match.end():line_end].strip()
        if not match.group("with") and tail and not tail.startswith(":"):
            continue
        replacement = REDIRECTED_CONNECTOR + " with " + REDIRECTED_TRAIT
        if match.group("with"):
            replacement += ", "
            end = match.end()
        else:
            end = match.end("type")
        edits.append((match.start("type"), end, replacement))

    # References, array element types, and unions cannot carry a connector
    # trait. Widen their removed type to MateConnector; instance declarations
    # above retain redirected behavior by adding the trait.
    for match in OLD_REDIRECTED_CONNECTOR.finditer(code):
        if not any(start <= match.start() < end for start, end, _ in edits):
            edits.append((match.start(), match.end(), REDIRECTED_CONNECTOR))

    for start, end, replacement in sorted(edits, reverse=True):
        content = content[:start] + replacement + content[end:]
    return content

def migrate(from_version, to_version):
    """
    Decorator to register a migration function.
    Usage: @migrate("1.0.0", "1.1.0")
    """
    def decorator(func):
        MIGRATIONS.append({
            "from": PLXVersion(from_version),
            "to": PLXVersion(to_version),
            "func": func
        })
        return func
    return decorator

# --- Migration Functions (Add your logic here) ---

@migrate("0.18.10", "0.19.0")
def migrate_v0_18_10_to_v0_19_0(content):
    print("  -> Migrating Charges namespaces to Geometries/Interactions...")

    # 1. Map specific full-path renames
    replacements = {
        "Physics.Charges.LinearElasticMaterial": "Physics.Geometries.LinearElasticMaterial",
        "Physics.Charges.Material": "Physics.Geometries.Material",
        "Physics1D.Charges.LinearConnector": "Physics1D.Interactions.LinearConnector",
        "Physics1D.Charges.RotationalConnector": "Physics1D.Interactions.RotationalConnector",
        "Physics3D.Charges.Box": "Physics3D.Geometries.Box",
        "Physics3D.Charges.Capsule": "Physics3D.Geometries.Capsule",
        "Physics3D.Charges.ContactGeometry": "Physics3D.Geometries.ContactGeometry",
        "Physics3D.Charges.ConvexMesh": "Physics3D.Geometries.ConvexMesh",
        "Physics3D.Charges.Cylinder": "Physics3D.Geometries.Cylinder",
        "Physics3D.Charges.ExternalTriMeshGeometry": "Physics3D.Geometries.ExternalTriMeshGeometry",
        "Physics3D.Charges.Sphere": "Physics3D.Geometries.Sphere",
        "Physics3D.Charges.TriMesh": "Physics3D.Geometries.TriMesh",
        "Physics3D.Charges.TriMeshGeometry": "Physics3D.Geometries.TriMeshGeometry",
        "Physics3D.Charges.AdaptiveMateConnector": "Physics3D.Interactions.AdaptiveMateConnector",
        "Physics3D.Charges.MateConnector": "Physics3D.Interactions.MateConnector",
        "Physics3D.Charges.RedirectedMateConnector": "Physics3D.Interactions.RedirectedMateConnector",
        "Physics3D.Charges.StructuralPlanarConnector": "Physics3D.Interactions.StructuralPlanarConnector",
        "Physics3D.Charges.World": "Physics3D.Interactions.World",
    }

    for old, new in replacements.items():
        content = content.replace(old, new)

    # 2. Rename 'charges' attribute to 'connectors'
    # Use regex with word boundaries to avoid accidentally replacing substrings in other words
    # Matches 'charges:' or '.charges:'
    content = re.sub(r'\bcharges\s*:', 'connectors:', content)
    content = re.sub(r'\.charges\s*:', '.connectors:', content)

    return content

@migrate("0.22.4", "0.23.0")
def migrate_v0_22_4_to_v0_23_0(content):
    print("  -> Migrating MachineModeling bearing namespaces and link length...")

    replacements = {
        "MachineModeling.Bearings.Pair": "MachineModeling.Components.Composite.BearingPair",
        "MachineModeling.Bearings.Base": "MachineModeling.Components.Primitive.Bearing.Base",
    }

    for old, new in replacements.items():
        content = content.replace(old, new)

    content = re.sub(r'\bbearing_separation\b', 'length', content)

    return content

@migrate("0.23.4", "0.24.0")
def migrate_v0_23_4_to_v0_24_0(content):
    print("  -> Migrating Vehicles bundle namespace updates...")

    replacements = {
        "DriveTrain.MechanicalDriveTrains.TwoWheelTemplate": "Vehicles.DriveTrain.TwoWheelDifferential",
        "MechanicalDriveTrains.TwoWheelTemplate": "Vehicles.DriveTrain.TwoWheelDifferential",
        "Vehicles.Bulldozers.Components.DriveTrains.DualTrack": "Vehicles.DriveTrain.DualTrackDifferential",
        "Bulldozers.Components.DriveTrains.DualTrack": "Vehicles.DriveTrain.DualTrackDifferential",
        "MachineModeling.Rippers.": "Vehicles.Ripper.",
        "Vehicles.Bulldozers.": "Vehicles.Bulldozer.",
        "Vehicles.DriveTrains.": "Vehicles.DriveTrain.",
        "Vehicles.Drivetrains.": "Vehicles.DriveTrain.",
        "Vehicles.Rippers.": "Vehicles.Ripper.",
        "Bulldozers.": "Bulldozer.",
        "DriveTrains.": "DriveTrain.",
        "Rippers.": "Ripper.",
        "CoreConnectors": "BaseConnectors",
    }

    for old, new in replacements.items():
        content = content.replace(old, new)

    return content

def _make_unique_import_alias(content, base_alias, reserved_aliases=None):
    reserved_aliases = reserved_aliases or set()
    alias = base_alias
    suffix = 2
    while alias in reserved_aliases or re.search(rf'\bas\s+{re.escape(alias)}\b', content):
        alias = f"{base_alias}{suffix}"
        suffix += 1
    return alias

@migrate("0.24.2", "0.25.0")
def migrate_v0_24_2_to_v0_25_0(content):
    print("  -> Migrating TextureMaterial to Material + BaseColor...")

    import_pattern = re.compile(
        r'(^[ \t]*import\s+@"(?P<path>[^"]+)"\s+as\s+(?P<alias>[A-Za-z_]\w*)\s*$)',
        re.MULTILINE
    )
    known_imports = {match.group("path"): match.group("alias") for match in import_pattern.finditer(content)}
    reserved_aliases = set(known_imports.values())

    block_pattern = re.compile(
        r'(?P<full_block>'
        r'^const\s+(?P<name>[A-Za-z_]\w*)\s+is\s+Visuals\.Materials\.TextureMaterial:\r?\n'
        r'(?P<body>(?:^[ \t]+.*(?:\r?\n|$))*)'
        r')',
        re.MULTILINE
    )

    result = []
    last_end = 0

    for match in block_pattern.finditer(content):
        newline = "\r\n" if "\r\n" in match.group("full_block") else "\n"
        name = match.group("name")
        body_lines = match.group("body").splitlines()

        texture_path = None
        kept_lines = []
        for line in body_lines:
            stripped = line.strip()
            path_match = re.match(r'path\s*:\s*@"([^"]+)"\s*$', stripped)
            if path_match and texture_path is None:
                texture_path = path_match.group(1)
                continue
            kept_lines.append(line)

        if texture_path is None:
            continue

        result.append(content[last_end:match.start()])

        path_alias = known_imports.get(texture_path)
        if path_alias is None:
            path_alias = _make_unique_import_alias(content, f"{name}BaseColorMap", reserved_aliases)
            known_imports[texture_path] = path_alias
            reserved_aliases.add(path_alias)
            result.append(f'import @"{texture_path}" as {path_alias}{newline}')

        migrated_lines = [
            f"const {name} is Visuals.Materials.Material:",
            "    with Visuals.Materials.SurfaceFeatures.BaseColor",
            f"    base_color_map: {path_alias}",
        ]
        migrated_lines.extend(kept_lines)
        result.append(newline.join(migrated_lines) + newline)
        last_end = match.end()

    if not result:
        return content

    result.append(content[last_end:])
    return "".join(result)

@migrate("0.25.7", "0.26.0")
def migrate_v0_25_7_to_v0_26_0(content):
    print("  -> Migrating SurfaceFeatures.Normals to SurfaceFeatures.Normal...")
    return content.replace("SurfaceFeatures.Normals", "SurfaceFeatures.Normal")

@migrate("0.26.8", "0.28.0")
def migrate_v0_26_8_to_v0_28_0(content):
    print("  -> Migrating MachineModeling and adaptive connector API updates...")

    content = re.sub(
        r"\bPhysics3D\.Interactions\.AdaptiveMateConnector\b",
        (
            "Physics3D.Interactions.MateConnector with "
            "Physics3D.Interactions.ConnectorProperties.Adaptive"
        ),
        content
    )
    content = re.sub(
        r"(?<!Physics3D\.)\bInteractions\.AdaptiveMateConnector\b",
        (
            "Interactions.MateConnector with "
            "Physics3D.Interactions.ConnectorProperties.Adaptive"
        ),
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.DualMatedCylinder\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.DualMate",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.DualMatedCylinder\b",
        "Actuators.Linear.Connections.Cylinder.DualMate",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.Properties\.AxialRotationLock\b",
        "MachineModeling.Connections.Properties.AxialRotationLock",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.Properties\.AxialRotationLock\b",
        "Connections.Properties.AxialRotationLock",
        content
    )
    content = re.sub(
        r",\s*MachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.Configurations\.AxialRotationLock\b",
        "",
        content
    )
    content = re.sub(
        r",\s*(?<!MachineModeling\.)Actuators\.Linear\.Connections\.Cylinder\.Configurations\.AxialRotationLock\b",
        "",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.Configurations\.AxialRotationLock\b,\s*",
        "",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.Configurations\.AxialRotationLock\b,\s*",
        "",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.Configurations\.(DisabledHingeAngleOutput|HingeProbeAngleOutput)\b",
        "MachineModeling.Connections.Properties.AxialRotationAngleOutput",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.Configurations\.(DisabledHingeAngleOutput|HingeProbeAngleOutput)\b",
        "Connections.Properties.AxialRotationAngleOutput",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.CylinderBearings\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.Bearings",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.CylinderBearings\b",
        "Actuators.Linear.Connections.Cylinder.Bearings",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.CylinderAxleBearings\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.AxleBearings",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.CylinderAxleBearings\b",
        "Actuators.Linear.Connections.Cylinder.AxleBearings",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.DualBallJointed\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.DualBall",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.DualBallJointed\b",
        "Actuators.Linear.Connections.Cylinder.DualBall",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.DualHinged\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.DualHinged\b",
        "Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.HingedCylinder\b",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.HingedCylinder\b",
        "Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )
    content = re.sub(
        r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\b(?=[ \t:\r\n]|$)",
        "MachineModeling.Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )
    content = re.sub(
        r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\b(?=[ \t:\r\n]|$)",
        "Actuators.Linear.Connections.Cylinder.DualHinge",
        content
    )

    content = re.sub(r"(^[ \t]*)hinge\.", r"\1snap_hinge.", content, flags=re.MULTILINE)

    return content

@migrate("0.28.0", "0.29.0")
def migrate_v0_28_0_to_v0_29_0(content):
    print("  -> Migrating linear cylinder namespaces into Actuators.Linear.Cylinder...")

    replacements = (
        (
            r"\bMachineModeling\.Actuators\.Linear\.Connections\.Cylinder\.Configurations\.RotationalLockConnector\b",
            "MachineModeling.Actuators.Linear.Cylinder.Configurations.RotationalLockConnector",
        ),
        (
            r"(?<!MachineModeling\.)\bActuators\.Linear\.Connections\.Cylinder\.Configurations\.RotationalLockConnector\b",
            "Actuators.Linear.Cylinder.Configurations.RotationalLockConnector",
        ),
        (
            r"\bMachineModeling\.Actuators\.Linear\.CylinderBearings\b",
            "MachineModeling.Actuators.Linear.Cylinder.WithBearings",
        ),
        (
            r"(?<!MachineModeling\.)\bActuators\.Linear\.CylinderBearings\b",
            "Actuators.Linear.Cylinder.WithBearings",
        ),
        (
            r"\bMachineModeling\.Actuators\.Linear\.CylinderPart\b",
            "MachineModeling.Actuators.Linear.Cylinder.Part",
        ),
        (
            r"(?<!MachineModeling\.)\bActuators\.Linear\.CylinderPart\b",
            "Actuators.Linear.Cylinder.Part",
        ),
        (
            r"\bMachineModeling\.Actuators\.Linear\.Cylinder\b(?=[ \t:\r\n]|$)",
            "MachineModeling.Actuators.Linear.Cylinder.Base",
        ),
        (
            r"(?<!MachineModeling\.)\bActuators\.Linear\.Cylinder\b(?=[ \t:\r\n]|$)",
            "Actuators.Linear.Cylinder.Base",
        ),
    )

    for pattern, replacement in replacements:
        content = re.sub(pattern, replacement, content)

    return content

def _mask_openplx_comments_and_strings(content):
    """Keep source offsets and indentation while hiding non-code text."""
    return re.sub(
        r'"""[\s\S]*?"""|"(?:\\.|[^"\\])*"|\#[^\r\n]*',
        lambda match: re.sub(r'[^\r\n]', ' ', match.group()),
        content,
    )


def migrate_connector_array_trait(content):
    """Preserve the connectors field on direct Interaction declarations."""
    code = _mask_openplx_comments_and_strings(content)
    declaration = re.compile(
        r'^(?P<indent>[ \t]*)(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*'
        r'[ \t]+(?:is|becomes)[ \t]+Physics\.Interactions\.Interaction\b'
        r'(?P<tail>[^\r\n]*)',
        re.MULTILINE,
    )
    trait = 'Physics.Interactions.Configurations.ConnectorArray'
    edits = []
    for match in declaration.finditer(code):
        header, colon, value = match.group('tail').partition(':')
        if re.search(r'\b(?:reference|or)\b|\[', header) or value.strip():
            continue

        indent_width = len(match.group('indent').expandtabs(4))
        block_end = match.end()
        body_indent = None
        for line in code[match.end():].splitlines(keepends=True):
            if line.strip():
                indentation = line[:len(line) - len(line.lstrip(' \t'))]
                if len(indentation.expandtabs(4)) <= indent_width:
                    break
                if body_indent is None:
                    body_indent = indentation
            block_end += len(line)
        block = code[match.start():block_end]
        if trait in block or re.search(r'(?m)^[ \t]*connectors[ \t]+(?:is|becomes)\b', block):
            continue

        newline = '\r\n' if content[match.end():].startswith('\r\n') else '\n'
        has_newline = content[match.end():].startswith(newline)
        insert_at = match.end() + (len(newline) if has_newline else 0)
        insertion = (body_indent or match.group('indent') + '    ') + 'with ' + trait
        edits.append((insert_at, insert_at,
                      insertion + newline if has_newline else newline + insertion))
        if not colon:
            colon_at = match.start('tail') + len(header.rstrip())
            edits.append((colon_at, colon_at, ':'))

    for start, end, replacement in sorted(edits, key=lambda edit: edit[:2], reverse=True):
        content = content[:start] + replacement + content[end:]
    return content


@migrate("0.29.0", "0.30.0")
def migrate_v0_29_0_to_v0_30_0(content):
    print("  -> Migrating link connection names and tiltrotator joint configurations...")
    code = _mask_openplx_comments_and_strings(content)
    edits = []
    link_pattern = re.compile(
        r'(?<![\w.])(?:MachineModeling\.)?Components\.Primitive\.Link\.Connections\.'
        r'(?P<name>DualMated|DualHinged)\b'
    )
    for match in link_pattern.finditer(code):
        edits.append((match.start('name'), match.end('name'), match.group('name')[:-1]))

    tiltrotator_pattern = re.compile(
        r'^(?P<indent>[ \t]*)(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*'
        r'[ \t]+(?:is|becomes)[ \t]+'
        r'(?P<type>(?:Vehicles\.)?Excavator\.Components\.Coupler\.Tiltrotator)'
        r'(?![\w.])(?P<tail>[^\r\n]*)',
        re.MULTILINE,
    )
    configuration_pattern = re.compile(
        r'\bwith\b[^:\r\n]*\b(?:Configurations\.)?(?:Hinges|BallJoints)\b'
    )
    for match in tiltrotator_pattern.finditer(code):
        # Include this declaration's indented body when checking for an
        # existing configuration, without inspecting sibling declarations.
        block_end = match.end()
        indent_width = len(match.group('indent').expandtabs(4))
        body_indent = None
        for line in code[match.end():].splitlines(keepends=True):
            if line.strip():
                indentation = line[:len(line) - len(line.lstrip(' \t'))]
                if len(indentation.expandtabs(4)) <= indent_width:
                    break
                if body_indent is None:
                    body_indent = indentation
            block_end += len(line)
        block = code[match.start():block_end]
        if configuration_pattern.search(block) or re.search(
            r'\b(?:left_tilt|right_tilt)[ \t]+becomes\b', block
        ):
            continue
        # Use a body trait: named model declarations do not support the same
        # inline type/trait expressions as instance declarations.
        tail = match.group('tail')
        header_tail, colon, value = tail.partition(':')
        if re.search(r'\b(?:reference|or)\b|\[', header_tail):
            continue
        if value.strip():
            continue  # Inline value assignments need manual review.
        newline = '\r\n' if content[match.end():].startswith('\r\n') else '\n'
        has_newline = content[match.end():].startswith(newline)
        insert_at = match.end() + (len(newline) if has_newline else 0)
        trait = (body_indent or match.group('indent') + '    ') + (
            'with Vehicles.Excavator.Components.Coupler.Configurations.Hinges'
        )
        edits.append((insert_at, insert_at,
                      trait + newline if has_newline else newline + trait))
        if not colon:
            colon_at = match.start('tail') + len(header_tail.rstrip())
            edits.append((colon_at, colon_at, ':'))

    for start, end, replacement in sorted(edits, key=lambda edit: edit[:2], reverse=True):
        content = content[:start] + replacement + content[end:]
    return content


@migrate("0.30.0", "0.31.0")
def migrate_v0_30_0_to_v0_31_0(content):
    print("  -> Migrating connector arrays and redirected MateConnectors to traits...")
    return migrate_redirected_connectors(migrate_connector_array_trait(content))


# --- Core Processing Logic ---

def run_migration_chain(file_path, start_v_str, target_v_str):
    current_v = PLXVersion(start_v_str)
    target_v = PLXVersion(target_v_str)

    # Sort migrations by 'from' version to ensure we step through correctly
    # (e.g., 1.0.0 comes before 1.1.0)
    sorted_migrations = sorted(MIGRATIONS, key=lambda x: x['from'])

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except Exception as e:
        print(f"  [!] Failed to read {file_path}: {e}")
        return

    original_content = content

    for m in sorted_migrations:
        # Include migrations whose destination crosses the current version,
        # even when starting from a patch release after their 'from' version.
        if current_v < m['to'] <= target_v:
            content = m['func'](content)
            current_v = m['to'] # Advance the version pointer

    if content != original_content:
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(content)
        print(f"  [+] Migrated to {current_v}")
    else:
        print(f"  [~] No changes needed.")

# --- CLI ---

def main():
    parser = argparse.ArgumentParser(description="Recursive .openplx Migrator")

    # Using 'dest' to avoid using the reserved 'from' keyword in our code
    parser.add_argument("--from", dest="from_version", required=True, help="Current version (1.0.0)")
    parser.add_argument("--to", dest="to_version", required=True, help="Target version (2.0.0)")
    parser.add_argument("directory", help="Folder to search recursively")

    args = parser.parse_args()

    search_path = Path(args.directory)
    if not search_path.is_dir():
        print(f"Error: {args.directory} is not a valid directory.")
        sys.exit(1)

    # Recursive find
    files = list(search_path.rglob("*.openplx"))

    if not files:
        print("No .openplx files found.")
        return

    print(f"Found {len(files)} files. Starting migration: {args.from_version} -> {args.to_version}")
    print("-" * 50)

    for file_path in files:
        print(f"Processing: {file_path.relative_to(search_path)}")
        run_migration_chain(file_path, args.from_version, args.to_version)

if __name__ == "__main__":
    main()
