import os
import json
import datetime
import traceback

import unreal


MAP_ROOT = "/Game/HyperTwistTraining/Maps"
MATERIAL_ROOT = "/Game/HyperTwistTraining/Materials"
CLASSIC_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_ClassicTraining"
FOLLOW_ALONG_MAP_PATH = f"{MAP_ROOT}/L_HyperTwist_FollowAlongTraining"
AUTHOR_TAG = "HyperTwistClassicCubeTrainingMap"
AUTHORING_MARKER_PATH = os.getenv("HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER", "").strip()
SAFE_TRANSIENT_LEVEL_PATH = "/Engine/Maps/Entry"
MASTER_MATERIAL_PATH = f"{MATERIAL_ROOT}/M_HT_ClassicCubeFaceMaster"
INTERNAL_MATERIAL_PATH = f"{MATERIAL_ROOT}/MI_HT_ClassicCube_Internal"
FACE_MATERIAL_SPECS = (
    ("MI_HT_ClassicCube_Up", unreal.LinearColor(1.0, 1.0, 1.0, 1.0)),
    ("MI_HT_ClassicCube_Down", unreal.LinearColor(1.0, 0.92, 0.1, 1.0)),
    ("MI_HT_ClassicCube_Front", unreal.LinearColor(0.0, 0.55, 0.2, 1.0)),
    ("MI_HT_ClassicCube_Back", unreal.LinearColor(0.0, 0.22, 0.75, 1.0)),
    ("MI_HT_ClassicCube_Left", unreal.LinearColor(0.92, 0.36, 0.02, 1.0)),
    ("MI_HT_ClassicCube_Right", unreal.LinearColor(0.78, 0.04, 0.04, 1.0)),
)


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


def warn(message: str) -> None:
    unreal.log_warning(f"[HyperTwistClassicCubeMapAuthoring] {message}")


def require_asset(asset_path: str):
    asset = unreal.EditorAssetLibrary.load_asset(asset_path)
    if asset is None:
        raise RuntimeError(f"Required asset was not found: {asset_path}")
    return asset


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 split_asset_path(asset_path: str):
    package_path, asset_name = asset_path.rsplit("/", 1)
    return package_path, asset_name


def create_asset(asset_path: str, asset_class, factory):
    package_path, asset_name = split_asset_path(asset_path)
    ensure_directory(package_path)
    asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
    asset = asset_tools.create_asset(asset_name, package_path, asset_class, factory)
    if asset is None:
        raise RuntimeError(f"Failed to create asset: {asset_path}")
    return asset


