/**
 * Manages fetching and caching entity data from Home Assistant
 */
import { ACTIVITY_THRESHOLDS } from './constants.js';

export class EntityDataFetcher {
  constructor(debugMode = false) {
    this._entityCache = {};
    this._debug = debugMode;
    this._hass = null;
    this._entities = [];
    this._positionHistory = new Map(); // entityId → PositionEntry[]
    this._lastPredictedActivity = new Map(); // entityId → string (last known predicted activity)
    this._candidateActivity = new Map(); // entityId → { activity: string, timestamp: number }
    this._activityStabilityMs = 3000; // 3 second hysteresis
  }

  /**
   * Sets the Home Assistant instance
   * @param {Object} hass - Home Assistant instance
   */
  setHass(hass) {
    this._hass = hass;
  }

  /**
   * Sets the entities to fetch
   * @param {Array} entities - Array of entity configurations
   */
  setEntities(entities) {
    this._entities = entities;
  }

  /**
   * Sets debug mode
   * @param {boolean} debug - Debug mode flag
   */
  setDebugMode(debug) {
    this._debug = debug;
  }

  /**
   * Logs debug messages
   * @param {string} message - Message to log
   * @param {...any} args - Additional arguments
   */
  _log(message, ...args) {
    if (this._debug) {
      console.log(`[EntityDataFetcher ${new Date().toISOString()}] ${message}`, ...args);
    }
  }

  /**
   * Fetches entity data from Home Assistant
   * @param {Object} config - Card configuration
   * @returns {Promise<Object|null>} Prepared entity data or null if no valid data
   */
  async fetchEntities(config) {
    if (!this._hass || !this._entities) {
      this._log('Cannot fetch entities: hass or entities not available');
      return null;
    }

    this._log('Fetching entities from hass...');

    let hasValidData = false;

    for (const entityConfig of this._entities) {
      try {
        // Fetch person entity from hass states
        const personState = this._hass.states[entityConfig.person];
        if (!personState) {
          console.error(`[EntityDataFetcher] Entity not found: ${entityConfig.person}`);
          continue;
        }

        // Check for valid GPS data
        if (!personState.attributes.latitude || !personState.attributes.longitude) {
          this._log(`No GPS data for ${entityConfig.person}`);
          continue;
        }

        // Fetch activity entity if specified
        let activityState = null;
        if (entityConfig.activity) {
          activityState = this._hass.states[entityConfig.activity];
        }

        // Calculate speed and update position history
        const currentPosition = {
          latitude: personState.attributes.latitude,
          longitude: personState.attributes.longitude,
          timestamp: Date.now()
        };

        const speedData = this.calculateSpeed(entityConfig.person, currentPosition);
        this._updatePositionHistory(entityConfig.person, currentPosition);

        // Store in cache
        this._entityCache[entityConfig.person] = {
          person: personState,
          activity: activityState,
          speed: speedData,
          predicted_activity: speedData ? this.predictActivity(speedData.speed_kmh) : null,
          timestamp: Date.now()
        };

        hasValidData = true;

        this._log(`Cached entity ${entityConfig.person}:`, personState.state,
          'Location:', personState.attributes.latitude, personState.attributes.longitude,
          'Speed:', speedData ? `${speedData.speed_kmh.toFixed(1)} km/h` : 'N/A');
      } catch (error) {
        console.error(`[EntityDataFetcher] Error fetching ${entityConfig.person}:`, error);
      }
    }

    if (!hasValidData) {
      this._log('No valid entity data to return');
      return null;
    }

    return this.prepareEntityData(config);
  }

