import * as THREE from 'three';

export class RouteManager {
  constructor(scene, settings) {
    this.scene = scene;
    this.settings = settings;

    this.graphData = null;

    // -- State --
    this.currentRouteNodes = [];
    this.savedRoutes = []; // { nodes: [], length: number, mesh: THREE.Mesh }

    // -- Visuals --
    this.markers = []; // Draggable spheres
    this.currentPathMesh = null; // The tube being edited

    this.ROAD_OFFSET = 2.5;

    // -- Callbacks --
    this.onRouteChanged = null; // function(lengthInMeters)
  }

  initGraph(data) {
    this.graphData = data;
    this.graphData.adjacency = {};

    // 1. Flip Coordinates
    for (let key in this.graphData.nodes) {
      this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
    }

    // 2. Build Adjacency
    this.graphData.edges.forEach((edge, index) => {
      if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; });

      if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
      this.graphData.adjacency[edge.u].push({
        to: edge.v, cost: edge.length || 1, edgeIndex: index
      });

      if (!edge.oneway) {
        if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
        this.graphData.adjacency[edge.v].push({
          to: edge.u, cost: edge.length || 1, edgeIndex: index, isReverse: true
        });
      }
    });
  }

  // ============================
  // API Methods (For UI/Input)
  // ============================

  addNodeByWorldPosition(vector3) {
    if (!this.graphData) return;
    const nodeId = this.findNearestNode(vector3.x, vector3.z);
    if (nodeId === null) return;

    if (this.currentRouteNodes.length > 0 &&
      this.currentRouteNodes[this.currentRouteNodes.length - 1] === nodeId) {
      return;
    }

    this.currentRouteNodes.push(nodeId);
    this.addMarkerVisual(nodeId);
    this.updatePathVisuals();
  }

  dragNode(markerObject, worldPoint) {
    if (!this.graphData) return;
    const index = this.markers.indexOf(markerObject);
    if (index === -1) return;

    const newNodeId = this.findNearestNode(worldPoint.x, worldPoint.z);

    if (this.currentRouteNodes[index] !== newNodeId) {
      this.currentRouteNodes[index] = newNodeId;

      const nodeData = this.graphData.nodes[newNodeId];
      markerObject.position.set(nodeData.x, 2, nodeData.y);
      markerObject.userData.nodeId = newNodeId;

      this.updatePathVisuals();
    }
  }

  saveCurrentRoute() {
    if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;

    // 1. Calculate final length
    const totalLength = this.currentPathMesh.userData.length || 0;

    // 2. Freeze the mesh (Change color to indicate saved state)
    this.currentPathMesh.material.color.setHex(0x10B981); // Emerald Green

    // 3. Store in saved list
    this.savedRoutes.push({
      nodes: [...this.currentRouteNodes],
      length: totalLength,
      mesh: this.currentPathMesh
    });

    // 4. Detach mesh from "current" reference so we don't delete it on reset
    this.currentPathMesh = null;

    // 5. Clear drafting state (remove markers, clear node list)
    this.resetDraftingState();
  }

  clearCurrentRoute() {
    // 1. Remove current mesh from scene
    if (this.currentPathMesh) {
      this.scene.remove(this.currentPathMesh);
      this.currentPathMesh.geometry.dispose();
      this.currentPathMesh = null;
    }
    // 2. Reset state
    this.resetDraftingState();
  }

  resetDraftingState() {
    this.currentRouteNodes = [];
    // Remove all markers
    this.markers.forEach(m => this.scene.remove(m));
    this.markers = [];

    // Notify UI
    if (this.onRouteChanged) this.onRouteChanged(0);
  }

  deleteSavedRoute(index) {
    if (index < 0 || index >= this.savedRoutes.length) return;

    const route = this.savedRoutes[index];

    // Remove mesh from scene
    if (route.mesh) {
      this.scene.remove(route.mesh);
      route.mesh.geometry.dispose();
    }

    this.savedRoutes.splice(index, 1);
  }

  getSavedRoutes() {
    return this.savedRoutes;
  }

  // ============================
  // Visuals & Logic
  // ============================

  updatePathVisuals() {
    // Needs 2+ nodes
    if (this.currentRouteNodes.length < 2) {
      if (this.currentPathMesh) {
        this.scene.remove(this.currentPathMesh);
        this.currentPathMesh = null;
      }
      if (this.onRouteChanged) this.onRouteChanged(0);
      return;
    }

    let fullPathPoints = [];
    let totalDist = 0;

    for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
      const start = this.currentRouteNodes[i];
      const end = this.currentRouteNodes[i + 1];

      const segmentEdges = this.computePathAStar(start, end);

      if (!segmentEdges) continue;

      segmentEdges.forEach(step => {
        totalDist += step.edgeData.cost || 0; // Accumulate distance

        const rawPoints = step.edgeData.points;
        let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
        if (step.isReverse) segmentPoints.reverse();

        const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
        offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
      });
    }

    // Update Mesh
    if (this.currentPathMesh) {
      this.scene.remove(this.currentPathMesh);
      this.currentPathMesh.geometry.dispose();
    }

    if (fullPathPoints.length < 2) return;

    const curve = new THREE.CatmullRomCurve3(fullPathPoints);
    const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
    const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });

    this.currentPathMesh = new THREE.Mesh(tubeGeom, tubeMat);
    // Store length on the mesh for easy access later
    this.currentPathMesh.userData.length = totalDist;

    this.scene.add(this.currentPathMesh);

    // Update markers color (First=Green, Last=Red, Others=Yellow)
    this.updateMarkerColors();

    // Trigger Callback
    if (this.onRouteChanged) this.onRouteChanged(totalDist);
  }

  updateMarkerColors() {
    this.markers.forEach((marker, i) => {
      let color = 0xFFFF00; // Default Yellow (Waypoint)
      if (i === 0) color = this.settings.colors.pathStart; // Green
      else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd; // Red
      marker.material.color.setHex(color);
    });
  }

  addMarkerVisual(nodeId) {
    const node = this.graphData.nodes[nodeId];
    const geom = new THREE.SphereGeometry(4);
    const mat = new THREE.MeshBasicMaterial({ color: this.settings.colors.pathEnd });
    const mesh = new THREE.Mesh(geom, mat);

    mesh.position.set(node.x, 2, node.y);
    mesh.userData = { isMarker: true, nodeId: nodeId };

    this.scene.add(mesh);
    this.markers.push(mesh);

    this.updateMarkerColors();
  }

  // ============================
  // Algorithms (A* & Helpers)
  // ============================

  findNearestNode(x, z) {
    let closestId = null;
    let minDist = Infinity;
    for (const [id, node] of Object.entries(this.graphData.nodes)) {
      const dx = node.x - x;
      const dz = node.y - z;
      const d2 = dx * dx + dz * dz;
      if (d2 < minDist) {
        minDist = d2;
        closestId = parseInt(id);
      }
    }
    return closestId;
  }

  computePathAStar(start, end) {
    if (start === end) return [];
    const openSet = new Set([start]);
    const cameFrom = {};
    const gScore = {};
    const fScore = {};
    gScore[start] = 0;
    fScore[start] = this.heuristic(start, end);

    while (openSet.size > 0) {
      let current = null;
      let minF = Infinity;
      for (const node of openSet) {
        const score = fScore[node] !== undefined ? fScore[node] : Infinity;
        if (score < minF) { minF = score; current = node; }
      }
      if (current === end) return this.reconstructPath(cameFrom, current);
      openSet.delete(current);

      const neighbors = this.graphData.adjacency[current] || [];
      for (const neighbor of neighbors) {
        const tentativeG = gScore[current] + neighbor.cost;
        if (tentativeG < (gScore[neighbor.to] !== undefined ? gScore[neighbor.to] : Infinity)) {
          cameFrom[neighbor.to] = { prev: current, edgeIdx: neighbor.edgeIndex, isReverse: neighbor.isReverse };
          gScore[neighbor.to] = tentativeG;
          fScore[neighbor.to] = tentativeG + this.heuristic(neighbor.to, end);
          openSet.add(neighbor.to);
        }
      }
    }
    return null;
  }

  heuristic(a, b) {
    const nA = this.graphData.nodes[a];
    const nB = this.graphData.nodes[b];
    return Math.sqrt((nA.x - nB.x) ** 2 + (nA.y - nB.y) ** 2);
  }

  reconstructPath(cameFrom, current) {
    const edges = [];
    while (current in cameFrom) {
      const data = cameFrom[current];
      edges.push({ edgeData: this.graphData.edges[data.edgeIdx], isReverse: data.isReverse });
      current = data.prev;
    }
    return edges.reverse();
  }

  getOffsetPath(points, offset) {
    if (points.length < 2) return points;
    const newPath = [];
    for (let i = 0; i < points.length - 1; i++) {
      const p1 = points[i];
      const p2 = points[i + 1];
      const dir = new THREE.Vector2().subVectors(p2, p1).normalize();
      const normal = new THREE.Vector2(-dir.y, dir.x);
      const off = normal.multiplyScalar(offset);
      newPath.push(new THREE.Vector2().addVectors(p1, off));
      if (i === points.length - 2) newPath.push(new THREE.Vector2().addVectors(p2, off));
    }
    return newPath;
  }
}
