#include "AutomationManager.h"
#include "StepperManager.h"
#include "async_network.h"
#include "mqtt_manager.h"
#include <math.h>

AutomationManager automationManager;

AutomationManager::AutomationManager()
    : _lastUpdate(0), _lastNightLockRun(0), _lastSunsetRun(0), _lastHeatRun(0),
      _presenceEmptySince(0), _presenceMotionSince(0),
      _presenceEmptyTriggered(false), _presenceMotionTriggered(false),
      _morningActive(false), _morningAborted(false), _morningStartPos(0), _morningLastSent(-1), _morningEndTime(0),
      _hasLinkedDevice(false), _linkedMotion(false), _linkedStill(false), _linkedCpuTemp(0),
      _linkedMode(0), _linkedLightState(false), _lastTelemetryTime(0),
      _wasMoving(false), _suppressingSwitch(false), _suppressEndTime(0),
      _suppressionPendingMode(0), _suppressionRetryCount(0), _suppressionLastRetry(0),
      _suppressionAckPending(false), _emptyReadingCount(0) {}

void AutomationManager::begin() {
  Serial.println("[AutomationManager] Initialized");
}

bool AutomationManager::isLinkedDeviceOnline() const {
  if (!_hasLinkedDevice) return false;
  return (millis() - _lastTelemetryTime) < LINKED_DEVICE_ONLINE_TIMEOUT_MS;
}

void AutomationManager::updateLinkedDeviceTelemetry(bool motion, bool still, float cpuTemp, uint8_t switchMode, bool lightState) {
  // FIX #22: Protect shared telemetry state with spinlock.
  // This function is called from MQTT message handler (within PubSubClient::loop),
  // while update() reads these fields from the main loop. Guard against future
  // multi-core architectures where these could run concurrently.
  portENTER_CRITICAL(&_telemetryMux);
  _linkedMotion = motion;
  _linkedStill = still;
  _linkedCpuTemp = cpuTemp;
  _linkedMode = switchMode;
  _linkedLightState = lightState;
  _lastTelemetryTime = millis();
  _hasLinkedDevice = true;
  portEXIT_CRITICAL(&_telemetryMux);
}

bool AutomationManager::isTimeNow(uint8_t targetHour, uint8_t targetMinute, int offsetMinutes) {
  if (!network.hasTime()) return false;
  
  time_t now = time(nullptr);
  struct tm *timeinfo = localtime(&now);
  
  // Create a target time today
  struct tm target_tm = *timeinfo;
  target_tm.tm_hour = targetHour;
  target_tm.tm_min = targetMinute;
  target_tm.tm_sec = 0;
  target_tm.tm_isdst = -1;
  
  time_t target_time = mktime(&target_tm);
  target_time += (offsetMinutes * 60); // Apply offset
  
  // 60-second execution window
  double diff = difftime(now, target_time);
  
  // Normalize difference to handle cross-midnight wraps for offsets
  if (diff < -43200) diff += 86400; // if target was 'tomorrow' but actually meant today
  if (diff > 43200) diff -= 86400;  // if target was 'yesterday'
  
  return (diff >= 0 && diff < 60);
}

