/**
 * useFleet.js — Multi-robot fleet connection manager.
 *
 * Maintains one ROSLIB.Ros WebSocket connection per robot.
 * Aggregates fleet-summary telemetry (battery, pose, alerts, mode) for each.
 * Persists robot registry to localStorage.
 *
 * API:
 *   const { robots, addRobot, removeRobot, updateRobot,
 *           connections, robotData,
 *           subscribeRobot, publishRobot, scanSubnet } = useFleet();
 *
 * Robot shape:
 *   { id, name, variant, host, port, videoPort, videoTopic }
 *   variant: 'balance' | 'rover' | 'tank' | 'social'
 *   videoTopic: '/camera/panoramic/compressed' (default)
 */

import { useState, useEffect, useRef, useCallback } from 'react';
import ROSLIB from 'roslib';

// ── Storage ────────────────────────────────────────────────────────────────────

const STORAGE_KEY = 'saltybot_fleet_v2';

const DEFAULT_ROBOTS = [
  {
    id: 'salty-01',
    name: 'SaltyBot #1',
    variant: 'balance',
    host: '192.168.1.100',
    port: 9090,
    videoPort: 8080,
    videoTopic: '/camera/panoramic/compressed',
  },
];

function loadRobots() {
  try {
    const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
    return Array.isArray(saved) && saved.length > 0 ? saved : DEFAULT_ROBOTS;
  } catch {
    return DEFAULT_ROBOTS;
  }
}

// ── Robot data structure ───────────────────────────────────────────────────────

const EMPTY_DATA = () => ({
  state:       null,   // 'DISARMED'|'ARMED'|'TILT FAULT'
  esp32Mode:   null,   // 'MANUAL'|'ASSISTED'
  controlMode: null,   // 'RC'|'RAMP_TO_AUTO'|'AUTO'|'RAMP_TO_RC'
  blendAlpha:  0,
  pipeline:    null,   // 'idle'|'listening'|'thinking'|'speaking'
  pitch:       0,
  motorCmd:    0,
  battery:     { voltage: 0, soc: 0 },
  pose:        null,   // { x, y, yaw }
  alerts:      [],     // [{ level, name, message }]
  lastSeen:    null,
});

// ── Hook ──────────────────────────────────────────────────────────────────────