def ensure_face_master_material():
    if unreal.EditorAssetLibrary.does_asset_exist(MASTER_MATERIAL_PATH):
        material = require_asset(MASTER_MATERIAL_PATH)
    else:
        material = create_asset(
            MASTER_MATERIAL_PATH,
            unreal.Material,
            unreal.MaterialFactoryNew(),
        )

        base_color = unreal.MaterialEditingLibrary.create_material_expression(
            material,
            unreal.MaterialExpressionVectorParameter,
            -640,
            -120,
        )
        base_color.set_editor_property("parameter_name", "BaseColor")
        base_color.set_editor_property("default_value", unreal.LinearColor(1.0, 1.0, 1.0, 1.0))

        glow_strength = unreal.MaterialEditingLibrary.create_material_expression(
            material,
            unreal.MaterialExpressionScalarParameter,
            -640,
            80,
        )
        glow_strength.set_editor_property("parameter_name", "GlowStrength")
        glow_strength.set_editor_property("default_value", 0.0)

        face_opacity = unreal.MaterialEditingLibrary.create_material_expression(
            material,
            unreal.MaterialExpressionScalarParameter,
            -640,
            240,
        )
        face_opacity.set_editor_property("parameter_name", "FaceOpacity")
        face_opacity.set_editor_property("default_value", 1.0)

        emissive = unreal.MaterialEditingLibrary.create_material_expression(
            material,
            unreal.MaterialExpressionMultiply,
            -280,
            0,
        )
        unreal.MaterialEditingLibrary.connect_material_expressions(base_color, "", emissive, "A")
        unreal.MaterialEditingLibrary.connect_material_expressions(glow_strength, "", emissive, "B")
        unreal.MaterialEditingLibrary.connect_material_property(
            base_color,
            "",
            unreal.MaterialProperty.MP_BASE_COLOR,
        )
        unreal.MaterialEditingLibrary.connect_material_property(
            emissive,
            "",
            unreal.MaterialProperty.MP_EMISSIVE_COLOR,
        )

        unreal.MaterialEditingLibrary.layout_material_expressions(material)

    material.set_editor_property("two_sided", False)
    # Phase 6C uses the same palette on instanced projection geometry. Without
    # this authored usage flag, cooked builds silently substitute a neutral
    # fallback shader even though the six material bindings remain non-null.
    material.set_editor_property("used_with_instanced_static_meshes", True)
    unreal.MaterialEditingLibrary.recompile_material(material)
    if not unreal.EditorAssetLibrary.save_loaded_asset(material, False):
        raise RuntimeError(f"Failed to save face master material: {MASTER_MATERIAL_PATH}")
    if not material.get_editor_property("used_with_instanced_static_meshes"):
        raise RuntimeError(
            "Face master material did not retain instanced-static-mesh usage after save."
        )
    log(f"Verified face master material and instanced-mesh usage: {MASTER_MATERIAL_PATH}")
    return material


def ensure_material_instance(asset_path: str, parent_material, color: unreal.LinearColor):
    if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
        instance = require_asset(asset_path)
    else:
        instance = create_asset(
            asset_path,
            unreal.MaterialInstanceConstant,
            unreal.MaterialInstanceConstantFactoryNew(),
        )

    unreal.MaterialEditingLibrary.set_material_instance_parent(instance, parent_material)
    unreal.MaterialEditingLibrary.set_material_instance_vector_parameter_value(
        instance,
        "BaseColor",
        color,
    )
    unreal.MaterialEditingLibrary.set_material_instance_scalar_parameter_value(
        instance,
        "GlowStrength",
        0.0,
    )
    unreal.MaterialEditingLibrary.set_material_instance_scalar_parameter_value(
        instance,
        "FaceOpacity",
        1.0,
    )
    if not unreal.EditorAssetLibrary.save_loaded_asset(instance, False):
        raise RuntimeError(f"Failed to save material instance: {asset_path}")
    log(f"Verified material instance {asset_path}")
    return instance