  /**
   * Determines which activity to use based on configuration
   * @param {Object} data - Entity cache data
   * @param {string} entityId - Entity identifier
   * @param {Object} config - Card configuration
   * @returns {string} Selected activity state
   */
  _getActivityToUse(data, entityId, config) {
    // Safety check for config
    if (!config) {
      this._log(`[Warning] No config provided to _getActivityToUse for ${entityId}. Defaulting to sensor.`);
    }

    // Check if speed-based prediction is enabled
    // We use optional chaining and strict equality to ensure we only enter this block
    // if the user explicitly selected 'speed_predicted'
    if (config?.activity_source === 'speed_predicted') {
      if (data.speed && data.speed.speed_kmh !== null && data.speed.speed_kmh !== undefined) {
        // Use stable predicted activity with hysteresis
        const activity = this._getStablePredictedActivity(entityId, data.speed.speed_kmh);
        if (activity) {
          this._log(`Activity for ${entityId}: predicted(${activity}) - speed available`);
          return activity;
        } else {
          // Activity is stabilizing, use last predicted activity if available
          const lastActivity = this._lastPredictedActivity.get(entityId);
          if (lastActivity) {
            this._log(`Activity for ${entityId}: stable(${lastActivity}) - stabilizing`);
            return lastActivity;
          }
          // No last activity, use unknown
          this._log(`Activity for ${entityId}: unknown - stabilizing, no last activity`);
          return 'unknown';
        }
      } else {
        // Speed is null, use sticky last predicted activity if available
        const lastActivity = this._lastPredictedActivity.get(entityId);
        if (lastActivity) {
          this._log(`Activity for ${entityId}: sticky(${lastActivity}) - speed null`);
          return lastActivity;
        }
        // Fallback to unknown if no previous predicted activity
        // IMPORTANT: We return 'unknown' here instead of falling back to sensor
        // because the user explicitly requested speed-based prediction.
        this._log(`Activity for ${entityId}: unknown - speed null, no sticky value`);
        return 'unknown';
      }
    }
    
    // Default to sensor activity if available
    const sensorActivity = data.activity ? data.activity.state : 'unknown';
    this._log(`Activity for ${entityId}: sensor(${sensorActivity})`);
    return sensorActivity;
  }

  /**
   * Prepares entity data for sending to iframe
   * @param {Object} config - Card configuration
   * @returns {Object} Formatted entity data
   */
  prepareEntityData(config) {
    if (!config) {
      console.warn('[EntityDataFetcher] prepareEntityData called without config. Activity prediction may be incorrect.');
    }

    const entityData = {};

    for (const [entityId, data] of Object.entries(this._entityCache)) {
      // Build picture URL
      let pictureUrl = data.person.attributes.entity_picture || '';
      if (pictureUrl && pictureUrl.startsWith('/')) {
        pictureUrl = window.location.origin + pictureUrl;
      }

      entityData[entityId] = {
        state: data.person.state,
        attributes: {
          latitude: data.person.attributes.latitude,
          longitude: data.person.attributes.longitude,
          friendly_name: data.person.attributes.friendly_name,
          entity_picture: pictureUrl
        },
        activity: this._getActivityToUse(data, entityId, config),
        speed: data.speed || null,
        predicted_activity: data.predicted_activity || null
      };
    }

    return entityData;
  }

  /**
   * Gets the entity cache
   * @returns {Object} Entity cache
   */
  getCache() {
    return this._entityCache;
  }

  /**
   * Checks if cache has data
   * @returns {boolean} True if cache has data
   */
  hasData() {
    return Object.keys(this._entityCache).length > 0;
  }

  /**
   * Gets the timestamp of the most recent cache update
   * @returns {number} Most recent timestamp
   */
  getLastUpdateTime() {
    if (!this.hasData()) return 0;
    return Math.max(...Object.values(this._entityCache).map(e => e.timestamp || 0));
  }

  /**
   * Calculates distance between two coordinates using Haversine formula
   * @param {number} lat1 - First latitude
   * @param {number} lon1 - First longitude
   * @param {number} lat2 - Second latitude
   * @param {number} lon2 - Second longitude
   * @returns {number} Distance in meters
   */
  _calculateHaversineDistance(lat1, lon1, lat2, lon2) {
    const R = 6371000; // Earth's radius in meters
    const φ1 = (lat1 * Math.PI) / 180;
    const φ2 = (lat2 * Math.PI) / 180;
    const Δφ = ((lat2 - lat1) * Math.PI) / 180;
    const Δλ = ((lon2 - lon1) * Math.PI) / 180;

    const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
              Math.cos(φ1) * Math.cos(φ2) *
              Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return R * c;
  }

