/**
 * Presence Manager
 *
 * Manages real-time location sharing with privacy controls.
 * Integrates with zkGPS for commitments and trust circles for
 * precision-based sharing.
 */

import type {
  UserPresence,
  LocationPresence,
  PresenceStatus,
  PresenceBroadcast,
  LocationBroadcastPayload,
  StatusBroadcastPayload,
  ProximityBroadcastPayload,
  PrecisionLevel,
  PresenceView,
  ViewableLocation,
  ProximityInfo,
  PresenceChannelConfig,
  PresenceChannelState,
  PresenceEvent,
  PresenceEventListener,
  LocationSource,
} from './types';
import {
  DEFAULT_PRESENCE_CONFIG,
  TRUST_LEVEL_PRECISION,
  getRadiusForPrecision,
  getPrecisionForTrustLevel,
} from './types';
import type { TrustLevel, GeohashCommitment, GeohashPrecision } from '../privacy/types';
import { TrustCircleManager, createTrustCircleManager } from '../privacy/trustCircles';
import { createCommitment, generateSalt } from '../privacy/commitments';
import { encodeGeohash, getGeohashBounds } from '../privacy/geohash';

// =============================================================================
// Presence Manager
// =============================================================================

/**
 * Manages presence for a channel
 */
export class PresenceManager {
  private config: PresenceChannelConfig;
  private state: PresenceChannelState;
  private trustCircles: TrustCircleManager;
  private listeners: Set<PresenceEventListener> = new Set();
  private updateTimer: ReturnType<typeof setInterval> | null = null;
  private locationWatchId: number | null = null;
  private lastLocationUpdate: number = 0;
  private broadcastCallback: ((broadcast: PresenceBroadcast) => void) | null = null;

  constructor(
    config: Partial<PresenceChannelConfig> & Pick<PresenceChannelConfig, 'channelId' | 'userPubKey' | 'userPrivKey' | 'displayName' | 'color'>
  ) {
    this.config = {
      ...DEFAULT_PRESENCE_CONFIG,
      ...config,
    };

    this.trustCircles = createTrustCircleManager(this.config.userPubKey, this.config.userPubKey);

    this.state = {
      config: this.config,
      self: {
        pubKey: this.config.userPubKey,
        displayName: this.config.displayName,
        color: this.config.color,
        location: null,
        status: 'online',
        lastSeen: new Date(),
        isMoving: false,
        deviceType: this.detectDeviceType(),
      },
      others: new Map(),
      views: new Map(),
      connectionState: 'connecting',
      lastSequence: 0,
    };
  }

  // ===========================================================================
  // Lifecycle
  // ===========================================================================

  /**
   * Start presence sharing
   */
  start(broadcastCallback: (broadcast: PresenceBroadcast) => void): void {
    this.broadcastCallback = broadcastCallback;
    this.state.connectionState = 'connected';

    // Start periodic presence updates
    this.updateTimer = setInterval(() => {
      this.broadcastPresence();
    }, this.config.updateInterval);

    // Broadcast initial presence
    this.broadcastPresence();

    this.emit({ type: 'connection:changed', state: 'connected' });
  }

  /**
   * Stop presence sharing
   */
  stop(): void {
    if (this.updateTimer) {
      clearInterval(this.updateTimer);
      this.updateTimer = null;
    }

    this.stopLocationWatch();

    // Broadcast leave message
    if (this.broadcastCallback) {
      this.broadcastCallback(this.createBroadcast('leave', null));
    }

    this.state.connectionState = 'disconnected';
    this.emit({ type: 'connection:changed', state: 'disconnected' });
  }

  // ===========================================================================
  // Location Sharing
  // ===========================================================================

  /**
   * Start watching device location
   */
  startLocationWatch(): void {
    if (!navigator.geolocation) {
      console.warn('Geolocation not available');
      return;
    }

    this.locationWatchId = navigator.geolocation.watchPosition(
      (position) => this.handleLocationUpdate(position),
      (error) => this.handleLocationError(error),
      {
        enableHighAccuracy: true,
        maximumAge: 5000,
        timeout: 10000,
      }
    );
  }

  /**
   * Stop watching device location
   */
  stopLocationWatch(): void {
    if (this.locationWatchId !== null && navigator.geolocation) {
      navigator.geolocation.clearWatch(this.locationWatchId);
      this.locationWatchId = null;
    }
  }