void AutomationManager::update() {
  unsigned long now = millis();
  // Evaluate rules every 1 second
  if (now - _lastUpdate < 1000) return;
  _lastUpdate = now;

  // Clear stale telemetry (> 3 hours old)
  // This allows the Presence automation to wait out long motion_timeout settings
  // without "forgetting" the linked device and aborting the close operation.
  if (_hasLinkedDevice && (now - _lastTelemetryTime > 10800000)) {
    _hasLinkedDevice = false;
    _linkedMotion = false;
    _linkedStill = false;
  }

  // --- Radar Suppression State Machine ---
  // (No network required — operates on local state only)
  bool isMovingNow = stepperManager.isMoving();
  if (isMovingNow && !_wasMoving) {
    // Started moving -> If paired switch is Auto and Light is Off, suppress it to Manual
    if (_hasLinkedDevice && _linkedMode == 0 && !_linkedLightState) {
        _suppressingSwitch = true;
        _suppressionAckPending = true;
        _suppressionPendingMode = 1; // MANUAL
        _suppressionRetryCount = 3;
        _suppressionLastRetry = now;
        mqttManager.publishLinkedDeviceMode(1); // Set to Manual
    }
  } else if (!isMovingNow && _wasMoving) {
    // Stopped moving -> Wait 45 seconds before restoring Auto
    if (_suppressingSwitch) {
        // FIX Bug #6: Use start-time + duration pattern instead of
        // _suppressEndTime = now + 45000 which overflows if millis() wraps.
        _suppressEndTime = now;  // Store WHEN movement stopped
    }
  }
  
  // Suppression ACK retry — if the linked switch didn't change mode, retry
  if (_suppressionAckPending && _suppressionRetryCount > 0) {
    if (_linkedMode == _suppressionPendingMode) {
      // ACK received — linked switch confirmed the mode change
      _suppressionAckPending = false;
      _suppressionRetryCount = 0;
    } else if (now - _suppressionLastRetry >= 2000) {
      // No ACK after 2 seconds — retry
      _suppressionRetryCount--;
      _suppressionLastRetry = now;
      Serial.printf("[Automation] Suppression retry (%d remaining) -> mode %d\n",
                    _suppressionRetryCount, _suppressionPendingMode);
      mqttManager.publishLinkedDeviceMode(_suppressionPendingMode);
      if (_suppressionRetryCount == 0) {
        _suppressionAckPending = false;
        Serial.println("[Automation] Suppression ACK failed after 3 retries");
      }
    }
  }
  
  // Check if it's time to restore the switch to Auto
  // FIX Bug #6: Overflow-safe elapsed-time check. (now - startTime) wraps
  // correctly for unsigned subtraction as long as the interval < 49 days.
  if (!isMovingNow && _suppressingSwitch && _suppressEndTime > 0 && (now - _suppressEndTime) >= 45000) {
      _suppressingSwitch = false;
      _suppressEndTime = 0;
      _suppressionAckPending = true;
      _suppressionPendingMode = 0; // AUTO
      _suppressionRetryCount = 3;
      _suppressionLastRetry = now;
      mqttManager.publishLinkedDeviceMode(0); // Set to Auto
  }
  _wasMoving = isMovingNow;
  // ----------------------------------------

  // --- Time-based automations: require synced time but NOT active WiFi ---
  // This ensures morning/night/sunset rules still fire during WiFi outages
  // as long as time was synced at least once.
  if (network.hasTime()) {
    processNightLock();
    processSunsetAutoClose();
    processMorningWakeUp();
  }

  // --- Sensor-based automations: no network required ---
  // Presence and heat protection use locally-cached telemetry from linked
  // device. They work fully offline once telemetry has been received.
  processPresence();
  processHeatProtection();
}

bool AutomationManager::isDaytime() {
  uint32_t sunrise = network.getSunriseTime();
  uint32_t sunset = network.getSunsetTime();
  if (sunrise == 0 || sunset == 0) return false;

  time_t now = time(nullptr);
  
  // IMPORTANT: localtime() returns a static pointer. Must copy before next call.
  time_t srEpoch = (time_t)sunrise;
  struct tm srTm = *localtime(&srEpoch);
  
  time_t ssEpoch = (time_t)sunset;
  struct tm ssTm = *localtime(&ssEpoch);
  
  struct tm nowTm = *localtime(&now);

  // Map sunrise/sunset hours to today
  struct tm targetSr = nowTm;
  targetSr.tm_hour = srTm.tm_hour;
  targetSr.tm_min = srTm.tm_min;
  targetSr.tm_sec = 0;
  targetSr.tm_isdst = -1;

  struct tm targetSs = nowTm;
  targetSs.tm_hour = ssTm.tm_hour;
  targetSs.tm_min = ssTm.tm_min;
  targetSs.tm_sec = 0;
  targetSs.tm_isdst = -1;

  time_t todaySunrise = mktime(&targetSr);
  time_t todaySunset = mktime(&targetSs);

  return (now >= todaySunrise && now < todaySunset);
}

void AutomationManager::processNightLock() {
  if (!config.night_enabled) return;
  
  // Day of week check
  if (network.hasTime()) {
    time_t nowSecs = time(nullptr);
    struct tm nowTm = *localtime(&nowSecs);
    if ((config.night_days & (1 << nowTm.tm_wday)) == 0) return;
  }
  
  unsigned long now = millis();
  if (isTimeNow(config.night_time_h, config.night_time_m)) {
    // 60-second cooldown internally due to isTimeNow, but add a 5 min safety cooldown
    if (now - _lastNightLockRun > 300000) {
      _lastNightLockRun = now;
      Serial.printf("[Automation] 🌙 Night Lock -> %d%%\n", config.night_target);
      stepperManager.setTargetPercentage(config.night_target, 0);  // publishes state internally
    }
  }
}

