<?php
// =============================================================================
// cron_heartbeat.php - RPi 오프라인 감지
//
// [사용법 1] 독립 실행 (cron 가능 시):
//   CLI: php cron_heartbeat.php
//   HTTP: cron_heartbeat.php?key=CRON_SECRET
//
// [사용법 2] Piggyback (cron 불가 시):
//   다른 PHP에서 require_once 후 check_offline_sensors_throttled() 호출
//   → 파일 타임스탬프 기반으로 5분에 1회만 실제 실행
// =============================================================================

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/sms_send.php';

/**
 * 오프라인 센서를 조회하여 SMS 발송 + offline_alerted 플래그 설정
 *
 * @return int 오프라인 감지된 센서 수
 */
function check_offline_sensors(): int {
    $db = get_db();

    $stmt = $db->prepare("
        SELECT * FROM sensor_status
        WHERE last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
          AND offline_alerted = 0
    ");
    $stmt->execute([':timeout' => HEARTBEAT_TIMEOUT_SEC]);
    $offline_sensors = $stmt->fetchAll();

    // 각 센서를 SMS 발송 전에 "원자적으로 선점"한다(0→1 전이가 한 프로세스에서만
    // 성공). 여러 진입점(sensor_data 쓰로틀, 로그인 시 즉시 체크, 대시보드)이
    // 동시에 실행돼도 오프라인 SMS 가 중복 발송되지 않도록 한다.
    // last_seen 조건을 함께 둬, SELECT 와 UPDATE 사이에 장비가 복귀 보고를 하면
    // (last_seen 갱신) 선점이 실패하여 오탐 오프라인 SMS 가 나가지 않도록 한다.
    $claim = $db->prepare("
        UPDATE sensor_status SET offline_alerted = 1
        WHERE sensor_id = :sensor_id
          AND offline_alerted = 0
          AND last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
    ");

    $count = 0;

    foreach ($offline_sensors as $sensor) {
        $claim->execute([
            ':sensor_id' => $sensor['sensor_id'],
            ':timeout'   => HEARTBEAT_TIMEOUT_SEC,
        ]);
        if ($claim->rowCount() === 0) {
            // 다른 동시 실행이 이미 선점했거나, 그 사이 장비가 복귀 보고를 했다.
            continue;
        }

        $sensor_info = [
            'sensor_id'       => $sensor['sensor_id'],
            'sensor_name'     => $sensor['sensor_name'],
            'device_id'       => $sensor['device_id'],
            'device_location' => $sensor['device_location'],
            'last_seen'       => $sensor['last_seen'],
        ];

        send_device_offline_sms($sensor_info);

        $count++;
        error_log("[Heartbeat] 오프라인 감지 - 센서{$sensor['sensor_id']} ({$sensor['sensor_name']})");
    }

    if ($count > 0) {
        error_log("[Heartbeat] " . date('Y-m-d H:i:s') . " - 오프라인 감지: {$count}건");
    }

    return $count;
}

/**
 * 쓰로틀 기반 오프라인 체크 (HEARTBEAT_CHECK_INTERVAL_SEC 간격으로 1회만 실행)
 * sensor_data.php, dashboard.php 등에서 호출
 */
function check_offline_sensors_throttled(): void {
    $lock_file = __DIR__ . '/var/heartbeat_last.txt';

    // 주기 판정을 원자적으로: 락을 쥔 채 마지막 실행 시각을 읽고, 주기가 도래했으면
    // 타임스탬프를 먼저 갱신한 뒤 락을 풀고 실행한다. 이렇게 하면 동시 호출(여러
    // 디바이스 POST / 대시보드 로드)이 몰려도 한 주기에 단 한 번만 스캔이 돈다.
    // (기존 비원자 read-then-write 는 동시 호출이 모두 stale 로 읽혀 중복 스캔됐다.)
    $fh = @fopen($lock_file, 'c+');
    if ($fh === false) {
        // 파일을 못 열면 가용성 우선: 잠금 없이 한 번 실행.
        try {
            check_offline_sensors();
        } catch (\Throwable $e) {
            error_log("[Heartbeat] 체크 실패: " . $e->getMessage());
        }
        return;
    }

    $should_run = false;
    if (flock($fh, LOCK_EX)) {
        $raw  = stream_get_contents($fh);
        $last = ($raw !== false && $raw !== '') ? (int)$raw : 0;
        if ($last === 0 || (time() - $last) >= HEARTBEAT_CHECK_INTERVAL_SEC) {
            ftruncate($fh, 0);
            rewind($fh);
            fwrite($fh, (string)time());
            fflush($fh);
            $should_run = true;
        }
        flock($fh, LOCK_UN);
    }
    fclose($fh);

    if ($should_run) {
        try {
            // 스캔(DB 작업)은 파일 락을 푼 뒤 실행해 락 보유 시간을 최소화한다.
            check_offline_sensors();
        } catch (\Throwable $e) {
            // heartbeat 실패가 호출자(dashboard, sensor_data)를 죽이면 안 됨
            error_log("[Heartbeat] 체크 실패: " . $e->getMessage());
        }
    }
}

// ── 독립 실행 시에만 아래 코드 동작 ──────────────────────────
// require_once로 불러올 때는 함수 정의만 로드됨
$_is_direct = (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__));

if ($_is_direct) {
    $is_cli = (php_sapi_name() === 'cli');

    if (!$is_cli) {
        $key = $_GET['key'] ?? '';
        if (!hash_equals(CRON_SECRET, $key)) {
            http_response_code(403);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'Forbidden';
            exit;
        }
    }

    $count = check_offline_sensors();

    if (!$is_cli) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode([
            'status'         => 'ok',
            'offline_count'  => $count,
            'checked_at'     => date('Y-m-d H:i:s'),
        ], JSON_UNESCAPED_UNICODE);
    }
}