  /**
   * Handle location update from device
   */
  private async handleLocationUpdate(position: GeolocationPosition): Promise<void> {
    const now = Date.now();

    // Throttle updates
    if (now - this.lastLocationUpdate < this.config.locationThrottle) {
      return;
    }
    this.lastLocationUpdate = now;

    const coords = position.coords;

    // Determine if moving based on speed
    const isMoving = (coords.speed ?? 0) > 0.5; // > 0.5 m/s = moving

    // Create zkGPS commitment for the location
    const fullGeohash = encodeGeohash(coords.latitude, coords.longitude, 12);
    const salt = generateSalt();
    const baseCommitment = await createCommitment({
      coordinate: { lat: coords.latitude, lng: coords.longitude },
      precision: 12 as GeohashPrecision,
      salt,
    });

    // Create GeohashCommitment from LocationCommitment
    const commitment: GeohashCommitment = {
      commitment: baseCommitment.commitment,
      geohash: fullGeohash,
      precision: baseCommitment.precision,
      timestamp: baseCommitment.timestamp,
      expiresAt: baseCommitment.expiresAt,
      salt,
    };

    // Update self location
    this.state.self.location = {
      coordinates: {
        latitude: coords.latitude,
        longitude: coords.longitude,
        altitude: coords.altitude ?? undefined,
        accuracy: coords.accuracy,
        heading: coords.heading ?? undefined,
        speed: coords.speed ?? undefined,
      },
      commitment,
      timestamp: new Date(position.timestamp),
      source: 'gps',
      isLive: true,
    };

    this.state.self.isMoving = isMoving;
    this.state.self.lastSeen = new Date();

    // Broadcast location update
    this.broadcastLocation();
  }

  /**
   * Handle location error
   */
  private handleLocationError(error: GeolocationPositionError): void {
    console.warn('Location error:', error.message);
    this.emit({ type: 'error', error: `Location error: ${error.message}` });
  }

  /**
   * Manually set location (for testing or manual input)
   */
  async setLocation(
    latitude: number,
    longitude: number,
    source: LocationSource = 'manual'
  ): Promise<void> {
    const salt = generateSalt();
    const fullGeohash = encodeGeohash(latitude, longitude, 12);
    const baseCommitment = await createCommitment({
      coordinate: { lat: latitude, lng: longitude },
      precision: 12 as GeohashPrecision,
      salt,
    });

    // Create GeohashCommitment from LocationCommitment
    const commitment: GeohashCommitment = {
      commitment: baseCommitment.commitment,
      geohash: fullGeohash,
      precision: baseCommitment.precision,
      timestamp: baseCommitment.timestamp,
      expiresAt: baseCommitment.expiresAt,
      salt,
    };

    this.state.self.location = {
      coordinates: {
        latitude,
        longitude,
      },
      commitment,
      timestamp: new Date(),
      source,
      isLive: source === 'gps',
    };

    this.state.self.lastSeen = new Date();
    this.broadcastLocation();
  }

  /**
   * Clear current location (stop sharing)
   */
  clearLocation(): void {
    this.state.self.location = null;
    this.broadcastPresence();
  }

  // ===========================================================================
  // Broadcasting
  // ===========================================================================

  /**
   * Broadcast current presence
   */
  private broadcastPresence(): void {
    if (!this.broadcastCallback) return;

    if (this.state.self.location) {
      this.broadcastLocation();
    } else {
      this.broadcastStatus();
    }
  }

  /**
   * Broadcast location update
   */
  private broadcastLocation(): void {
    if (!this.broadcastCallback || !this.state.self.location) return;

    const location = this.state.self.location;

    // Create precision levels for each trust level
    const precisionLevels: PrecisionLevel[] = [];
    const fullGeohash = location.commitment.geohash;

    for (const [level, precision] of Object.entries(TRUST_LEVEL_PRECISION)) {
      precisionLevels.push({
        trustLevel: level as TrustLevel,
        geohash: fullGeohash.substring(0, precision),
        precision,
      });
    }

    const payload: LocationBroadcastPayload = {
      commitment: location.commitment,
      precisionLevels,
      isMoving: this.state.self.isMoving,
      heading: location.coordinates.heading,
      speedCategory: this.getSpeedCategory(location.coordinates.speed),
    };

    const broadcast = this.createBroadcast('location', payload);
    this.broadcastCallback(broadcast);
  }