void AutomationManager::processSunsetAutoClose() {
  if (!config.sunset_enabled) return;
  
  uint32_t sunsetTime = network.getSunsetTime();
  if (sunsetTime == 0) return; // Weather not fetched yet

  time_t now = time(nullptr);
  
  // IMPORTANT: localtime() returns a pointer to a static buffer.
  // Must copy the result before the second call, or it gets overwritten.
  time_t sunsetEpoch = (time_t)sunsetTime;
  struct tm sunsetTmCopy = *localtime(&sunsetEpoch);
  struct tm nowTmCopy = *localtime(&now);
  
  // Map sunset time to TODAY (in case the cached fetch was yesterday)
  struct tm targetTm = nowTmCopy;
  targetTm.tm_hour = sunsetTmCopy.tm_hour;
  targetTm.tm_min = sunsetTmCopy.tm_min;
  targetTm.tm_sec = 0;
  targetTm.tm_isdst = -1;
  
  time_t adjustedSunset = mktime(&targetTm);
  
  // Apply configured offset (minutes)
  time_t targetTime = adjustedSunset + (config.sunset_offset * 60);
  
  // 60-second window
  double diff = difftime(now, targetTime);
  
  // Normalize difference to handle cross-midnight wraps for sunset offsets
  if (diff < -43200) diff += 86400;
  if (diff > 43200) diff -= 86400;
  
  if (diff >= 0 && diff < 60) {
    if (millis() - _lastSunsetRun > 300000) { // 5 min cooldown
      _lastSunsetRun = millis();
      Serial.printf("[Automation] 🌇 Sunset Auto-Close -> %d%%\n", config.sunset_target);
      stepperManager.setTargetPercentage(config.sunset_target, 0);  // publishes state internally
    }
  }
}