export function useFleet() {
  const [robots, setRobots]           = useState(loadRobots);
  const [connections, setConnections] = useState({});
  const [robotData, setRobotData]     = useState(() =>
    Object.fromEntries(loadRobots().map(r => [r.id, EMPTY_DATA()]))
  );
  const [scanning, setScanning]       = useState(false);

  const rosRef  = useRef({});  // { [id]: ROSLIB.Ros }
  const subsRef = useRef({});  // { [id]: ROSLIB.Topic[] }

  // Persist robots
  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(robots));
  }, [robots]);

  // ── Connection lifecycle ────────────────────────────────────────────────────

  useEffect(() => {
    const currentIds = new Set(robots.map(r => r.id));

    // Disconnect removed robots
    Object.keys(rosRef.current).forEach(id => {
      if (!currentIds.has(id)) {
        subsRef.current[id]?.forEach(t => { try { t.unsubscribe(); } catch {} });
        delete subsRef.current[id];
        try { rosRef.current[id].close(); } catch {}
        delete rosRef.current[id];
        setConnections(c => { const n = { ...c }; delete n[id]; return n; });
      }
    });

    // Connect new robots
    robots.forEach(robot => {
      if (rosRef.current[robot.id]) return;

      const ros = new ROSLIB.Ros({ url: `ws://${robot.host}:${robot.port}` });
      rosRef.current[robot.id] = ros;
      subsRef.current[robot.id] = [];

      ros.on('connection', () => {
        setConnections(c => ({ ...c, [robot.id]: { connected: true, error: null } }));
        _setupSubs(robot.id, ros);
      });
      ros.on('error', err => {
        setConnections(c => ({
          ...c,
          [robot.id]: { connected: false, error: String(err?.message ?? err ?? 'error') }
        }));
      });
      ros.on('close', () => {
        setConnections(c => ({
          ...c,
          [robot.id]: { connected: false, error: null }
        }));
      });
    });

    return () => {
      Object.values(subsRef.current).forEach(subs =>
        subs.forEach(t => { try { t.unsubscribe(); } catch {} })
      );
      Object.values(rosRef.current).forEach(ros => { try { ros.close(); } catch {} });
      rosRef.current = {};
      subsRef.current = {};
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [robots]);

  // ── Fleet-summary subscriptions ────────────────────────────────────────────

  function _sub(id, ros, name, type, cb) {
    const t = new ROSLIB.Topic({ ros, name, messageType: type });
    t.subscribe(cb);
    subsRef.current[id] = [...(subsRef.current[id] ?? []), t];
  }

  function _update(id, patch) {
    setRobotData(prev => ({
      ...prev,
      [id]: { ...(prev[id] ?? EMPTY_DATA()), ...patch },
    }));
  }

  function _setupSubs(id, ros) {
    // Balance state
    _sub(id, ros, '/saltybot/balance_state', 'std_msgs/String', msg => {
      try {
        const d = JSON.parse(msg.data);
        _update(id, {
          state:     d.state,
          esp32Mode: d.mode,
          pitch:     d.pitch_deg ?? 0,
          motorCmd:  d.motor_cmd ?? 0,
          lastSeen:  Date.now(),
        });
      } catch {}
    });

    // Control mode (RC ↔ AUTO blend)
    _sub(id, ros, '/saltybot/control_mode', 'std_msgs/String', msg => {
      try {
        const d = JSON.parse(msg.data);
        _update(id, { controlMode: d.mode, blendAlpha: d.blend_alpha ?? 0 });
      } catch {}
    });

    // Odometry → pose
    _sub(id, ros, '/odom', 'nav_msgs/Odometry', msg => {
      const p = msg.pose?.pose;
      if (!p) return;
      const o = p.orientation;
      const yaw = Math.atan2(
        2 * (o.w * o.z + o.x * o.y),
        1 - 2 * (o.y * o.y + o.z * o.z)
      );
      _update(id, { pose: { x: p.position.x, y: p.position.y, yaw }, lastSeen: Date.now() });
    });

    // Diagnostics → battery + alerts
    _sub(id, ros, '/diagnostics', 'diagnostic_msgs/DiagnosticArray', msg => {
      const alerts = [];
      const kv = {};
      for (const status of msg.status ?? []) {
        if (status.level > 0) {
          alerts.push({ level: status.level, name: status.name, message: status.message });
        }
        for (const pair of status.values ?? []) kv[pair.key] = pair.value;
      }
      const patch = { alerts };
      if (kv.battery_voltage_v != null) {
        const v = parseFloat(kv.battery_voltage_v);
        patch.battery = {
          voltage: v,
          soc: Math.max(0, Math.min(100, ((v - 12) / (16.8 - 12)) * 100)),
        };
      }
      _update(id, patch);
    });

    // Social pipeline state (optional)
    _sub(id, ros, '/social/orchestrator/state', 'std_msgs/String', msg => {
      try {
        _update(id, { pipeline: JSON.parse(msg.data)?.state ?? null });
      } catch {}
    });
  }

  // ── Public API ─────────────────────────────────────────────────────────────

  const addRobot = useCallback(robot => {
    const id = robot.id ?? `bot-${Date.now()}`;
    const full = {
      videoPort: 8080,
      videoTopic: '/camera/panoramic/compressed',
      ...robot,
      id,
    };
    setRobots(r => [...r.filter(b => b.id !== id), full]);
    setRobotData(d => ({ ...d, [id]: EMPTY_DATA() }));
  }, []);

  const removeRobot = useCallback(id => {
    setRobots(r => r.filter(b => b.id !== id));
    setRobotData(d => { const n = { ...d }; delete n[id]; return n; });
  }, []);

  const updateRobot = useCallback((id, patch) => {
    // Force reconnect by dropping the existing connection
    subsRef.current[id]?.forEach(t => { try { t.unsubscribe(); } catch {} });
    subsRef.current[id] = [];
    try { rosRef.current[id]?.close(); } catch {}
    delete rosRef.current[id];
    setRobots(r => r.map(b => b.id === id ? { ...b, ...patch } : b));
  }, []);

  /** Subscribe to a topic on a specific robot. Returns unsubscribe fn. */
  const subscribeRobot = useCallback((id, name, type, cb) => {
    const ros = rosRef.current[id];
    if (!ros) return () => {};
    const t = new ROSLIB.Topic({ ros, name, messageType: type });
    t.subscribe(cb);
    subsRef.current[id] = [...(subsRef.current[id] ?? []), t];
    return () => {
      try { t.unsubscribe(); } catch {}
      subsRef.current[id] = subsRef.current[id]?.filter(x => x !== t) ?? [];
    };
  }, []);

  /** Publish to a topic on a specific robot. */
  const publishRobot = useCallback((id, name, type, data) => {
    const ros = rosRef.current[id];
    if (!ros) return;
    const t = new ROSLIB.Topic({ ros, name, messageType: type });
    t.publish(new ROSLIB.Message(data));
  }, []);

  /** Send a single Nav2 goal pose to a robot. */
  const sendGoal = useCallback((id, x, y, yaw = 0) => {
    const cy2 = Math.cos(yaw / 2);
    const sy2 = Math.sin(yaw / 2);
    publishRobot(id, '/goal_pose', 'geometry_msgs/PoseStamped', {
      header: { frame_id: 'map', stamp: { sec: 0, nanosec: 0 } },
      pose: {
        position: { x, y, z: 0 },
        orientation: { x: 0, y: 0, z: sy2, w: cy2 },
      },
    });
  }, [publishRobot]);

  /** Send a patrol route (array of {x,y,yaw?}) to a robot via PoseArray → GPS follower. */
  const sendPatrol = useCallback((id, waypoints) => {
    publishRobot(id, '/outdoor/waypoints', 'geometry_msgs/PoseArray', {
      header: { frame_id: 'map', stamp: { sec: 0, nanosec: 0 } },
      poses: waypoints.map(({ x, y, yaw = 0 }) => {
        const cy2 = Math.cos(yaw / 2);
        const sy2 = Math.sin(yaw / 2);
        return {
          position: { x, y, z: 0 },
          orientation: { x: 0, y: 0, z: sy2, w: cy2 },
        };
      }),
    });
  }, [publishRobot]);

  /**
   * Probe a list of candidate URLs to find reachable rosbridge servers.
   * Reports each discovered robot back via the addRobot callback.
   */
  const scanSubnet = useCallback(async (baseIp, portRange = [9090, 9097]) => {
    setScanning(true);
    const [portMin, portMax] = portRange;
    const ipParts = baseIp.split('.');
    const prefix = ipParts.slice(0, 3).join('.');

    const results = [];
    const probes = [];
    for (let host4 = 1; host4 <= 254; host4++) {
      for (let port = portMin; port <= portMax; port++) {
        probes.push({ host: `${prefix}.${host4}`, port });
      }
    }

    // Probe in batches of 32 to avoid thundering-herd
    const BATCH = 32;
    for (let i = 0; i < probes.length; i += BATCH) {
      const batch = probes.slice(i, i + BATCH);
      await Promise.allSettled(
        batch.map(({ host, port }) =>
          new Promise(resolve => {
            const ws = new WebSocket(`ws://${host}:${port}`);
            const timer = setTimeout(() => { try { ws.close(); } catch {} resolve(null); }, 800);
            ws.onopen = () => {
              clearTimeout(timer);
              results.push({ host, port });
              ws.close();
              resolve({ host, port });
            };
            ws.onerror = () => { clearTimeout(timer); resolve(null); };
          })
        )
      );
    }

    setScanning(false);
    return results;
  }, []);

  return {
    robots,
    addRobot,
    removeRobot,
    updateRobot,
    connections,
    robotData,
    subscribeRobot,
    publishRobot,
    sendGoal,
    sendPatrol,
    scanning,
    scanSubnet,
  };
}