  /**
   * Broadcast status update
   */
  private broadcastStatus(): void {
    if (!this.broadcastCallback) return;

    const payload: StatusBroadcastPayload = {
      status: this.state.self.status,
      message: this.state.self.statusMessage,
      deviceType: this.state.self.deviceType,
    };

    const broadcast = this.createBroadcast('status', payload);
    this.broadcastCallback(broadcast);
  }

  /**
   * Create a broadcast message
   */
  private createBroadcast(
    type: PresenceBroadcast['type'],
    payload: PresenceBroadcast['payload']
  ): PresenceBroadcast {
    this.state.lastSequence++;

    return {
      senderPubKey: this.config.userPubKey,
      type,
      payload,
      signature: this.signBroadcast(type, payload),
      timestamp: new Date(),
      sequence: this.state.lastSequence,
      ttl: this.config.presenceTtl,
    };
  }

  /**
   * Sign a broadcast (simplified - in production use proper crypto)
   */
  private signBroadcast(type: string, payload: any): string {
    const message = JSON.stringify({ type, payload, key: this.config.userPrivKey });
    // In production, use proper signing
    let hash = 0;
    for (let i = 0; i < message.length; i++) {
      hash = (hash << 5) - hash + message.charCodeAt(i);
      hash = hash & hash;
    }
    return hash.toString(16);
  }

  // ===========================================================================
  // Receiving
  // ===========================================================================

  /**
   * Handle incoming broadcast from another user
   */
  handleBroadcast(broadcast: PresenceBroadcast): void {
    // Ignore our own broadcasts
    if (broadcast.senderPubKey === this.config.userPubKey) return;

    // Check TTL
    const age = (Date.now() - broadcast.timestamp.getTime()) / 1000;
    if (age > broadcast.ttl) {
      return; // Expired
    }

    switch (broadcast.type) {
      case 'location':
        this.handleLocationBroadcast(broadcast);
        break;
      case 'status':
        this.handleStatusBroadcast(broadcast);
        break;
      case 'proximity':
        this.handleProximityBroadcast(broadcast);
        break;
      case 'leave':
        this.handleLeaveBroadcast(broadcast);
        break;
    }
  }

  /**
   * Handle location broadcast
   */
  private handleLocationBroadcast(broadcast: PresenceBroadcast): void {
    const payload = broadcast.payload as LocationBroadcastPayload;
    const senderKey = broadcast.senderPubKey;

    // Get or create user presence
    let user = this.state.others.get(senderKey);
    const isNew = !user;

    if (!user) {
      user = {
        pubKey: senderKey,
        displayName: senderKey.substring(0, 8) + '...',
        color: this.generateUserColor(senderKey),
        location: null,
        status: 'online',
        lastSeen: new Date(),
        isMoving: false,
        deviceType: 'unknown',
      };
      this.state.others.set(senderKey, user);
    }

    // Update user's location (we store the commitment, not decoded location)
    user.location = {
      coordinates: { latitude: 0, longitude: 0 }, // We don't know exact coords
      commitment: payload.commitment,
      timestamp: broadcast.timestamp,
      source: 'network' as LocationSource,
      isLive: true,
    };
    user.isMoving = payload.isMoving;
    user.lastSeen = broadcast.timestamp;
    user.status = 'online';

    // Create view for this user based on trust level
    const view = this.createPresenceView(user, payload);
    this.state.views.set(senderKey, view);

    if (isNew) {
      this.emit({ type: 'user:joined', user });
    } else {
      this.emit({ type: 'user:updated', user, changes: ['location'] });
    }

    if (view.location) {
      this.emit({ type: 'location:updated', pubKey: senderKey, location: view.location });
    }
  }

  /**
   * Handle status broadcast
   */
  private handleStatusBroadcast(broadcast: PresenceBroadcast): void {
    const payload = broadcast.payload as StatusBroadcastPayload;
    const senderKey = broadcast.senderPubKey;

    let user = this.state.others.get(senderKey);
    if (!user) {
      user = {
        pubKey: senderKey,
        displayName: senderKey.substring(0, 8) + '...',
        color: this.generateUserColor(senderKey),
        location: null,
        status: payload.status,
        lastSeen: broadcast.timestamp,
        isMoving: false,
        deviceType: payload.deviceType ?? 'unknown',
      };
      this.state.others.set(senderKey, user);
      this.emit({ type: 'user:joined', user });
    } else {
      user.status = payload.status;
      user.statusMessage = payload.message;
      user.lastSeen = broadcast.timestamp;
      if (payload.deviceType) user.deviceType = payload.deviceType;
      this.emit({ type: 'status:changed', pubKey: senderKey, status: payload.status });
    }
  }

