/**
 * Simple WebSocket-based room sync
 *
 * This is a lightweight sync layer that works without WASM dependencies.
 * Can be replaced with Automerge later when we have a proper sync server.
 *
 * Architecture:
 * - Each room is a shared state object
 * - Changes are broadcast via WebSocket to all participants
 * - State is stored in localStorage for reconnection
 * - Falls back to local-only mode if WebSocket unavailable
 */

import type { Participant, ParticipantLocation, Waypoint } from '@/types';

// Room state that gets synced
export interface RoomState {
  id: string;
  slug: string;
  name: string;
  createdAt: string;
  participants: Record<string, ParticipantState>;
  waypoints: WaypointState[];
}

export interface ParticipantState {
  id: string;
  name: string;
  emoji: string;
  color: string;
  joinedAt: string;
  lastSeen: string;
  status: string;
  location?: LocationState;
}

export interface LocationState {
  latitude: number;
  longitude: number;
  accuracy: number;
  altitude?: number;
  heading?: number;
  speed?: number;
  timestamp: string;
  source: string;
  indoor?: {
    level: number;
    x: number;
    y: number;
    spaceName?: string;
  };
}

export interface WaypointState {
  id: string;
  name: string;
  emoji?: string;
  latitude: number;
  longitude: number;
  indoor?: {
    level: number;
    x: number;
    y: number;
  };
  createdBy: string;
  createdAt: string;
  type: string;
}

// Message types for sync
export type SyncMessage =
  | { type: 'join'; participant: ParticipantState }
  | { type: 'leave'; participantId: string }
  | { type: 'location'; participantId: string; location: LocationState }
  | { type: 'status'; participantId: string; status: string }
  | { type: 'waypoint_add'; waypoint: WaypointState }
  | { type: 'waypoint_remove'; waypointId: string }
  | { type: 'full_state'; state: RoomState }
  | { type: 'request_state' }
  | { type: 'request_location' };

type SyncCallback = (state: RoomState) => void;
type ConnectionCallback = (connected: boolean) => void;
type LocationRequestCallback = () => void;

// Validate that coordinates are reasonable (not 0,0 or out of bounds)
function isValidLocation(location: LocationState | undefined): boolean {
  if (!location) return false;
  const { latitude, longitude } = location;
  // Basic bounds check
  if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) return false;
  // Reject (0,0) which is a common invalid default
  if (latitude === 0 && longitude === 0) return false;
  // Reject NaN
  if (isNaN(latitude) || isNaN(longitude)) return false;
  return true;
}

export class RoomSync {
  private slug: string;
  private state: RoomState;
  private ws: WebSocket | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private onStateChange: SyncCallback;
  private onConnectionChange: ConnectionCallback;
  private onLocationRequest: LocationRequestCallback | null = null;
  private participantId: string;
  private currentParticipant: ParticipantState | null = null;
  private authToken: string | null = null;

  constructor(
    slug: string,
    participantId: string,
    onStateChange: SyncCallback,
    onConnectionChange: ConnectionCallback,
    onLocationRequest?: LocationRequestCallback,
    authToken?: string | null
  ) {
    this.slug = slug;
    this.participantId = participantId;
    this.onStateChange = onStateChange;
    this.onConnectionChange = onConnectionChange;
    this.onLocationRequest = onLocationRequest || null;
    this.authToken = authToken || null;

    // Initialize or load state
    this.state = this.loadState() || this.createInitialState();
  }

  setLocationRequestCallback(callback: LocationRequestCallback): void {
    this.onLocationRequest = callback;
  }

  private createInitialState(): RoomState {
    return {
      id: crypto.randomUUID(),
      slug: this.slug,
      name: this.slug,
      createdAt: new Date().toISOString(),
      participants: {},
      waypoints: [],
    };
  }

  private loadState(): RoomState | null {
    try {
      const stored = localStorage.getItem(`rmaps_room_${this.slug}`);
      if (stored) {
        const state = JSON.parse(stored) as RoomState;
        // Clean up stale participants (not seen in last hour)
        return this.cleanupStaleParticipants(state);
      }
    } catch (e) {
      console.warn('Failed to load room state:', e);
    }
    return null;
  }