void AutomationManager::processMorningWakeUp() {
  if (!config.morning_enabled) {
    _morningActive = false;
    _morningAborted = false;
    return;
  }

  time_t nowSecs = time(nullptr);
  
  // Guard against evaluating timestamps before NTP synchronizes
  if (!network.hasTime()) return;
  
  struct tm nowTmCopy = *localtime(&nowSecs);
  int currentWday = nowTmCopy.tm_wday; // 0=Sun..6=Sat

  int activeDayIdx = -1;
  time_t activeStartTime = 0;
  time_t activeEndTime = 0;
  int32_t activeDuration = 0;
  int32_t activeTarget = 0;

  // Test [Yesterday, Today, Tomorrow] boundaries to capture schedules overlapping midnight backwards or forwards
  for (int offset = -1; offset <= 1; offset++) {
      int testDayIdx = (currentWday + offset + 7) % 7;
      
      if ((config.morning_days & (1 << testDayIdx)) == 0) continue;
      
      int32_t duration = config.morning_duration[testDayIdx];
      if (duration <= 0) continue;

      struct tm targetTm = nowTmCopy;
      targetTm.tm_mday += offset; // Let mktime handle month/year/DST wrapping
      targetTm.tm_hour = config.morning_time_h[testDayIdx];
      targetTm.tm_min = config.morning_time_m[testDayIdx];
      targetTm.tm_sec = 0;
      targetTm.tm_isdst = -1;
      
      // Calculate epoch for the TARGET (FINISH) time based on user configuration
      time_t testEndTime = mktime(&targetTm);
      
      // Start time is duration minutes BEFORE the finish time
      time_t testStartTime = testEndTime - (duration * 60);

      // If we are currently inside this exact window, lock on
      if (nowSecs >= testStartTime && nowSecs <= testEndTime + 60) {
          activeDayIdx = testDayIdx;
          activeStartTime = testStartTime;
          activeEndTime = testEndTime;
          activeDuration = duration;
          activeTarget = config.morning_target[testDayIdx];
          break; // Found the active schedule
      }
  }

  // If outside of ALL computed time windows, clean up state
  if (activeDayIdx == -1) {
    _morningActive = false;
    _morningAborted = false;  // Reset abort flag so it can run again
    return;
  }

  // If previously aborted (manual override during this window), stay aborted
  if (_morningAborted) return;

  // Inside time window — initialize if not already active
  if (!_morningActive) {
    int32_t currentPos = stepperManager.getCurrentPercentage();
    
    // User requirement: Prevent moving DOWN. Only proceed if target opens blinds further.
    if (activeTarget <= currentPos) {
      Serial.printf("[Automation] 🛑 Morning wake-up ABORTED: Target (%d%%) <= current (%d%%). Prevented moving DOWN.\n", activeTarget, currentPos);
      _morningActive = false;
      _morningAborted = true;
      return;
    }

    _morningActive = true;
    _morningStartPos = currentPos;
    _morningLastSent = _morningStartPos;
    _morningEndTime = activeEndTime;
    Serial.printf("[Automation] 🌅 Morning wake-up started. Start pos: %d%%, target: %d%%, duration: %d min\n",
                  _morningStartPos, activeTarget, activeDuration);
  }

  // Safety check: if target time changed mid-flight via MQTT config update, reset
  if (_morningEndTime != activeEndTime) {
    int32_t currentPos = stepperManager.getCurrentPercentage();
    if (activeTarget <= currentPos) {
      Serial.printf("[Automation] 🛑 Morning wake-up ABORTED mid-flight: Target (%d%%) <= current (%d%%).\n", activeTarget, currentPos);
      _morningActive = false;
      _morningAborted = true;
      return;
    }
    _morningStartPos = currentPos;
    _morningLastSent = _morningStartPos;
    _morningEndTime = activeEndTime;
  }

  float elapsedMins = difftime(nowSecs, activeStartTime) / 60.0f;

  // Quantize elapsed time into 1-minute chunks (floor) so we only move once per minute
  float quantizedMins = floor(elapsedMins);
  float progress = quantizedMins / (float)activeDuration;
  if (progress > 1.0f) progress = 1.0f;
  if (progress < 0.0f) progress = 0.0f;

  int32_t currentTarget = (int32_t)round(_morningStartPos + (activeTarget - _morningStartPos) * progress);

  // Enforce "ONLY GO UP" rule in case of floating point rounding edge cases
  if (currentTarget < _morningLastSent) {
    currentTarget = _morningLastSent;
  }

  // Check for manual override — compare the COMMANDED target position (not live position)
  // to what we last commanded. If someone used the app or button, getTargetPercentage()
  // will differ from what we last sent. Tolerance is 10% to prevent int rounding errors.
  if (_morningLastSent != _morningStartPos) { // Only check after we've sent at least one command
    int commandedPos = stepperManager.getTargetPercentage();
    if (abs(commandedPos - _morningLastSent) > 10) {
      Serial.printf("[Automation] 🛑 Morning wake-up ABORTED: commanded=%d%%, expected=%d%% (manual override detected)\n",
                    commandedPos, _morningLastSent);
      _morningActive = false;
      _morningAborted = true;  // Stay aborted until window ends
      return;
    }
  }

  // Send move command if the target has changed from what we last sent
  if (currentTarget != _morningLastSent) {
    _morningLastSent = currentTarget;
    Serial.printf("[Automation] 🌅 Morning Gradual Open -> %d%% (progress: %.0f%%)\n", currentTarget, progress * 100);
    stepperManager.setTargetPercentage(currentTarget, 1000);  // publishes state internally
  }

  // Completion: ensure the blind arrives at the exact target
  if (progress >= 1.0f && _morningActive) {
    if (_morningLastSent != activeTarget && activeTarget > _morningLastSent) {
      stepperManager.setTargetPercentage(activeTarget, 1000);
    }
    Serial.printf("[Automation] 🌅 Morning Gradual Open FINISHED -> %d%%\n", activeTarget);
    if (mqttManager.isConnected()) {
      mqttManager.publishState(true);

      // FEAT-4: Cross-device morning sync — exit bedtime mode on linked Zaylo.
      // When the blinds finish opening, tell the light switch to return to AUTO
      // so its motion sensor re-activates for the morning routine.
      // CRITICAL: Must use {"sleep": false} not {"mode": 0} — the Zaylo blocks
      // mode changes during bedtime to prevent accidental wake-ups.
      if (_hasLinkedDevice && isLinkedDeviceOnline() && _linkedMode == 3) {
        Serial.println("[Automation] 🌅 Exiting bedtime on linked switch (morning sync)");
        mqttManager.publishLinkedDeviceSleepExit();
      }
    }
    _morningActive = false;
    _morningAborted = true; // Stay locked out until the window fully elapses
  }
}