  /**
   * Calculates average speed based on position history over available points
   * @param {string} entityId - Entity identifier
   * @param {Object} currentPosition - Current position data
   * @returns {Object|null} Speed data or null if insufficient history
   */
  calculateSpeed(entityId, currentPosition) {
    const history = this._positionHistory.get(entityId) || [];
    
    // Need at least 2 points to calculate speed
    if (history.length < 1) {
      return null;
    }

    let totalDistance = 0;
    let totalTime = 0;
    let pointsUsed = 0;
    
    // Use all available history points to average out GPS jitter
    // Start from the oldest point and work forward to current position
    const startIndex = 0;
    
    // Calculate cumulative distance and time across all history points
    for (let i = startIndex; i < history.length; i++) {
      const point1 = history[i];
      const point2 = i + 1 < history.length ? history[i + 1] : currentPosition;
      
      const distance = this._calculateHaversineDistance(
        point1.latitude,
        point1.longitude,
        point2.latitude,
        point2.longitude
      );
      
      const timeDiff = (point2.timestamp - point1.timestamp) / 1000; // seconds
      
      // Only include valid time intervals to avoid skewing the average
      if (timeDiff > 0) {
        totalDistance += distance;
        totalTime += timeDiff;
        pointsUsed++;
      }
    }
    
    // We need at least some valid time intervals
    if (totalTime < 5) { // Increased from 1 to 5 seconds to reduce GPS jitter
      return null;
    }

    const speedKmh = (totalDistance / 1000) / (totalTime / 3600); // km/h
    const speedMph = speedKmh * 0.621371; // mph

    return {
      speed_kmh: speedKmh,
      speed_mph: speedMph,
      time_diff: totalTime,
      distance_m: totalDistance
    };
  }

  /**
   * Predicts activity based on speed
   * @param {number} speedKmh - Speed in km/h
   * @returns {string|null} Predicted activity or null
   */
  predictActivity(speedKmh) {
    if (speedKmh === null || speedKmh === undefined) {
      return null;
    }

    if (speedKmh < ACTIVITY_THRESHOLDS.still) {
      return 'still';
    } else if (speedKmh < ACTIVITY_THRESHOLDS.walking) {
      return 'walking';
    } else {
      return 'in_vehicle';
    }
  }

  /**
   * Gets stable predicted activity with 3-second hysteresis
   * @param {string} entityId - Entity identifier
   * @param {number} speedKmh - Speed in km/h
   * @returns {string|null} Activity if stable, null if stabilizing
   */
  _getStablePredictedActivity(entityId, speedKmh) {
    const currentActivity = this.predictActivity(speedKmh);
    const candidate = this._candidateActivity.get(entityId);
    const now = Date.now();

    if (!candidate || candidate.activity !== currentActivity) {
      // New candidate activity, start tracking
      this._candidateActivity.set(entityId, { activity: currentActivity, timestamp: now });
      this._log(`Activity candidate for ${entityId}: ${currentActivity} (new)`);
      return this._lastPredictedActivity.get(entityId) || null;
    }

    // Same candidate, check if we've passed stability period
    const elapsed = now - candidate.timestamp;
    if (elapsed > this._activityStabilityMs) {
      // Activity is stable, update last predicted activity
      this._lastPredictedActivity.set(entityId, currentActivity);
      this._log(`Activity stable for ${entityId}: ${currentActivity} (elapsed: ${elapsed}ms)`);
      return currentActivity;
    }

    // Still stabilizing, return last stable activity
    this._log(`Activity stabilizing for ${entityId}: ${currentActivity} (elapsed: ${elapsed}ms)`);
    return this._lastPredictedActivity.get(entityId) || null;
  }

  /**
   * Updates position history for an entity (ring buffer of max 5 entries)
   * @param {string} entityId - Entity identifier
   * @param {Object} position - Current position data
   */
  _updatePositionHistory(entityId, position) {
    if (!this._positionHistory.has(entityId)) {
      this._positionHistory.set(entityId, []);
    }

    const history = this._positionHistory.get(entityId);
    history.push({
      latitude: position.latitude,
      longitude: position.longitude,
      timestamp: position.timestamp
    });

    // Keep only the last 5 entries (ring buffer)
    if (history.length > 5) {
      history.shift();
    }

    this._positionHistory.set(entityId, history);
  }
}
