import hashlib
import json
import os
import traceback

import unreal


PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MAP_ROOT = "/Game/HyperTwistTraining/Maps"
SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry"
AUTHOR_TAG = "HyperTwistHigherDimensionalTrainingShell"
LEGACY_CLASSIC_AUTHOR_TAG = "HyperTwistClassicCubeTrainingMap"
TRAINING_SHELL_CLASS_PATH = "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingShellActor"
HIGHER_DIMENSIONAL_GAME_MODE_PATH = (
    "/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode"
)
ENABLE_HEADLESS_TRAINING_SHELL_ACTOR = (
    os.getenv("HYPERTWIST_ENABLE_TRAINING_SHELL_ACTOR", "").strip().lower()
    in {"1", "true", "yes"}
)
PRESERVED_LEVEL_FRAMEWORK_CLASS_NAMES = {
    "Brush",
    "DefaultPhysicsVolume",
    "LevelScriptActor",
    "WorldSettings",
}
MANIFEST_RELATIVE_PATH = os.path.join(
    "docs",
    "generated",
    "higher_dimensional_training_maps",
    "phase6c_dedicated_family_map_manifest.json",
)
MANIFEST_ABSOLUTE_PATH = os.path.join(PROJECT_ROOT, MANIFEST_RELATIVE_PATH)
AUTHORING_RECEIPT_DIRECTORY = os.path.join(
    PROJECT_ROOT,
    "UnrealHyperTwist",
    "Saved",
    "HyperTwistMapAuthoring",
)

MAP_CONFIGS = (
    {
        "map_kind": "magic120cell",
        "family_key": "magic120cell",
        "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_Magic120CellTraining",
        "map_file_relative_path": os.path.join(
            "UnrealHyperTwist",
            "Content",
            "HyperTwistTraining",
            "Maps",
            "L_HyperTwist_Magic120CellTraining.umap",
        ),
        "training_shell_id": "phase6c/magic120cell/dedicated-training-shell",
        "title": "Magic120Cell dedicated training shell",
        "summary": (
            "First-party authored dedicated-family training shell for the owned "
            "Magic120Cell higher-dimensional activation lane."
        ),
        "activation_profile_id": "magic120cell-cleanroom-runtime-activation",
        "host_surface_id": "phase6c/magic120cell/runtime-host-surface",
        "launch_surface_id": "phase6c/magic120cell/dedicated-training-launch-surface",
        "view_context_surface_id": "phase6c/magic120cell/dedicated-training-view-context-surface",
        "session_surface_id": "phase6c/magic120cell/dedicated-training-session-surface",
        "interactive_scene_surface_id": "phase6c/magic120cell/interactive-scene-surface",
        "scene_context_id": "phase6c/magic120cell/interactive-scene-context",
        "puzzle_id": "polychoron/magic120cell",
        "runtime_mode_id": "magic120cell-full-color-runtime-v1",
        "projection_profile_id": "magic120cell-4d-projection-distance-v1",
        "primary_persistence_boundary_id": "magic120cell-persistence-boundary",
        "player_start_location": unreal.Vector(-560.0, 0.0, 190.0),
        "shell_location": unreal.Vector(0.0, 0.0, 0.0),
        "shell_rotation": unreal.Rotator(0.0, 22.0, 0.0),
        "training_shell_tags": [
            "phase6c",
            "family:magic120cell",
            "host:dedicated-family-map",
            "projection:magic120cell-4d-projection-distance-v1",
            "persistence:magic120cell-persistence-boundary",
        ],
    },
    {
        "map_kind": "magiccube5d",
        "family_key": "magiccube5d",
        "map_asset_path": f"{MAP_ROOT}/L_HyperTwist_MagicCube5DTraining",
        "map_file_relative_path": os.path.join(
            "UnrealHyperTwist",
            "Content",
            "HyperTwistTraining",
            "Maps",
            "L_HyperTwist_MagicCube5DTraining.umap",
        ),
        "training_shell_id": "phase6c/magiccube5d/dedicated-training-shell",
        "title": "MagicCube5D dedicated training shell",
        "summary": (
            "First-party authored dedicated-family training shell for the owned "
            "MagicCube5D higher-dimensional activation lane."
        ),
        "activation_profile_id": "magiccube5d-cleanroom-runtime-activation",
        "host_surface_id": "phase6c/magiccube5d/runtime-host-surface",
        "launch_surface_id": "phase6c/magiccube5d/dedicated-training-launch-surface",
        "view_context_surface_id": "phase6c/magiccube5d/dedicated-training-view-context-surface",
        "session_surface_id": "phase6c/magiccube5d/dedicated-training-session-surface",
        "interactive_scene_surface_id": "phase6c/magiccube5d/interactive-scene-surface",
        "scene_context_id": "phase6c/magiccube5d/interactive-scene-context",
        "puzzle_id": "hypercube/magiccube5d/order3",
        "runtime_mode_id": "magiccube5d-order3-runtime-v1",
        "projection_profile_id": "magiccube5d-5d-projection-distance-v1",
        "primary_persistence_boundary_id": "magiccube5d-persistence-boundary",
        "player_start_location": unreal.Vector(-620.0, -120.0, 200.0),
        "shell_location": unreal.Vector(0.0, 0.0, 0.0),
        "shell_rotation": unreal.Rotator(0.0, -18.0, 0.0),
        "training_shell_tags": [
            "phase6c",
            "family:magiccube5d",
            "host:dedicated-family-map",
            "projection:magiccube5d-5d-projection-distance-v1",
            "persistence:magiccube5d-persistence-boundary",
        ],
    },
)