def ensure_cube_material_family():
    ensure_directory(MATERIAL_ROOT)
    master_material = ensure_face_master_material()
    face_materials = [
        ensure_material_instance(f"{MATERIAL_ROOT}/{asset_name}", master_material, color)
        for asset_name, color in FACE_MATERIAL_SPECS
    ]
    internal_material = ensure_material_instance(
        INTERNAL_MATERIAL_PATH,
        master_material,
        unreal.LinearColor(0.05, 0.05, 0.05, 1.0),
    )
    return face_materials, internal_material


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 ensure_floor_plane() -> None:
    floor_actor = spawn_actor(
        unreal.StaticMeshActor,
        "HT_ClassicCubeTraining_Floor",
        unreal.Vector(0.0, 0.0, -24.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )
    static_mesh_component = floor_actor.static_mesh_component
    static_mesh_component.set_editor_property(
        "static_mesh",
        require_asset("/Engine/BasicShapes/Cube.Cube"),
    )
    static_mesh_component.set_editor_property("mobility", unreal.ComponentMobility.STATIC)
    # A thin volumetric floor avoids the zero-extent bounds path that can crash
    # UE 5.7 editor automation when a Plane is authored under a headless RHI.
    floor_actor.set_actor_scale3d(unreal.Vector(14.0, 14.0, 0.08))


def ensure_directional_light() -> None:
    directional_light = spawn_actor(
        unreal.DirectionalLight,
        "HT_ClassicCubeTraining_Sun",
        unreal.Vector(-300.0, 150.0, 420.0),
        unreal.Rotator(-42.0, -38.0, 0.0),
    )
    light_component = directional_light.get_component_by_class(unreal.DirectionalLightComponent)
    if light_component is not None:
        light_component.set_editor_property("mobility", unreal.ComponentMobility.MOVABLE)
        light_component.set_editor_property("intensity", 9.0)


def ensure_skylight() -> None:
    skylight = spawn_actor(
        unreal.SkyLight,
        "HT_ClassicCubeTraining_Sky",
        unreal.Vector(0.0, 0.0, 220.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )
    skylight_component = skylight.get_component_by_class(unreal.SkyLightComponent)
    if skylight_component is not None:
        skylight_component.set_editor_property("mobility", unreal.ComponentMobility.MOVABLE)
        skylight_component.set_editor_property("real_time_capture", True)
        skylight_component.set_editor_property("intensity", 1.2)


def ensure_sky_atmosphere() -> None:
    spawn_actor(
        unreal.SkyAtmosphere,
        "HT_ClassicCubeTraining_Atmosphere",
        unreal.Vector(0.0, 0.0, 0.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )


def ensure_reflection_capture() -> None:
    reflection_capture = spawn_actor(
        unreal.SphereReflectionCapture,
        "HT_ClassicCubeTraining_Reflection",
        unreal.Vector(0.0, 0.0, 140.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )
    reflection_capture.set_actor_scale3d(unreal.Vector(12.0, 12.0, 12.0))


def ensure_player_start() -> None:
    spawn_actor(
        unreal.PlayerStart,
        "HT_ClassicCubeTraining_PlayerStart",
        unreal.Vector(-540.0, 0.0, 170.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )


def ensure_cube_actor():
    cube_class = require_class("/Script/UnrealHyperTwist.HyperTwistClassicCubeActor")
    cube_actor = spawn_actor(
        cube_class,
        "HT_ClassicCubeTraining_Cube",
        unreal.Vector(0.0, 0.0, 0.0),
        unreal.Rotator(0.0, 0.0, 0.0),
    )
    face_materials, internal_material = ensure_cube_material_family()
    cube_actor.set_editor_property("face_materials", face_materials)
    cube_actor.set_editor_property("internal_material", internal_material)
    if hasattr(cube_actor, "reset_cube"):
        cube_actor.reset_cube()
    else:
        warn("Classic cube actor did not expose reset_cube() to Python; relying on saved properties.")
    return cube_actor


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 validate_authored_actor_family():
    actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    if actor_subsystem is None:
        raise RuntimeError("EditorActorSubsystem was not available for authored-map validation.")

    expected_labels = {
        "HT_ClassicCubeTraining_Floor",
        "HT_ClassicCubeTraining_Sun",
        "HT_ClassicCubeTraining_Sky",
        "HT_ClassicCubeTraining_Atmosphere",
        "HT_ClassicCubeTraining_Reflection",
        "HT_ClassicCubeTraining_PlayerStart",
        "HT_ClassicCubeTraining_Cube",
    }
    actor_labels = {
        actor.get_actor_label()
        for actor in actor_subsystem.get_all_level_actors()
        if actor is not None
    }
    missing_labels = sorted(expected_labels - actor_labels)
    if missing_labels:
        raise RuntimeError(
            "Authored map is missing required actors: " + ", ".join(missing_labels)
        )
    return sorted(expected_labels)


def write_completion_marker(target: str, map_asset_path: str, actor_labels) -> None:
    if not AUTHORING_MARKER_PATH:
        raise RuntimeError(
            "HYPERTWIST_CLASSIC_CUBE_AUTHORING_MARKER must identify the transactional completion marker."
        )

    marker_path = os.path.abspath(AUTHORING_MARKER_PATH)
    os.makedirs(os.path.dirname(marker_path), exist_ok=True)
    marker_payload = {
        "schemaVersion": 1,
        "stage": "complete",
        "target": target,
        "mapAssetPath": map_asset_path,
        "actorLabels": actor_labels,
        "completedAtUtc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    }
    temporary_path = marker_path + ".tmp"
    with open(temporary_path, "w", encoding="utf-8", newline="\n") as marker_file:
        json.dump(marker_payload, marker_file, indent=2, sort_keys=True)
        marker_file.write("\n")
        marker_file.flush()
        os.fsync(marker_file.fileno())
    os.replace(temporary_path, marker_path)
    log(f"Wrote transactional completion marker {marker_path}")


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 recreate_level(level_subsystem, map_asset_path: str) -> None:
    if unreal.EditorAssetLibrary.does_asset_exist(map_asset_path):
        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}'."
            )

        if not unreal.EditorAssetLibrary.delete_asset(map_asset_path):
            raise RuntimeError(f"Failed to delete existing level: {map_asset_path}")

        log(f"Deleted existing level {map_asset_path} before rebuild")

    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 author_map(map_asset_path: str, game_mode_class_path: str):
    level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
    if level_subsystem is None:
        raise RuntimeError("LevelEditorSubsystem was not available.")

    recreate_level(level_subsystem, map_asset_path)
    save_current_level_or_raise(level_subsystem, "level recreation")
    configure_world_settings(game_mode_class_path)
    save_current_level_or_raise(level_subsystem, "world settings")
    ensure_floor_plane()
    save_current_level_or_raise(level_subsystem, "floor placement")
    ensure_directional_light()
    save_current_level_or_raise(level_subsystem, "directional light placement")
    ensure_skylight()
    save_current_level_or_raise(level_subsystem, "skylight placement")
    ensure_sky_atmosphere()
    save_current_level_or_raise(level_subsystem, "sky-atmosphere placement")
    ensure_reflection_capture()
    save_current_level_or_raise(level_subsystem, "reflection capture placement")
    ensure_player_start()
    save_current_level_or_raise(level_subsystem, "player start placement")
    ensure_cube_actor()
    save_current_level_or_raise(level_subsystem, "cube placement")
    actor_labels = validate_authored_actor_family()
    log(f"Finished authoring {map_asset_path}")
    return actor_labels


def main() -> None:
    ensure_directory(MAP_ROOT)
    ensure_directory(MATERIAL_ROOT)
    ensure_cube_material_family()

    requested_map_kind = os.getenv("HYPERTWIST_CLASSIC_CUBE_MAP_KIND", "").strip().lower()
    if requested_map_kind in ("", "all", "both"):
        targets = (
            (
                CLASSIC_MAP_PATH,
                "/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode",
            ),
            (
                FOLLOW_ALONG_MAP_PATH,
                "/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode",
            ),
        )
    elif requested_map_kind == "classic":
        targets = (
            (
                CLASSIC_MAP_PATH,
                "/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode",
            ),
        )
    elif requested_map_kind in ("followalong", "follow-along"):
        targets = (
            (
                FOLLOW_ALONG_MAP_PATH,
                "/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode",
            ),
        )
    else:
        raise RuntimeError(
            f"Unsupported HYPERTWIST_CLASSIC_CUBE_MAP_KIND value: {requested_map_kind}"
        )

    if len(targets) != 1:
        raise RuntimeError(
            "Transactional map authoring requires one target per UnrealEditor process."
        )

    for map_asset_path, game_mode_class_path in targets:
        actor_labels = author_map(map_asset_path, game_mode_class_path)
        write_completion_marker(requested_map_kind, map_asset_path, actor_labels)

    log("Classic cube authored maps are ready.")


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