import * as THREE from "./vendor/three/three.module.js";
import { GLTFLoader } from "./vendor/three/GLTFLoader.js";
import { GeoApiClient } from "../libs/geo-api-client/dist/index.js";

class BrowserStorage {
  getItem(key) {
    return localStorage.getItem(key);
  }
  setItem(key, value) {
    localStorage.setItem(key, value);
  }
  removeItem(key) {
    localStorage.removeItem(key);
  }
}

const statusEl = document.getElementById("status");
const apiBaseEl = document.getElementById("apiBase");
const publicKeyPreviewEl = document.getElementById("publicKeyPreview");
const collectionInfoEl = document.getElementById("collectionInfo");
const collectionNameEl = document.getElementById("collectionName");
const assetFileEl = document.getElementById("assetFile");
const assetNameEl = document.getElementById("assetName");
const assetDescEl = document.getElementById("assetDesc");
const assetsListEl = document.getElementById("assetsList");

let client = new GeoApiClient(apiBaseEl.value.trim(), new BrowserStorage());
let keys = null;
let accessToken = "";
let collectionId = "";
let selectedLngLat = null;
let pendingMarker = null;

let map;
let threeLayer;
let threeScene;
let threeRenderer;
let threeCamera;
const featureMeshes = new Map();
const modelTemplateCache = new Map();
const gltfLoader = new GLTFLoader();
const ownFeatureMarkers = new Map();
const ownFeatureCoords = new Map();
let renderCycle = 0;

function setStatus(message) {
  statusEl.textContent = message;
}

function currentApiBase() {
  return apiBaseEl.value.trim().replace(/\/+$/g, "");
}

function extFromFilename(name) {
  const idx = name.lastIndexOf(".");
  if (idx <= 0) return "";
  return name.slice(idx + 1).toLowerCase();
}

function normalizeExt(value) {
  const text = String(value || "").trim().toLowerCase();
  if (!text) return "";
  return text.includes(".") ? extFromFilename(text) : text;
}

function kindFromExt(ext) {
  return ext === "gltf" || ext === "glb" ? "3d" : "image";
}