def log(message: str) -> None:
    unreal.log(f"[HyperTwistHigherDimensionalMapAuthoring] {message}")


def require_class(class_path: str):
    loaded_class = unreal.load_class(None, class_path)
    if loaded_class is None:
        raise RuntimeError(f"Required class was not found: {class_path}")
    return loaded_class


def ensure_directory(directory_path: str) -> None:
    if not unreal.EditorAssetLibrary.does_directory_exist(directory_path):
        if not unreal.EditorAssetLibrary.make_directory(directory_path):
            raise RuntimeError(f"Failed to create content directory: {directory_path}")


def spawn_actor(actor_class, label: str, location: unreal.Vector, rotation: unreal.Rotator):
    actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    if actor_subsystem is None:
        raise RuntimeError("EditorActorSubsystem was not available.")

    actor = actor_subsystem.spawn_actor_from_class(actor_class, location, rotation)
    if actor is None:
        raise RuntimeError(f"Failed to spawn actor: {label}")

    actor.set_actor_label(label)
    actor.set_editor_property("tags", [unreal.Name(AUTHOR_TAG)])
    return actor


def build_map_object_path(map_asset_path: str) -> str:
    asset_name = map_asset_path.rsplit("/", 1)[-1]
    return f"{map_asset_path}.{asset_name}"


def map_package_exists(config) -> bool:
    map_asset_path = config["map_asset_path"]
    map_file_absolute_path = resolve_map_file_absolute_path(config)
    map_object_path = build_map_object_path(map_asset_path)
    return (
        os.path.exists(map_file_absolute_path)
        or unreal.EditorAssetLibrary.does_asset_exist(map_asset_path)
        or unreal.EditorAssetLibrary.does_asset_exist(map_object_path)
    )


