import * as THREE from 'three';

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

    this.graphData = null;

    // -- State --
    this.isDrafting = false; // New flag
    this.currentRouteNodes = [];
    this.savedRoutes = [];

    // -- Visuals --
    this.markers = [];
    this.currentPathMesh = null;
    this.ghostMarker = null; // Transparent sphere

    this.servedNodes = new Set();
    this.servedCoordinates = [];
    this.ROAD_OFFSET = 2.5;

    this.onRouteChanged = null;
    this.gameManager = null;
    this.vehicleSystem = null;

    // Draft state
    this.latestPathPoints = [];

    this.initGhostMarker();
  }

  setVehicleSystem(vs) {
    this.vehicleSystem = vs;
  }

  setGameManager(gm) {
    this.gameManager = gm;
  }

  initGraph(data) {
    this.graphData = data;
    this.graphData.adjacency = {};
    for (let key in this.graphData.nodes) {
      this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
    }
    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 });
      }
    });
  }

  // ============================
  // Draft Mode & Ghost Marker
  // ============================

  initGhostMarker() {
    const geom = new THREE.SphereGeometry(4);
    const mat = new THREE.MeshBasicMaterial({
      color: this.settings.colors.pathStart,
      transparent: true,
      opacity: 0.5
    });
    this.ghostMarker = new THREE.Mesh(geom, mat);
    this.ghostMarker.visible = false;
    this.ghostMarker.name = "GHOST_MARKER"; // Ignore in raycasting
    this.scene.add(this.ghostMarker);
  }

  startDrafting() {
    this.isDrafting = true;
    this.resetDraftingState();
  }

  stopDrafting() {
    this.isDrafting = false;
    this.ghostMarker.visible = false;
    this.clearCurrentRoute(); // Clean up visuals
  }

  // Called by Main.js input listener on mouse move
  updateGhostMarker(worldPoint) {
    if (!this.isDrafting || !this.graphData) {
      this.ghostMarker.visible = false;
      return;
    }

    if (!worldPoint) {
      this.ghostMarker.visible = false;
      return;
    }

    const nodeId = this.findNearestNode(worldPoint.x, worldPoint.z);
    if (nodeId !== null) {
      const node = this.graphData.nodes[nodeId];
      this.ghostMarker.position.set(node.x, 2, node.y);
      this.ghostMarker.visible = true;
    } else {
      this.ghostMarker.visible = false;
    }
  }

  // ============================
  // Save / Load / Serialization
  // ============================

  getSerializableRoutes() {
    return this.savedRoutes.map(r => ({
      nodes: r.nodes,
      color: r.color
    }));
  }

  loadRoutes(routesData) {
    this.savedRoutes.forEach(r => {
      if (r.mesh) {
        this.scene.remove(r.mesh);
        r.mesh.geometry.dispose();
      }
    });
    this.savedRoutes = [];
    this.servedNodes.clear();

    if (this.vehicleSystem) this.vehicleSystem.clearVehicles();

    routesData.forEach((data, index) => {
      this.rebuildRouteFromData(data.nodes, data.color || this.getRandomColor(), index);
    });

    this.refreshServedNodes();
  }

  rebuildRouteFromData(nodes, color, routeIndex) {
    const pathResult = this.calculateGeometryFromNodes(nodes);
    if (!pathResult) return;

    const tubeMat = new THREE.MeshBasicMaterial({ color: color });
    const mesh = new THREE.Mesh(pathResult.geometry, tubeMat);
    this.scene.add(mesh);

    if (this.vehicleSystem && pathResult.points.length > 0) {
      this.vehicleSystem.addBusToRoute(pathResult.points, color, routeIndex);
    }

    const ridership = this.calculateRidership(nodes);

    this.savedRoutes.push({
      nodes: [...nodes],
      stats: { length: pathResult.length, cost: 0, ridership },
      mesh: mesh,
      color: color
    });
  }

  getRandomColor() {
    const colors = ["#ef4444", "#f97316", "#f59e0b", "#84cc16", "#10b981", "#06b6d4", "#3b82f6", "#8b5cf6", "#d946ef"];
    return colors[Math.floor(Math.random() * colors.length)];
  }

  // ============================
  // Gameplay Actions
  // ============================

  saveCurrentRoute() {
    if (!this.isDrafting) return false;
    if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) {
      alert("Route must have at least 2 points.");
      return false;
    }

    const length = this.currentPathMesh.userData.length || 0;
    const cost = this.gameManager.getProjectedCost(length);

    if (!this.gameManager.canAfford(cost)) {
      alert("Insufficient Funds!");
      return false;
    }

    this.gameManager.deductFunds(cost);

    const color = this.getRandomColor();
    this.currentPathMesh.material.color.set(color);

    const routeIndex = this.savedRoutes.length;

    if (this.vehicleSystem && this.latestPathPoints.length > 0) {
      this.vehicleSystem.addBusToRoute(this.latestPathPoints, color, routeIndex);
    }

    const ridership = this.calculateRidership(this.currentRouteNodes);

    this.savedRoutes.push({
      nodes: [...this.currentRouteNodes],
      stats: { length, cost, ridership },
      mesh: this.currentPathMesh,
      color: color
    });

    // We do NOT call stopDrafting here, UIManager handles the logic to call stopDrafting
    // We just return success
    this.currentPathMesh = null; // Detach mesh from manager so it stays in scene
    this.refreshServedNodes();
    this.gameManager.recalculateApproval();
    this.gameManager.updateUI();

    return true;
  }

  updateRouteColor(index, hexColor) {
    if (index < 0 || index >= this.savedRoutes.length) return;
    const route = this.savedRoutes[index];
    route.color = hexColor;
    if (route.mesh) route.mesh.material.color.set(hexColor);
    if (this.vehicleSystem) this.vehicleSystem.updateRouteColor(index, hexColor);
  }

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

    const route = this.savedRoutes[index];
    if (route.mesh) {
      this.scene.remove(route.mesh);
      route.mesh.geometry.dispose();
    }

    this.savedRoutes.splice(index, 1);

    if (this.vehicleSystem) {
      this.vehicleSystem.clearVehicles();
      this.savedRoutes.forEach((r, idx) => {
        const pathRes = this.calculateGeometryFromNodes(r.nodes);
        if (pathRes && pathRes.points.length > 0) {
          this.vehicleSystem.addBusToRoute(pathRes.points, r.color, idx);
        }
      });
    }

    this.refreshServedNodes();
    this.gameManager.recalculateApproval();
    this.gameManager.updateUI();
  }

  editSavedRoute(index) {
    // Delete and pull back to draft
    if (index < 0 || index >= this.savedRoutes.length) return;

    const route = this.savedRoutes[index];
    this.currentRouteNodes = [...route.nodes];
    this.deleteSavedRoute(index);

    // Visualize draft immediately
    this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId));
    this.updatePathVisuals();
  }

  // ============================
  // Helpers
  // ============================

  calculateGeometryFromNodes(nodeList) {
    if (nodeList.length < 2) return null;

    let fullPathPoints = [];
    let totalDist = 0;

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

      if (!segmentEdges) continue;

      segmentEdges.forEach(step => {
        let dist = step.edgeData.length;
        if (!dist) {
          const p1 = step.edgeData.points[0];
          const p2 = step.edgeData.points[step.edgeData.points.length - 1];
          dist = Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
        }
        totalDist += dist;

        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)));
      });
    }

    if (fullPathPoints.length < 2) return null;

    const curve = new THREE.CatmullRomCurve3(fullPathPoints);
    const geometry = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);

    return { geometry, length: totalDist, points: fullPathPoints };
  }

  calculateRidership(nodeList) {
    if (!this.graphData || nodeList.length < 2) return 0;
    let totalPop = 0;
    let totalJobs = 0;
    nodeList.forEach(nodeId => {
      const node = this.graphData.nodes[nodeId];
      if (node) {
        totalPop += (node.pop || 0);
        totalJobs += (node.jobs || 0);
      }
    });
    const synergy = Math.min(totalPop, totalJobs);
    const GAME_BALANCE_MULTIPLIER = 1.0;
    return Math.floor(synergy * GAME_BALANCE_MULTIPLIER);
  }

  addNodeByWorldPosition(vector3) {
    if (!this.isDrafting) return; // BLOCK INPUT IF NOT DRAFTING
    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.isDrafting) return; // BLOCK DRAG IF NOT DRAFTING
    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();
    }
  }

  clearCurrentRoute() {
    if (this.currentPathMesh) { this.scene.remove(this.currentPathMesh); this.currentPathMesh.geometry.dispose(); this.currentPathMesh = null; }
    this.resetDraftingState();
  }

  resetDraftingState() {
    this.currentRouteNodes = [];
    this.markers.forEach(m => this.scene.remove(m));
    this.markers = [];
    if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
  }

  getSavedRoutes() { return this.savedRoutes; }

  updatePathVisuals() {
    if (this.currentRouteNodes.length < 2) {
      if (this.currentPathMesh) {
        this.scene.remove(this.currentPathMesh);
        this.currentPathMesh = null;
      }
      if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
      return;
    }

    const result = this.calculateGeometryFromNodes(this.currentRouteNodes);
    if (!result) return;

    this.latestPathPoints = result.points;

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

    const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
    this.currentPathMesh = new THREE.Mesh(result.geometry, tubeMat);
    this.currentPathMesh.userData.length = result.length;
    this.scene.add(this.currentPathMesh);

    this.updateMarkerColors();

    const projectedRiders = this.calculateRidership(this.currentRouteNodes);
    const projectedCost = this.gameManager ? this.gameManager.getProjectedCost(result.length) : 0;

    if (this.onRouteChanged) {
      this.onRouteChanged({
        length: result.length,
        cost: projectedCost,
        ridership: projectedRiders
      });
    }
  }

  updateMarkerColors() {
    this.markers.forEach((marker, i) => {
      let color = 0xFFFF00; // Yellow
      if (i === 0) color = this.settings.colors.pathStart;
      else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd;
      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();
  }

  refreshServedNodes() {
    this.servedNodes.clear();
    this.servedCoordinates = [];

    this.savedRoutes.forEach(route => {
      route.nodes.forEach(nodeId => {
        if (!this.servedNodes.has(nodeId)) {
          this.servedNodes.add(nodeId);
          const node = this.graphData.nodes[nodeId];
          if (node) {
            this.servedCoordinates.push({ x: node.x, z: node.y });
          }
        }
      });
    });
  }

  getDistanceToNearestTransit(x, z) {
    if (this.servedCoordinates.length === 0) return Infinity;
    let minSq = Infinity;
    for (let i = 0; i < this.servedCoordinates.length; i++) {
      const sc = this.servedCoordinates[i];
      const dx = sc.x - x;
      const dz = sc.z - z;
      const d2 = dx * dx + dz * dz;
      if (d2 < minSq) minSq = d2;
    }
    return Math.sqrt(minSq);
  }

  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;
  }
}

