import math
import struct
from pathlib import Path

import unreal


MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
PROJECT_ROOT = Path(r"Y:\AgrarianGameBulid")
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
HEIGHTMAP_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_1009.r16"
LANDSCAPE_SIZE = 1009
XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1)
Z_SCALE_CM = 100.0
LANDSCAPE_MIN_XY = -50000.0

PLAYER_START_LABEL = "AGR_DemoPlayerStart"
SAFE_Z_OFFSET_CM = 220.0
MIN_ELEVATION_M = 2.0
MAX_SLOPE_DEGREES = 8.0
SLOPE_SAMPLE_DISTANCE_CM = 500.0
MIN_WATER_DISTANCE_CM = 10000.0
MIN_RESOURCE_DISTANCE_CM = 5000.0
KNOWN_SAFE_FALLBACK_XY = (-22000.0, -3500.0)
RESOURCE_PREFIXES = (
    "AGR_GZ_Wood",
    "AGR_GZ_Fiber",
    "AGR_GZ_Stone",
    "AGR_DemoWoodResource",
    "AGR_DemoFiberResource",
)
WATER_LABELS = {
    "AGR_GZ_FreshWaterSource_01",
    "AGR_GZ_EnvVar_Water_Surface_01",
}


def get_actor_label(actor):
    try:
        return actor.get_actor_label()
    except Exception:
        return actor.get_name()


def load_heightmap():
    raw = HEIGHTMAP_PATH.read_bytes()
    expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
    if len(raw) != expected_bytes:
        raise RuntimeError(f"Unexpected heightmap size: {len(raw)} != {expected_bytes}")
    return struct.unpack(f"<{LANDSCAPE_SIZE * LANDSCAPE_SIZE}H", raw)


def terrain_elevation_m(height_values, x_cm, y_cm):
    sample_x = max(0, min(LANDSCAPE_SIZE - 1, round((x_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
    sample_y = max(0, min(LANDSCAPE_SIZE - 1, round((y_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
    height_value = height_values[int(sample_y) * LANDSCAPE_SIZE + int(sample_x)]
    return (float(height_value) - 32768.0) * Z_SCALE_CM / (100.0 * 128.0)


def terrain_slope_degrees(height_values, x_cm, y_cm):
    dz_dx_m = (
        terrain_elevation_m(height_values, x_cm + SLOPE_SAMPLE_DISTANCE_CM, y_cm)
        - terrain_elevation_m(height_values, x_cm - SLOPE_SAMPLE_DISTANCE_CM, y_cm)
    ) / ((SLOPE_SAMPLE_DISTANCE_CM * 2.0) / 100.0)
    dz_dy_m = (
        terrain_elevation_m(height_values, x_cm, y_cm + SLOPE_SAMPLE_DISTANCE_CM)
        - terrain_elevation_m(height_values, x_cm, y_cm - SLOPE_SAMPLE_DISTANCE_CM)
    ) / ((SLOPE_SAMPLE_DISTANCE_CM * 2.0) / 100.0)
    return math.degrees(math.atan(math.sqrt((dz_dx_m * dz_dx_m) + (dz_dy_m * dz_dy_m))))


def distance_2d(a, b):
    return math.hypot(a.x - b.x, a.y - b.y)


def main():
    if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
        raise RuntimeError(f"Could not load map: {MAP_PATH}")

    failures = []
    actors = unreal.EditorLevelLibrary.get_all_level_actors()
    starts = [actor for actor in actors if get_actor_label(actor) == PLAYER_START_LABEL]
    if len(starts) != 1:
        raise RuntimeError(f"Expected exactly one {PLAYER_START_LABEL}, found {len(starts)}")

    player_start = starts[0]
    location = player_start.get_actor_location()
    height_values = load_heightmap()
    terrain_elevation = terrain_elevation_m(height_values, location.x, location.y)
    terrain_z_cm = terrain_elevation * 100.0
    slope_degrees = terrain_slope_degrees(height_values, location.x, location.y)
    actual_z_offset_cm = location.z - terrain_z_cm

    resource_points = [
        actor.get_actor_location()
        for actor in actors
        if get_actor_label(actor).startswith(RESOURCE_PREFIXES)
    ]
    water_points = [
        actor.get_actor_location()
        for actor in actors
        if get_actor_label(actor) in WATER_LABELS
    ]

    if terrain_elevation < MIN_ELEVATION_M:
        failures.append(f"spawn elevation {terrain_elevation:.2f}m below {MIN_ELEVATION_M:.2f}m")
    if actual_z_offset_cm < SAFE_Z_OFFSET_CM - 1.0:
        failures.append(f"spawn z offset {actual_z_offset_cm:.1f}cm below {SAFE_Z_OFFSET_CM:.1f}cm")
    if slope_degrees > MAX_SLOPE_DEGREES:
        failures.append(f"spawn slope {slope_degrees:.2f}deg exceeds {MAX_SLOPE_DEGREES:.2f}deg")
    if not resource_points:
        failures.append("no resource actors found for spacing validation")
    elif min(distance_2d(location, point) for point in resource_points) < MIN_RESOURCE_DISTANCE_CM:
        failures.append("spawn is too close to a resource cluster")
    if not water_points:
        failures.append("no water actors found for spacing validation")
    elif min(distance_2d(location, point) for point in water_points) < MIN_WATER_DISTANCE_CM:
        failures.append("spawn is too close to water")

    fallback_point = unreal.Vector(KNOWN_SAFE_FALLBACK_XY[0], KNOWN_SAFE_FALLBACK_XY[1], 0.0)
    if distance_2d(location, fallback_point) > 1.0:
        failures.append(
            "spawn is not using the known safe fallback coordinate "
            f"{KNOWN_SAFE_FALLBACK_XY}, got ({location.x:.0f}, {location.y:.0f})"
        )

    roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
    technical_design = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "Docs/TechnicalDesignDocument.md"
    for path, snippet in [
        (roadmap, "[x] Add spawn area with validation"),
        (technical_design, "safe Ground Zero spawn"),
    ]:
        with open(path, "r", encoding="utf-8") as handle:
            text = handle.read()
        if snippet not in text:
            failures.append(f"{path} missing `{snippet}`")

    if failures:
        raise RuntimeError("Ground Zero safe spawn verification failed: " + "; ".join(failures))

    unreal.log(
        "Ground Zero safe spawn verification complete: "
        f"location ({location.x:.0f}, {location.y:.0f}, {location.z:.0f}), "
        f"terrain {terrain_elevation:.2f}m, slope {slope_degrees:.2f}deg, z offset {actual_z_offset_cm:.0f}cm."
    )


main()