void AutomationManager::processPresence() {
  if (!config.presence_enabled || !_hasLinkedDevice) return;
  
  // If linked device is offline, don't auto-close (safe fallback)
  if (!isLinkedDeviceOnline()) return;
  
  unsigned long now = millis();
  
  // Time filter check
  if (config.presence_time_filter != 0) {
      bool day = isDaytime();
      if (config.presence_time_filter == 1 && !day) return; // day only
      if (config.presence_time_filter == 2 && day) return;  // night only
  }

  bool sensorEmpty = (!_linkedMotion && !_linkedStill);

  // --- Hysteresis: require PRESENCE_HYSTERESIS_COUNT consecutive empty readings ---
  bool isRoomEmpty;
  if (sensorEmpty) {
    _emptyReadingCount++;
    if (_emptyReadingCount >= PRESENCE_HYSTERESIS_COUNT) {
      isRoomEmpty = true;
      _emptyReadingCount = PRESENCE_HYSTERESIS_COUNT; // Clamp to prevent overflow
    } else {
      isRoomEmpty = false; // Not enough consecutive readings yet
    }
  } else {
    _emptyReadingCount = 0; // Reset on any presence
    isRoomEmpty = false;
  }

  if (isRoomEmpty) {
    _presenceMotionSince = 0; // Reset enter debounce timer
    _presenceMotionTriggered = false; // Reset motion trigger

    // Room is empty, start empty timer
    if (_presenceEmptySince == 0) {
      _presenceEmptySince = now;
      _presenceEmptyTriggered = false; // Resets on new empty session
    }

    if (!_presenceEmptyTriggered) {
      unsigned long elapsedEmpty = now - _presenceEmptySince;
      unsigned long timeoutMs = config.motion_timeout * 1000UL;

      if (elapsedEmpty >= timeoutMs) {
        _presenceEmptyTriggered = true; // Mark as fired to allow manual override
        
        // Don't spam if already at target
        if (stepperManager.getTargetPercentage() == config.presence_target) return;

        Serial.printf("[Automation] 🚶 Presence Auto Close -> %d%%\n", config.presence_target);
        stepperManager.setTargetPercentage(config.presence_target, 0);  // publishes state internally
      }
    }

  } else {
    // Motion Detected
    _presenceEmptySince = 0; // Reset empty timer
    _presenceEmptyTriggered = false; // Reset empty trigger

    if (config.presence_action == 1 && config.presence_open_target >= 0 && config.presence_open_target <= 100) {
        // Clamp to valid range for safety (in case config was corrupted)
        int32_t openTarget = constrain(config.presence_open_target, 0, 100);
        if (_presenceMotionSince == 0) {
            _presenceMotionSince = now;
            _presenceMotionTriggered = false;
        }
        
        if (!_presenceMotionTriggered) {
          // 2 second debounce before opening
          if (now - _presenceMotionSince >= 2000) {
              _presenceMotionTriggered = true; // Mark as fired
              
              if (stepperManager.getTargetPercentage() != openTarget) {
                  // Ensure blind isn't currently moving to avoid spamming
                  if (!stepperManager.isMoving()) {
                      Serial.printf("[Automation] \xF0\x9F\x9A\xB6 Presence Auto Open -> %d%%\n", openTarget);
                      stepperManager.setTargetPercentage(openTarget, 0);  // publishes state internally
                  }
              }
          }
        }
    }
  }
}

void AutomationManager::processHeatProtection() {
  if (!config.temp_enabled || !_hasLinkedDevice) return;

  unsigned long now = millis();
  
  if (_linkedCpuTemp >= config.temp_threshold) {
    // 15 min cooldown
    if (now - _lastHeatRun > 900000) {
      _lastHeatRun = now;
      if (stepperManager.getTargetPercentage() != config.temp_target) {
        Serial.printf("[Automation] 🌡️ Heat Protection -> %d%%\n", config.temp_target);
        stepperManager.setTargetPercentage(config.temp_target, 0);  // publishes state internally
      }
    }
  }
}