async function sha256Hex(file) {
  const buffer = await file.arrayBuffer();
  const digest = await crypto.subtle.digest("SHA-256", buffer);
  return Array.from(new Uint8Array(digest))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

function setClientBase(baseUrl) {
  const normalized = baseUrl.trim().replace(/\/+$/g, "");
  client = new GeoApiClient(normalized, new BrowserStorage());
  if (accessToken) client.setAccessToken(accessToken);
  localStorage.setItem("geo_api_base", normalized);
  setStatus(`API base updated: ${normalized}`);
}

function buildRasterStyle() {
  return {
    version: 8,
    sources: {
      "osm-raster": {
        type: "raster",
        tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
        tileSize: 256,
        attribution: "&copy; OpenStreetMap contributors",
      },
    },
    layers: [
      {
        id: "osm-raster-layer",
        type: "raster",
        source: "osm-raster",
      },
    ],
  };
}

function buildMapShareLink(feature, asset) {
  const coords = feature?.geometry?.coordinates;
  if (!Array.isArray(coords) || coords.length < 2) {
    return client.resolveRelativeLink(asset.link);
  }
  const url = new URL(window.location.origin + window.location.pathname);
  url.searchParams.set("shared", "1");
  url.searchParams.set("lng", String(coords[0]));
  url.searchParams.set("lat", String(coords[1]));
  url.searchParams.set("kind", asset.kind || "3d");
  url.searchParams.set("public", asset.isPublic ? "1" : "0");
  url.searchParams.set("assetId", asset.id);
  url.searchParams.set("assetLink", client.resolveRelativeLink(asset.link));
  return url.toString();
}

function extFromLink(link) {
  if (!link) return "";
  try {
    const url = new URL(link, `${currentApiBase()}/`);
    return extFromFilename(url.pathname);
  } catch {
    return extFromFilename(link.split("?")[0]);
  }
}

function is3DAsset(asset) {
  if (!asset) return false;
  const kind = String(asset.kind || "").toLowerCase();
  const ext = normalizeExt(asset.ext) || extFromLink(asset.link);
  return kind === "3d" && (ext === "glb" || ext === "gltf");
}

async function renderSharedAssetFromQuery() {
  const params = new URLSearchParams(window.location.search);
  if (params.get("shared") !== "1") return;
  const lng = Number(params.get("lng"));
  const lat = Number(params.get("lat"));
  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return;
  const kind = params.get("kind") === "image" ? "image" : "3d";
  const isPublic = params.get("public") !== "0";
  const assetId = params.get("assetId") || "shared-asset";
  const assetLink = params.get("assetLink") || "";

  const loadedModel = await addObjectMeshFromAsset(
    `shared-${assetId}`,
    lng,
    lat,
    {
      id: assetId,
      kind,
      ext: extFromLink(assetLink) || (kind === "3d" ? "glb" : "png"),
      isPublic,
      link: assetLink,
    },
    renderCycle
  );
  if (pendingMarker) pendingMarker.remove();
  pendingMarker = new maplibregl.Marker({ color: "#f59e0b" }).setLngLat({ lng, lat }).addTo(map);
  map.easeTo({ center: [lng, lat], zoom: Math.max(map.getZoom(), 14), pitch: 55, bearing: -15 });
  setStatus(loadedModel ? "Shared 3D model loaded on map." : "Shared object loaded with fallback shape.");
}

function createThreeLayer() {
  return {
    id: "threejs-custom-layer",
    type: "custom",
    renderingMode: "3d",
    onAdd(m, gl) {
      threeCamera = new THREE.Camera();
      threeScene = new THREE.Scene();
      const ambient = new THREE.AmbientLight(0xffffff, 0.8);
      const directional = new THREE.DirectionalLight(0xffffff, 0.7);
      directional.position.set(0, -70, 100).normalize();
      threeScene.add(ambient);
      threeScene.add(directional);

      threeRenderer = new THREE.WebGLRenderer({
        canvas: m.getCanvas(),
        context: gl,
        antialias: true,
      });
      threeRenderer.autoClear = false;
    },
    render(gl, matrix) {
      const m = new THREE.Matrix4().fromArray(matrix);
      threeCamera.projectionMatrix = m;
      threeRenderer.resetState();
      threeRenderer.render(threeScene, threeCamera);
      map.triggerRepaint();
      gl.disable(gl.DEPTH_TEST);
    },
  };
}

function disposeMaterial(material) {
  if (!material) return;
  for (const value of Object.values(material)) {
    if (value && value.isTexture) value.dispose();
  }
  material.dispose();
}

function disposeObject3D(object) {
  if (!object) return;
  object.traverse((node) => {
    if (!node.isMesh) return;
    if (node.geometry) node.geometry.dispose();
    if (node.material) {
      if (Array.isArray(node.material)) {
        for (const material of node.material) disposeMaterial(material);
      } else {
        disposeMaterial(node.material);
      }
    }
  });
}

function clearFeatureMeshes() {
  for (const object of featureMeshes.values()) {
    threeScene.remove(object);
    disposeObject3D(object);
  }
  featureMeshes.clear();
}

function clearOwnFeatureMarkers() {
  for (const marker of ownFeatureMarkers.values()) {
    marker.remove();
  }
  ownFeatureMarkers.clear();
  ownFeatureCoords.clear();
}

function positionObjectOnMap(object, lng, lat, scaleMeters) {
  const merc = maplibregl.MercatorCoordinate.fromLngLat({ lng, lat }, 0);
  const meters = merc.meterInMercatorCoordinateUnits();
  object.position.set(merc.x, merc.y, merc.z);
  object.scale.setScalar(meters * scaleMeters);
}

function addFallbackMesh(featureId, lng, lat, isPublic, kind) {
  const is3D = kind === "3d";
  const geometry = is3D ? new THREE.BoxGeometry(1.2, 1.2, 2.2) : new THREE.PlaneGeometry(1.8, 1.8);
  const color = isPublic ? 0x44dd88 : 0xdd5566;
  const material = new THREE.MeshStandardMaterial({ color, transparent: true, opacity: 0.92 });
  const mesh = new THREE.Mesh(geometry, material);
  positionObjectOnMap(mesh, lng, lat, is3D ? 18 : 24);
  if (!is3D) {
    mesh.rotation.x = Math.PI / 2;
  }
  threeScene.add(mesh);
  featureMeshes.set(featureId, mesh);
  map.triggerRepaint();
}

function prepareModelRoot(modelRoot) {
  const box = new THREE.Box3().setFromObject(modelRoot);
  if (box.isEmpty()) return;
  const center = new THREE.Vector3();
  const size = new THREE.Vector3();
  box.getCenter(center);
  box.getSize(size);
  modelRoot.position.x -= center.x;
  modelRoot.position.y -= center.y;
  modelRoot.position.z -= box.min.z;
  modelRoot.userData.baseSize = Math.max(size.x, size.y, size.z) || 1;
}

function cloneModelTemplate(modelRoot) {
  const clone = modelRoot.clone(true);
  clone.traverse((node) => {
    if (!node.isMesh) return;
    node.castShadow = true;
    node.receiveShadow = true;
  });
  return clone;
}

async function loadModelTemplate(modelURL) {
  const cached = modelTemplateCache.get(modelURL);
  if (cached) return cached;

  const pending = new Promise((resolve, reject) => {
    gltfLoader.setRequestHeader(accessToken ? { Authorization: `Bearer ${accessToken}` } : {});
    gltfLoader.load(
      modelURL,
      (gltf) => {
        const root = gltf.scene || gltf.scenes?.[0];
        if (!root) {
          reject(new Error("GLTF file has no scene graph."));
          return;
        }
        prepareModelRoot(root);
        resolve(root);
      },
      undefined,
      reject
    );
  });

  modelTemplateCache.set(modelURL, pending);
  try {
    return await pending;
  } catch (error) {
    modelTemplateCache.delete(modelURL);
    throw error;
  }
}

async function addObjectMeshFromAsset(featureId, lng, lat, asset, cycleID) {
  if (!asset) {
    addFallbackMesh(featureId, lng, lat, true, "3d");
    return false;
  }

  if (!is3DAsset(asset) || !asset.link) {
    addFallbackMesh(featureId, lng, lat, Boolean(asset.isPublic), asset.kind || "3d");
    return false;
  }

  try {
    const modelURL = client.resolveRelativeLink(asset.link);
    const template = await loadModelTemplate(modelURL);
    if (cycleID !== renderCycle) return false;

    const modelRoot = cloneModelTemplate(template);
    const baseSize = Number(template.userData.baseSize) || 1;
    positionObjectOnMap(modelRoot, lng, lat, 12 / baseSize);
    threeScene.add(modelRoot);
    featureMeshes.set(featureId, modelRoot);
    map.triggerRepaint();
    return true;
  } catch (error) {
    console.warn(`Failed to load model for feature ${featureId}:`, error);
    addFallbackMesh(featureId, lng, lat, Boolean(asset.isPublic), asset.kind || "3d");
    return false;
  }
}

async function moveOwnFeature(featureID, lng, lat) {
  const existing = ownFeatureCoords.get(featureID);
  const alt = Array.isArray(existing) && existing.length >= 3 ? existing[2] : 0;
  const res = await fetch(`${currentApiBase()}/v1/features/${featureID}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      geometry: { type: "Point", coordinates: [lng, lat, alt] },
    }),
  });
  if (!res.ok) {
    const maybeJson = await res.json().catch(() => ({}));
    throw new Error(maybeJson.error || `Failed to move feature (${res.status})`);
  }
}

function addOwnFeatureMarker(featureID, lng, lat) {
  const marker = new maplibregl.Marker({ color: "#2563eb", draggable: true }).setLngLat({ lng, lat }).addTo(map);
  marker.on("dragend", async () => {
    const p = marker.getLngLat();
    try {
      await moveOwnFeature(featureID, p.lng, p.lat);
      await refreshFeatures();
      setStatus(`Feature ${featureID} moved.`);
    } catch (error) {
      setStatus(error.message);
    }
  });
  ownFeatureMarkers.set(featureID, marker);
}

async function ensureKeys() {
  keys = await client.ensureKeysInStorage();
  publicKeyPreviewEl.textContent = `Public key: ${keys.publicKey.slice(0, 24)}...`;
}

async function register() {
  if (!keys) await ensureKeys();
  await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey);
}

async function login() {
  if (!keys) await ensureKeys();
  accessToken = await client.loginWithSignature(keys.publicKey, keys.privateKey);
  client.setAccessToken(accessToken);
  await refreshFeatures();
}

async function ensureCollection() {
  if (collectionId) return collectionId;
  const created = await client.createCollection(collectionNameEl.value.trim() || "MapLibre 3D objects demo");
  collectionId = created.id;
  collectionInfoEl.textContent = `${created.name} (${created.id})`;
  return collectionId;
}

function renderAssets(features) {
  assetsListEl.innerHTML = "";
  for (const feature of features) {
    const assets = Array.isArray(feature.properties?.assets) ? feature.properties.assets : [];
    for (const asset of assets) {
      const absoluteLink = client.resolveRelativeLink(asset.link);
      const card = document.createElement("div");
      card.className = "asset-card";
      card.innerHTML = `
        <div><strong>${asset.kind}</strong> • ${asset.ext}</div>
        <div class="muted">Feature: ${feature.id}</div>
        <div class="muted">Visibility: ${asset.isPublic ? "public" : "private"}</div>
        <div class="muted">Link: ${asset.link}</div>
      `;
      const actions = document.createElement("div");
      actions.className = "asset-actions";

      const openBtn = document.createElement("button");
      openBtn.textContent = "Open";
      openBtn.onclick = () => window.open(absoluteLink, "_blank", "noopener,noreferrer");
      actions.appendChild(openBtn);

      const toggleBtn = document.createElement("button");
      toggleBtn.textContent = asset.isPublic ? "Set Private" : "Set Public";
      toggleBtn.onclick = async () => {
        try {
          await client.setAssetVisibility(asset.id, !asset.isPublic);
          await refreshFeatures();
          setStatus(`Updated visibility for ${asset.id}`);
        } catch (error) {
          setStatus(error.message);
        }
      };
      actions.appendChild(toggleBtn);

      const copyBtn = document.createElement("button");
      copyBtn.textContent = "Copy Share Link";
      copyBtn.onclick = async () => {
        const shareLink = buildMapShareLink(feature, asset);
        await navigator.clipboard.writeText(shareLink);
        setStatus("Map share link copied to clipboard.");
      };
      actions.appendChild(copyBtn);

      card.appendChild(actions);
      assetsListEl.appendChild(card);
    }
  }
  if (!assetsListEl.children.length) {
    assetsListEl.innerHTML = `<div class="muted">No assets linked yet.</div>`;
  }
}

async function refreshFeatures() {
  renderCycle += 1;
  const cycleID = renderCycle;
  const publicResp = await fetch(`${currentApiBase()}/v1/features/public?kind=3d`);
  if (!publicResp.ok) {
    throw new Error(`Failed to load public features: HTTP ${publicResp.status}`);
  }
  const publicData = await publicResp.json();
  const byID = new Map((publicData.features || []).map((feature) => [feature.id, feature]));

  const ownFeatureIDs = new Set();
  if (accessToken) {
    const { collections } = await client.listCollections();
    if (!collectionId && collections.length > 0) {
      collectionId = collections[0].id;
      collectionInfoEl.textContent = `${collections[0].name} (${collections[0].id})`;
    }
    const ownFeatureSets = await Promise.all(
      collections.map(async (collection) => {
        const { features } = await client.listFeatures(collection.id);
        return features;
      })
    );
    for (const feature of ownFeatureSets.flat()) {
      byID.set(feature.id, feature);
      ownFeatureIDs.add(feature.id);
    }
  }

  const features = Array.from(byID.values());
  clearFeatureMeshes();
  clearOwnFeatureMarkers();
  for (const feature of features) {
    const coords = feature.geometry?.coordinates;
    if (!coords || coords.length < 2) continue;
    const lng = coords[0];
    const lat = coords[1];
    ownFeatureCoords.set(feature.id, coords);
    const assets = Array.isArray(feature.properties?.assets) ? feature.properties.assets : [];
    const preferred3D = assets.find((asset) => is3DAsset(asset));
    const first = preferred3D || assets[0];
    if (first) {
      await addObjectMeshFromAsset(feature.id, lng, lat, first, cycleID);
    }
    if (ownFeatureIDs.has(feature.id) && accessToken) {
      addOwnFeatureMarker(feature.id, lng, lat);
    }
  }
  renderAssets(features);
}

async function createFeatureAndUpload() {
  if (!selectedLngLat) {
    throw new Error("Click the map to choose object location first.");
  }
  const file = assetFileEl.files?.[0];
  if (!file) {
    throw new Error("Select a 3D/image file first.");
  }
  const ext = extFromFilename(file.name);
  if (!ext) {
    throw new Error("File extension is required.");
  }

  await ensureCollection();
  const featureName = assetNameEl.value.trim() || file.name;
  const feature = await client.createPointFeature(
    collectionId,
    selectedLngLat.lng,
    selectedLngLat.lat,
    { name: featureName, placement: "maplibre-demo" }
  );

  const checksum = await sha256Hex(file);
  const kind = kindFromExt(ext);
  const created = await client.createOrLinkAsset({
    featureId: feature.id,
    checksum,
    ext,
    kind,
    mimeType: file.type || "application/octet-stream",
    sizeBytes: file.size,
    name: featureName,
    description: assetDescEl.value.trim(),
    isPublic: true,
  });

  try {
    await client.uploadAssetBinary(created.asset.id, file, file.type || "application/octet-stream");
  } catch (error) {
    throw new Error(`Upload failed: ${error.message}`);
  }

  await refreshFeatures();
  assetNameEl.value = "";
  assetDescEl.value = "";
  assetFileEl.value = "";
  setStatus("3D/image object stored and rendered on map.");
}

document.getElementById("applyApi").onclick = () => setClientBase(apiBaseEl.value);
document.getElementById("ensureKeys").onclick = async () => {
  try {
    await ensureKeys();
    setStatus("Keys are ready.");
  } catch (error) {
    setStatus(error.message);
  }
};
document.getElementById("register").onclick = async () => {
  try {
    await register();
    setStatus("Registered.");
  } catch (error) {
    setStatus(error.message);
  }
};
document.getElementById("login").onclick = async () => {
  try {
    await login();
    setStatus("Logged in. Loaded all collections on map.");
  } catch (error) {
    setStatus(error.message);
  }
};
document.getElementById("createCollection").onclick = async () => {
  try {
    await ensureCollection();
    setStatus("Collection is ready.");
  } catch (error) {
    setStatus(error.message);
  }
};
document.getElementById("uploadAsset").onclick = async () => {
  try {
    if (!accessToken) throw new Error("Login first.");
    await createFeatureAndUpload();
  } catch (error) {
    setStatus(error.message);
  }
};

map = new maplibregl.Map({
  container: "map",
  style: buildRasterStyle(),
  center: [-16.2518, 28.4636],
  zoom: 12,
  pitch: 55,
  bearing: -15,
  hash: false,
  antialias: true,
});
map.addControl(new maplibregl.NavigationControl(), "top-right");

map.on("load", () => {
  threeLayer = createThreeLayer();
  map.addLayer(threeLayer);
  renderSharedAssetFromQuery().catch((error) => setStatus(error.message));
  refreshFeatures().catch((error) => setStatus(error.message));
});

map.on("click", (event) => {
  selectedLngLat = event.lngLat;
  if (pendingMarker) pendingMarker.remove();
  pendingMarker = new maplibregl.Marker({ color: "#22d3ee" })
    .setLngLat(event.lngLat)
    .addTo(map);
  setStatus(`Selected location: ${event.lngLat.lat.toFixed(5)}, ${event.lngLat.lng.toFixed(5)}`);
});

const savedBase = localStorage.getItem("geo_api_base");
if (savedBase) {
  apiBaseEl.value = savedBase;
  setClientBase(savedBase);
}