  /**
   * Handle proximity broadcast
   */
  private handleProximityBroadcast(broadcast: PresenceBroadcast): void {
    const payload = broadcast.payload as ProximityBroadcastPayload;

    // Only process if we're the target
    if (payload.targetPubKey !== this.config.userPubKey) return;

    const senderKey = broadcast.senderPubKey;
    const view = this.state.views.get(senderKey);

    if (view) {
      view.proximity = {
        category: payload.distanceCategory,
        verified: true, // Has proof
        mutuallyVisible: true,
      };

      this.emit({ type: 'proximity:detected', pubKey: senderKey, proximity: view.proximity });
    }
  }

  /**
   * Handle leave broadcast
   */
  private handleLeaveBroadcast(broadcast: PresenceBroadcast): void {
    const senderKey = broadcast.senderPubKey;

    this.state.others.delete(senderKey);
    this.state.views.delete(senderKey);

    this.emit({ type: 'user:left', pubKey: senderKey });
  }

  /**
   * Create a presence view based on trust level
   */
  private createPresenceView(
    user: UserPresence,
    payload: LocationBroadcastPayload
  ): PresenceView {
    // Get trust level for this user
    const trustLevel = this.trustCircles.getTrustLevel(user.pubKey) ?? 'public';

    // Find the precision level for our trust relationship
    const precisionLevel = payload.precisionLevels.find(
      (p) => p.trustLevel === trustLevel
    );

    let location: ViewableLocation | null = null;

    if (precisionLevel) {
      const geohash = precisionLevel.geohash;
      const bounds = getGeohashBounds(geohash);
      const center = {
        latitude: (bounds.minLat + bounds.maxLat) / 2,
        longitude: (bounds.minLng + bounds.maxLng) / 2,
      };

      const ageSeconds = (Date.now() - payload.commitment.timestamp) / 1000;

      location = {
        geohash,
        precision: precisionLevel.precision,
        center,
        bounds,
        uncertaintyRadius: getRadiusForPrecision(precisionLevel.precision),
        ageSeconds,
        isMoving: payload.isMoving,
        heading: payload.heading,
        speedCategory: payload.speedCategory,
      };
    }

    // Calculate proximity if we have our own location
    let proximity: ProximityInfo | undefined;
    if (location && this.state.self.location) {
      proximity = this.calculateProximity(location);
    }

    return {
      user: {
        pubKey: user.pubKey,
        displayName: user.displayName,
        color: user.color,
      },
      location,
      status: user.status,
      lastSeen: user.lastSeen,
      trustLevel,
      isVerified: true, // Has commitment
      proximity,
    };
  }

  /**
   * Calculate proximity to another user
   */
  private calculateProximity(otherLocation: ViewableLocation): ProximityInfo {
    if (!this.state.self.location) {
      return { category: 'far', verified: false, mutuallyVisible: false };
    }

    const myCoords = this.state.self.location.coordinates;
    const distance = this.haversineDistance(
      myCoords.latitude,
      myCoords.longitude,
      otherLocation.center.latitude,
      otherLocation.center.longitude
    );

    let category: ProximityInfo['category'];
    if (distance < 50) category = 'here';
    else if (distance < 500) category = 'nearby';
    else if (distance < 5000) category = 'same-area';
    else if (distance < 50000) category = 'same-city';
    else category = 'far';

    return {
      category,
      verified: false,
      approximateMeters: distance,
      mutuallyVisible: distance < otherLocation.uncertaintyRadius * 2,
    };
  }

  // ===========================================================================
  // Trust Circle Management
  // ===========================================================================

  /**
   * Set trust level for a contact
   */
  setTrustLevel(pubKey: string, level: TrustLevel): void {
    this.trustCircles.setTrustLevel(pubKey, level);

    // Update view if we have one
    const user = this.state.others.get(pubKey);
    if (user && user.location) {
      // Re-request their location at new precision
      // In a real implementation, this would request updated data
    }
  }

  /**
   * Get trust level for a contact
   */
  getTrustLevel(pubKey: string): TrustLevel {
    return this.trustCircles.getTrustLevel(pubKey) ?? 'public';
  }