def delete_existing_level(level_subsystem, config) -> bool:
    map_asset_path = config["map_asset_path"]
    if not map_package_exists(config):
        return False

    if not level_subsystem.load_level(SAFE_TRANSIENT_LEVEL_PATH):
        raise RuntimeError(
            f"Failed to load transient level '{SAFE_TRANSIENT_LEVEL_PATH}' before rebuilding "
            f"'{map_asset_path}'."
        )

    map_object_path = build_map_object_path(map_asset_path)
    deleted_via_asset_api = False
    for candidate_path in (map_object_path, map_asset_path):
        if unreal.EditorAssetLibrary.does_asset_exist(candidate_path):
            deleted_via_asset_api = unreal.EditorAssetLibrary.delete_asset(candidate_path)
            if deleted_via_asset_api:
                log(f"Deleted existing level asset {candidate_path} before rebuild")
                break

    map_file_absolute_path = resolve_map_file_absolute_path(config)
    if os.path.exists(map_file_absolute_path):
        os.remove(map_file_absolute_path)
        log(f"Removed existing level file {map_file_absolute_path} before rebuild")
        return True

    if deleted_via_asset_api:
        return True

    raise RuntimeError(f"Failed to delete existing level package: {map_asset_path}")


def recreate_level(level_subsystem, config) -> None:
    map_asset_path = config["map_asset_path"]
    delete_existing_level(level_subsystem, config)

    if not level_subsystem.new_level(map_asset_path):
        raise RuntimeError(f"Failed to create level: {map_asset_path}")

    log(f"Created new level {map_asset_path}")


def should_preserve_existing_actor(actor) -> bool:
    actor_class = actor.get_class()
    actor_class_name = actor_class.get_name() if actor_class is not None else ""
    return actor_class_name in PRESERVED_LEVEL_FRAMEWORK_CLASS_NAMES


def clear_existing_training_map_actors() -> None:
    actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    if actor_subsystem is None:
        raise RuntimeError("EditorActorSubsystem was not available.")

    author_tags = {AUTHOR_TAG, LEGACY_CLASSIC_AUTHOR_TAG}
    destroyed_tagged_count = 0
    destroyed_fallback_count = 0

    for actor in actor_subsystem.get_all_level_actors():
        if actor is None:
            continue

        if should_preserve_existing_actor(actor):
            continue

        actor_tags = {str(tag) for tag in actor.tags}
        if not actor_tags.isdisjoint(author_tags):
            if actor_subsystem.destroy_actor(actor):
                destroyed_tagged_count += 1
            continue

        if actor_subsystem.destroy_actor(actor):
            destroyed_fallback_count += 1

    log(
        "Removed "
        f"{destroyed_tagged_count} tagged and {destroyed_fallback_count} fallback actors "
        "before reauthoring"
    )


def save_current_level_or_raise(level_subsystem, label: str) -> None:
    if not level_subsystem.save_current_level():
        raise RuntimeError(f"Failed to save level after {label}.")
    log(f"Saved current level after {label}")


def configure_world_settings(game_mode_class_path: str) -> None:
    editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
    if editor_subsystem is None:
        raise RuntimeError("UnrealEditorSubsystem was not available.")

    world = editor_subsystem.get_editor_world()
    if world is None:
        raise RuntimeError("Editor world was not available after loading the target level.")

    world_settings = world.get_world_settings()
    if world_settings is None:
        raise RuntimeError("World settings were not available for the target level.")

    world_settings.set_editor_property("default_game_mode", require_class(game_mode_class_path))


def ensure_player_start(config) -> None:
    player_start = spawn_actor(
        unreal.PlayerStart,
        f"HT_{config['family_key']}_DedicatedSurfaceAnchor",
        config["player_start_location"],
        unreal.Rotator(0.0, 0.0, 0.0),
    )
    anchor_tags = [
        AUTHOR_TAG,
        f"training-shell-id:{config['training_shell_id']}",
        f"activation-profile:{config['activation_profile_id']}",
        f"host-surface:{config['host_surface_id']}",
        f"launch-surface:{config['launch_surface_id']}",
        f"view-context-surface:{config['view_context_surface_id']}",
        f"session-surface:{config['session_surface_id']}",
        f"interactive-scene-surface:{config['interactive_scene_surface_id']}",
        f"scene-context:{config['scene_context_id']}",
        f"puzzle:{config['puzzle_id']}",
        f"runtime-mode:{config['runtime_mode_id']}",
        f"projection:{config['projection_profile_id']}",
        f"persistence:{config['primary_persistence_boundary_id']}",
        "ownership:dedicated-family",
    ]
    anchor_tags.extend(config["training_shell_tags"])
    player_start.set_editor_property(
        "tags",
        [unreal.Name(tag) for tag in dict.fromkeys(anchor_tags)],
    )