  private cleanupStaleParticipants(state: RoomState): RoomState {
    const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes (was 1 hour)
    const now = Date.now();
    const cleanedParticipants: Record<string, ParticipantState> = {};
    let removedCount = 0;
    let invalidLocationCount = 0;

    for (const [id, participant] of Object.entries(state.participants)) {
      const lastSeen = new Date(participant.lastSeen).getTime();
      const isStale = now - lastSeen > STALE_THRESHOLD_MS;
      const isCurrentUser = id === this.participantId;

      // Keep current user (they'll be updated) and non-stale participants
      if (isCurrentUser || !isStale) {
        // Also clean up invalid locations
        if (participant.location && !isValidLocation(participant.location)) {
          console.log(`Removing invalid location for participant ${id}:`, participant.location);
          delete participant.location;
          invalidLocationCount++;
        }
        cleanedParticipants[id] = participant;
      } else {
        removedCount++;
      }
    }

    if (removedCount > 0) {
      console.log(`Cleaned up ${removedCount} stale participant(s) from room state`);
    }
    if (invalidLocationCount > 0) {
      console.log(`Cleaned up ${invalidLocationCount} invalid location(s) from room state`);
    }

    return {
      ...state,
      participants: cleanedParticipants,
    };
  }

  private saveState(): void {
    try {
      localStorage.setItem(`rmaps_room_${this.slug}`, JSON.stringify(this.state));
    } catch (e) {
      console.warn('Failed to save room state:', e);
    }
  }

  private notifyStateChange(): void {
    this.saveState();
    this.onStateChange({ ...this.state });
  }

  // Connect to sync server (when available)
  connect(syncUrl?: string): void {
    if (!syncUrl) {
      // No sync server - local only mode
      console.log('Running in local-only mode (no sync server)');
      this.onConnectionChange(true);
      return;
    }

    try {
      const wsUrl = this.authToken
        ? `${syncUrl}/room/${this.slug}?token=${encodeURIComponent(this.authToken)}`
        : `${syncUrl}/room/${this.slug}`;
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('Connected to sync server');
        this.onConnectionChange(true);

        // Send join message if we have participant info
        if (this.currentParticipant) {
          console.log('Sending join message for:', this.currentParticipant.name);
          this.send({ type: 'join', participant: this.currentParticipant });
        }
      };

      this.ws.onmessage = (event) => {
        try {
          const message: SyncMessage = JSON.parse(event.data);
          this.handleMessage(message);
        } catch (e) {
          console.warn('Invalid sync message:', e);
        }
      };

      this.ws.onclose = () => {
        console.log('Disconnected from sync server');
        this.onConnectionChange(false);
        this.scheduleReconnect(syncUrl);
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
    } catch (e) {
      console.error('Failed to connect to sync server:', e);
      this.onConnectionChange(false);
    }
  }