  /**
   * Get trust circles manager
   */
  getTrustCircles(): TrustCircleManager {
    return this.trustCircles;
  }

  // ===========================================================================
  // Status Management
  // ===========================================================================

  /**
   * Set own status
   */
  setStatus(status: PresenceStatus, message?: string): void {
    this.state.self.status = status;
    this.state.self.statusMessage = message;
    this.broadcastStatus();
  }

  /**
   * Get own status
   */
  getStatus(): PresenceStatus {
    return this.state.self.status;
  }

  // ===========================================================================
  // Queries
  // ===========================================================================

  /**
   * Get all presence views
   */
  getViews(): PresenceView[] {
    return Array.from(this.state.views.values());
  }

  /**
   * Get view for a specific user
   */
  getView(pubKey: string): PresenceView | undefined {
    return this.state.views.get(pubKey);
  }

  /**
   * Get all online users
   */
  getOnlineUsers(): UserPresence[] {
    return Array.from(this.state.others.values()).filter(
      (u) => u.status === 'online' || u.status === 'away'
    );
  }

  /**
   * Get users within a distance category
   */
  getUsersNearby(
    maxCategory: ProximityInfo['category'] = 'same-area'
  ): PresenceView[] {
    const categories: ProximityInfo['category'][] = [
      'here',
      'nearby',
      'same-area',
      'same-city',
      'far',
    ];
    const maxIndex = categories.indexOf(maxCategory);

    return Array.from(this.state.views.values()).filter((v) => {
      if (!v.proximity) return false;
      const viewIndex = categories.indexOf(v.proximity.category);
      return viewIndex <= maxIndex;
    });
  }

  /**
   * Get current state
   */
  getState(): PresenceChannelState {
    return this.state;
  }

  /**
   * Get own presence
   */
  getSelf(): UserPresence {
    return this.state.self;
  }

  // ===========================================================================
  // Events
  // ===========================================================================

  /**
   * Subscribe to events
   */
  on(listener: PresenceEventListener): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private emit(event: PresenceEvent): void {
    for (const listener of this.listeners) {
      try {
        listener(event);
      } catch (e) {
        console.error('Error in presence event listener:', e);
      }
    }
  }

  // ===========================================================================
  // Utilities
  // ===========================================================================

  /**
   * Detect device type
   */
  private detectDeviceType(): UserPresence['deviceType'] {
    if (typeof navigator === 'undefined') return 'unknown';

    const ua = navigator.userAgent.toLowerCase();
    if (/mobile|android|iphone|ipad|ipod/.test(ua)) {
      if (/ipad|tablet/.test(ua)) return 'tablet';
      return 'mobile';
    }
    return 'desktop';
  }

  /**
   * Get speed category from speed in m/s
   */
  private getSpeedCategory(
    speed?: number
  ): LocationBroadcastPayload['speedCategory'] {
    if (speed === undefined || speed < 0.5) return 'stationary';
    if (speed < 2) return 'walking';
    if (speed < 8) return 'cycling';
    if (speed < 50) return 'driving';
    return 'flying';
  }

  /**
   * Generate color from public key
   */
  private generateUserColor(pubKey: string): string {
    let hash = 0;
    for (let i = 0; i < pubKey.length; i++) {
      hash = pubKey.charCodeAt(i) + ((hash << 5) - hash);
    }
    const hue = hash % 360;
    return `hsl(${hue}, 70%, 50%)`;
  }

  /**
   * Haversine distance calculation
   */
  private haversineDistance(
    lat1: number,
    lon1: number,
    lat2: number,
    lon2: number
  ): number {
    const R = 6371000; // Earth radius in meters
    const dLat = ((lat2 - lat1) * Math.PI) / 180;
    const dLon = ((lon2 - lon1) * Math.PI) / 180;
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos((lat1 * Math.PI) / 180) *
        Math.cos((lat2 * Math.PI) / 180) *
        Math.sin(dLon / 2) *
        Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }
}

// =============================================================================
// Factory Function
// =============================================================================

/**
 * Create a presence manager
 */
export function createPresenceManager(
  config: Partial<PresenceChannelConfig> &
    Pick<PresenceChannelConfig, 'channelId' | 'userPubKey' | 'userPrivKey' | 'displayName' | 'color'>
): PresenceManager {
  return new PresenceManager(config);
}