def ensure_training_shell_actor(config) -> None:
    shell_class = require_class(TRAINING_SHELL_CLASS_PATH)
    shell_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
        shell_class,
        config["shell_location"],
        config["shell_rotation"],
    )
    if shell_actor is None:
        raise RuntimeError(
            f"Failed to spawn dedicated training shell actor for {config['family_key']}."
        )

    shell_actor.set_actor_label(f"HT_{config['family_key']}_TrainingShell")
    shell_actor.set_editor_property("family_key", config["family_key"])
    shell_actor.set_editor_property("training_shell_id", config["training_shell_id"])
    shell_actor.set_editor_property("title", config["title"])
    shell_actor.set_editor_property("summary", config["summary"])
    shell_actor.set_editor_property("map_asset_path", config["map_asset_path"])
    shell_actor.set_editor_property("activation_profile_id", config["activation_profile_id"])
    shell_actor.set_editor_property("host_surface_id", config["host_surface_id"])
    shell_actor.set_editor_property("launch_surface_id", config["launch_surface_id"])
    shell_actor.set_editor_property("view_context_surface_id", config["view_context_surface_id"])
    shell_actor.set_editor_property("session_surface_id", config["session_surface_id"])
    shell_actor.set_editor_property(
        "interactive_scene_surface_id",
        config["interactive_scene_surface_id"],
    )
    shell_actor.set_editor_property("scene_context_id", config["scene_context_id"])
    shell_actor.set_editor_property("puzzle_id", config["puzzle_id"])
    shell_actor.set_editor_property("runtime_mode_id", config["runtime_mode_id"])
    shell_actor.set_editor_property("projection_profile_id", config["projection_profile_id"])
    shell_actor.set_editor_property(
        "primary_persistence_boundary_id",
        config["primary_persistence_boundary_id"],
    )
    shell_actor.set_editor_property(
        "authoring_manifest_relative_path",
        MANIFEST_RELATIVE_PATH.replace("\\", "/"),
    )
    shell_actor.set_editor_property("dedicated_family_ownership", True)
    shell_actor.set_editor_property("training_shell_tags", config["training_shell_tags"])
    shell_tags = [
        AUTHOR_TAG,
        f"training-shell-id:{config['training_shell_id']}",
        f"activation-profile:{config['activation_profile_id']}",
        f"host-surface:{config['host_surface_id']}",
        f"launch-surface:{config['launch_surface_id']}",
        f"view-context-surface:{config['view_context_surface_id']}",
        f"session-surface:{config['session_surface_id']}",
        f"interactive-scene-surface:{config['interactive_scene_surface_id']}",
        f"scene-context:{config['scene_context_id']}",
        f"puzzle:{config['puzzle_id']}",
        f"runtime-mode:{config['runtime_mode_id']}",
        f"projection:{config['projection_profile_id']}",
        f"persistence:{config['primary_persistence_boundary_id']}",
        "ownership:dedicated-family",
    ]
    shell_tags.extend(config["training_shell_tags"])
    shell_actor.set_editor_property(
        "tags",
        [unreal.Name(tag) for tag in dict.fromkeys(shell_tags)],
    )
    try:
        shell_actor.rerun_construction_scripts()
    except Exception:
        log(
            "Training shell actor construction rerun was unavailable; "
            "continuing with direct metadata state."
        )


def resolve_map_file_absolute_path(config) -> str:
    return os.path.join(PROJECT_ROOT, config["map_file_relative_path"])