  private scheduleReconnect(syncUrl: string): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    this.reconnectTimer = setTimeout(() => {
      console.log('Attempting to reconnect...');
      this.connect(syncUrl);
    }, 5000);
  }

  private send(message: SyncMessage): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    }
  }

  private handleMessage(message: SyncMessage): void {
    switch (message.type) {
      case 'full_state':
        // Merge with local state, keeping our participant
        const myParticipant = this.state.participants[this.participantId];
        this.state = message.state;
        if (myParticipant) {
          this.state.participants[this.participantId] = myParticipant;
        }
        break;

      case 'join':
        this.state.participants[message.participant.id] = message.participant;
        break;

      case 'leave':
        delete this.state.participants[message.participantId];
        break;

      case 'location':
        if (this.state.participants[message.participantId]) {
          // Validate location before accepting
          if (message.location && isValidLocation(message.location)) {
            this.state.participants[message.participantId].location = message.location;
            this.state.participants[message.participantId].lastSeen = new Date().toISOString();
          } else if (message.location === null) {
            // Allow explicit null to clear location
            delete this.state.participants[message.participantId].location;
            this.state.participants[message.participantId].lastSeen = new Date().toISOString();
          } else {
            console.warn('Ignoring invalid location from participant:', message.participantId, message.location);
          }
        }
        break;

      case 'status':
        if (this.state.participants[message.participantId]) {
          this.state.participants[message.participantId].status = message.status;
          this.state.participants[message.participantId].lastSeen = new Date().toISOString();
        }
        break;

      case 'waypoint_add':
        this.state.waypoints.push(message.waypoint);
        break;

      case 'waypoint_remove':
        this.state.waypoints = this.state.waypoints.filter(
          (w) => w.id !== message.waypointId
        );
        break;

      case 'request_location':
        // Server is requesting a location update from us
        console.log('[RoomSync] Received location request from server');
        if (this.onLocationRequest) {
          this.onLocationRequest();
        }
        return; // Don't notify state change for this message type
    }

    this.notifyStateChange();
  }

  // Public methods for updating state
  join(participant: ParticipantState): void {
    // Store participant for sending after WebSocket connects
    this.currentParticipant = participant;
    this.state.participants[participant.id] = participant;

    // Try to send join (will succeed if already connected)
    this.send({ type: 'join', participant });
    this.notifyStateChange();
  }

  leave(): void {
    delete this.state.participants[this.participantId];
    this.send({ type: 'leave', participantId: this.participantId });
    this.notifyStateChange();

    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
  }

  updateLocation(location: LocationState): void {
    console.log('RoomSync.updateLocation called:', location.latitude, location.longitude);
    // Validate location before setting
    if (!isValidLocation(location)) {
      console.warn('Rejecting invalid location:', location);
      return;
    }
    if (this.state.participants[this.participantId]) {
      this.state.participants[this.participantId].location = location;
      this.state.participants[this.participantId].lastSeen = new Date().toISOString();
      this.send({ type: 'location', participantId: this.participantId, location });
      console.log('Location set for participant:', this.participantId, 'Total participants:', Object.keys(this.state.participants).length);
      this.notifyStateChange();
    } else {
      console.warn('Cannot update location - participant not found:', this.participantId);
    }
  }

  clearLocation(): void {
    console.log('RoomSync.clearLocation called');
    if (this.state.participants[this.participantId]) {
      delete this.state.participants[this.participantId].location;
      this.state.participants[this.participantId].lastSeen = new Date().toISOString();
      // Broadcast a null location to clear it for other participants
      this.send({ type: 'location', participantId: this.participantId, location: null as any });
      console.log('Location cleared for participant:', this.participantId);
      this.notifyStateChange();
    }
  }

  updateStatus(status: string): void {
    if (this.state.participants[this.participantId]) {
      this.state.participants[this.participantId].status = status;
      this.state.participants[this.participantId].lastSeen = new Date().toISOString();
      this.send({ type: 'status', participantId: this.participantId, status });
      this.notifyStateChange();
    }
  }

  updateIndoorPosition(indoor: { level: number; x: number; y: number }): void {
    console.log('RoomSync.updateIndoorPosition called:', indoor.level, indoor.x, indoor.y);
    if (this.state.participants[this.participantId]) {
      // Update or create location with indoor data
      // Use CCH venue center as default when no outdoor location exists
      const existingLocation = this.state.participants[this.participantId].location;
      const CCH_CENTER = { latitude: 53.5555, longitude: 9.9898 }; // Hamburg CCH venue center
      const location: LocationState = existingLocation || {
        latitude: CCH_CENTER.latitude,
        longitude: CCH_CENTER.longitude,
        accuracy: 50, // Indoor accuracy
        timestamp: new Date().toISOString(),
        source: 'manual',
      };

      location.indoor = {
        level: indoor.level,
        x: indoor.x,
        y: indoor.y,
      };
      location.timestamp = new Date().toISOString();
      location.source = 'manual';

      this.state.participants[this.participantId].location = location;
      this.state.participants[this.participantId].lastSeen = new Date().toISOString();
      this.send({ type: 'location', participantId: this.participantId, location });
      console.log('Indoor position set for participant:', this.participantId);
      this.notifyStateChange();
    } else {
      console.warn('Cannot update indoor position - participant not found:', this.participantId);
    }
  }

  addWaypoint(waypoint: WaypointState): void {
    this.state.waypoints.push(waypoint);
    this.send({ type: 'waypoint_add', waypoint });
    this.notifyStateChange();
  }

  removeWaypoint(waypointId: string): void {
    this.state.waypoints = this.state.waypoints.filter((w) => w.id !== waypointId);
    this.send({ type: 'waypoint_remove', waypointId });
    this.notifyStateChange();
  }

  getState(): RoomState {
    return { ...this.state };
  }
}

// Convert sync state to typed Participant
export function stateToParticipant(state: ParticipantState): Participant {
  return {
    id: state.id,
    name: state.name,
    emoji: state.emoji,
    color: state.color,
    joinedAt: new Date(state.joinedAt),
    lastSeen: new Date(state.lastSeen),
    status: state.status as Participant['status'],
    location: state.location
      ? {
          latitude: state.location.latitude,
          longitude: state.location.longitude,
          accuracy: state.location.accuracy,
          altitude: state.location.altitude,
          heading: state.location.heading,
          speed: state.location.speed,
          timestamp: new Date(state.location.timestamp),
          source: state.location.source as ParticipantLocation['source'],
          indoor: state.location.indoor,
        }
      : undefined,
    privacySettings: {
      sharingEnabled: true,
      defaultPrecision: 'exact',
      showIndoorFloor: true,
      ghostMode: false,
    },
  };
}

// Convert sync state to typed Waypoint
export function stateToWaypoint(state: WaypointState): Waypoint {
  return {
    id: state.id,
    name: state.name,
    emoji: state.emoji,
    location: {
      latitude: state.latitude,
      longitude: state.longitude,
      indoor: state.indoor,
    },
    createdBy: state.createdBy,
    createdAt: new Date(state.createdAt),
    type: state.type as Waypoint['type'],
  };
}