def compute_md5(file_path: str) -> str:
    digest = hashlib.md5()
    with open(file_path, "rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def get_target_receipt_absolute_path(config) -> str:
    return os.path.join(
        AUTHORING_RECEIPT_DIRECTORY,
        f"phase6c_{config['map_kind']}_complete.json",
    )


def validate_authored_map(config):
    actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
    if actor_subsystem is None or editor_subsystem is None:
        raise RuntimeError("Editor subsystems were unavailable during authored-map validation.")

    actor_labels = {
        actor.get_actor_label()
        for actor in actor_subsystem.get_all_level_actors()
        if actor is not None
    }
    expected_actor_labels = {f"HT_{config['family_key']}_DedicatedSurfaceAnchor"}
    missing_actor_labels = sorted(expected_actor_labels - actor_labels)
    if missing_actor_labels:
        raise RuntimeError(
            "Authored map is missing required presentation actors: "
            + ", ".join(missing_actor_labels)
        )

    world = editor_subsystem.get_editor_world()
    world_settings = world.get_world_settings() if world is not None else None
    game_mode_class = (
        world_settings.get_editor_property("default_game_mode")
        if world_settings is not None
        else None
    )
    if game_mode_class is None or game_mode_class.get_name() != "HyperTwistHigherDimensionalTrainingGameMode":
        raise RuntimeError("Authored map does not own the dedicated higher-dimensional game mode.")

    return sorted(expected_actor_labels)


def write_target_completion_receipt(config, actor_labels) -> None:
    map_file_absolute_path = resolve_map_file_absolute_path(config)
    if not os.path.exists(map_file_absolute_path):
        raise RuntimeError(
            f"Cannot write completion receipt because the authored map is missing: {map_file_absolute_path}"
        )

    os.makedirs(AUTHORING_RECEIPT_DIRECTORY, exist_ok=True)
    receipt_path = get_target_receipt_absolute_path(config)
    receipt = {
        "schemaVersion": "hypertwist/phase6c-map-authoring-receipt/v1",
        "mapKind": config["map_kind"],
        "mapAssetPath": config["map_asset_path"],
        "mapHashMd5": compute_md5(map_file_absolute_path),
        "gameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
        "presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
        "validatedActorLabels": actor_labels,
        "authoringComplete": True,
    }
    with open(receipt_path, "w", encoding="utf-8") as handle:
        json.dump(receipt, handle, indent=2)
        handle.write("\n")
    log(f"Wrote hash-bound completion receipt to {receipt_path}")


def ensure_manifest_directory() -> None:
    os.makedirs(os.path.dirname(MANIFEST_ABSOLUTE_PATH), exist_ok=True)


def build_manifest_entries():
    entries = []
    for config in MAP_CONFIGS:
        map_file_absolute_path = resolve_map_file_absolute_path(config)
        if not os.path.exists(map_file_absolute_path):
            continue

        entries.append(
            {
                "mapKind": config["map_kind"],
                "familyKey": config["family_key"],
                "mapAssetPath": config["map_asset_path"],
                "mapFileRelativePath": config["map_file_relative_path"].replace("\\", "/"),
                "mapHashMd5": compute_md5(map_file_absolute_path),
                "trainingShellId": config["training_shell_id"],
                "activationProfileId": config["activation_profile_id"],
                "hostSurfaceId": config["host_surface_id"],
                "launchSurfaceId": config["launch_surface_id"],
                "viewContextSurfaceId": config["view_context_surface_id"],
                "sessionSurfaceId": config["session_surface_id"],
                "interactiveSceneSurfaceId": config["interactive_scene_surface_id"],
                "sceneContextId": config["scene_context_id"],
                "puzzleId": config["puzzle_id"],
                "runtimeModeId": config["runtime_mode_id"],
                "projectionProfileId": config["projection_profile_id"],
                "primaryPersistenceBoundaryId": config["primary_persistence_boundary_id"],
                "authorTag": AUTHOR_TAG,
                "authoringManifestRelativePath": MANIFEST_RELATIVE_PATH.replace("\\", "/"),
                "authoredViaGameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
                "runtimePresentationClassPath": TRAINING_SHELL_CLASS_PATH,
                "presentationEnvironmentOwnership": "runtime-game-mode-and-shell-components",
                "canonicalRenderableElementCount": (
                    120 if config["family_key"] == "magic120cell" else 242
                ),
                "trainingShellTags": config["training_shell_tags"],
            }
        )
    return entries


def write_manifest() -> None:
    ensure_manifest_directory()
    classic_map_absolute_path = os.path.join(
        PROJECT_ROOT,
        "UnrealHyperTwist",
        "Content",
        "HyperTwistTraining",
        "Maps",
        "L_HyperTwist_ClassicTraining.umap",
    )
    manifest = {
        "manifestId": "phase6c/dedicated-family-training-map-authoring",
        "manifestVersion": "2026.07.19",
        "authorTag": AUTHOR_TAG,
        "authoringScriptRelativePath": "scripts/hypertwist_author_higher_dimensional_training_maps.py",
        "authoredThroughGameModeClassPath": HIGHER_DIMENSIONAL_GAME_MODE_PATH,
        "classicReferenceMapHashMd5": (
            compute_md5(classic_map_absolute_path)
            if os.path.exists(classic_map_absolute_path)
            else ""
        ),
        "entries": build_manifest_entries(),
    }
    with open(MANIFEST_ABSOLUTE_PATH, "w", encoding="utf-8") as handle:
        json.dump(manifest, handle, indent=2)
        handle.write("\n")
    log(f"Wrote higher-dimensional training-map manifest to {MANIFEST_ABSOLUTE_PATH}")


def author_map(config) -> None:
    level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
    if level_subsystem is None:
        raise RuntimeError("LevelEditorSubsystem was not available.")

    log(f"Starting authored rebuild for {config['map_asset_path']}")
    recreate_level(level_subsystem, config)
    log("Created clean target level")
    save_current_level_or_raise(level_subsystem, "clean level recreation")
    configure_world_settings(HIGHER_DIMENSIONAL_GAME_MODE_PATH)
    log("Configured dedicated higher-dimensional runtime game mode")
    save_current_level_or_raise(
        level_subsystem,
        f"{config['family_key']} dedicated training-shell game mode",
    )
    ensure_player_start(config)
    save_current_level_or_raise(level_subsystem, "dedicated surface-anchor placement")
    log(
        "Placed dedicated surface anchor; floor, lighting, and projection presentation "
        "are self-healing runtime-owned surfaces"
    )
    if ENABLE_HEADLESS_TRAINING_SHELL_ACTOR:
        ensure_training_shell_actor(config)
        save_current_level_or_raise(level_subsystem, "dedicated training-shell actor placement")
        log("Placed dedicated training shell actor")
    else:
        log(
            "Skipped dedicated training shell actor on the headless authoring lane; "
            "the dedicated game mode owns deterministic runtime spawning"
        )
    save_current_level_or_raise(
        level_subsystem,
        f"{config['family_key']} final dedicated shell surfaces",
    )
    validated_actor_labels = validate_authored_map(config)
    write_target_completion_receipt(config, validated_actor_labels)
    log(f"Finished authoring {config['map_asset_path']}")


def main() -> None:
    ensure_directory(MAP_ROOT)
    requested_map_kind = os.getenv("HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND", "").strip().lower()
    write_manifest_requested = requested_map_kind in ("", "all", "both")
    if requested_map_kind in ("", "all", "both"):
        targets = MAP_CONFIGS
    else:
        targets = tuple(
            config for config in MAP_CONFIGS if config["map_kind"] == requested_map_kind
        )
        if not targets:
            raise RuntimeError(
                f"Unsupported HYPERTWIST_HIGHER_DIMENSIONAL_MAP_KIND value: {requested_map_kind}"
            )

    for config in targets:
        author_map(config)

    if write_manifest_requested:
        write_manifest()

    log("Higher-dimensional dedicated-family authored maps are ready.")


if __name__ == "__main__":
    try:
        main()
    except Exception as error:
        unreal.log_error(
            f"[HyperTwistHigherDimensionalMapAuthoring] {error}\n{traceback.format_exc()}"
        )
        raise
