<?php
// =============================================================================
// api/sensor_data.php - STM32 SHT30 보드로부터 온습도 측정 데이터를 받는 API
//
// [요청 형식] POST application/json
// {
//   "device_id":        "stm32-sht30-01",
//   "device_location":  "서버실",
//   "sensor_id":        2,
//   "sensor_name":      "2번 센서 (SHT30)",
//   "event_type":       "periodic" | "startup",
//   "timestamp":        1700000000,
//   "metric_type":      "sht30",
//   "temperature_c":    24.50,
//   "humidity_percent": 48.30,
//   "metric_status":    "normal",      // 펌웨어 plausibility 판정(참고용; 서버가 재판정)
//   "app_version":      "v2606-sht30"
// }
//
// [인증 방식 2가지]
//  1) 신규(STM32 펌웨어): HTTP 헤더 X-Signature = HMAC-SHA256(key=API_KEY, msg=raw_body)
//     - 본문 바이트 그대로 서명 → JSON 재직렬화/부동소수 포맷 호환 문제 없음
//     - timestamp 신선도(INGEST_MAX_CLOCK_SKEW_SEC)로 재전송(replay) 방어
//  2) 레거시: 본문 "signature" 필드 = sha256(API_KEY . ksort(json))  (하위호환, deprecated)
//
// [온습도 임계 경보] config.php 의 METRIC_* 임계로 서버가 고온/저온/고습/저습을
//   판정하고 종류별 쿨다운으로 SMS를 발송한다. 정상 복귀 시 1회 복귀 SMS를 보낸다.
//   (판정/발송 로직은 sms_send.php)
//
// [응답 형식] 200 OK application/json
// {"status":"ok","message":"저장 완료", ...}
// =============================================================================

// CORS 허용 (기기 → 서버 POST 전용, 브라우저 접근 불필요)
header('Access-Control-Allow-Origin: https://pores.kr');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Device-Id, X-Signature');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// POST만 허용
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['status' => 'error', 'message' => 'Method Not Allowed']);
    exit;
}

require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/sms_send.php';
require_once dirname(__DIR__) . '/cron_heartbeat.php';

// ── 운영 비밀값 강제: 기본 API_KEY 로는 수신을 거부(fail-closed) ──────────
// config.local.php 미설치/미설정 상태에서 공개된 기본 키로 위조 데이터가
// 수신되는 것을 차단한다. (security: 시작 시 비밀값 존재 검증)
if (api_key_is_default()) {
    error_log("[API] 거부: API_KEY 가 기본값입니다. config.local.php 에 운영 키를 설정하세요.");
    json_response(['status' => 'error', 'message' => '서버 구성 오류'], 503);
}

// 디바이스가 보낸 문자열에서 제어문자(개행 등)를 제거해 SMS/로그 라인 주입을 막는다.
// (서명은 raw 본문에 대해 이미 검증되었으므로 파싱 후 정제해도 안전하다.)
function api_sanitize_str(string $s): string {
    return preg_replace('/[[:cntrl:]]+/', ' ', $s);
}

function api_table_exists(PDO $db, string $table): bool {
    try {
        $stmt = $db->prepare("
            SELECT COUNT(*)
            FROM information_schema.TABLES
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = :table
        ");
        $stmt->execute([':table' => $table]);
        return (int)$stmt->fetchColumn() > 0;
    } catch (Throwable $e) {
        error_log("[API] table check failed: {$table} " . $e->getMessage());
        return false;
    }
}

// ── 요청 파싱 ─────────────────────────────────────────────
$raw = file_get_contents('php://input');

// 본문 크기 상한: 인증 전 대용량 본문을 json_decode/HMAC 에 태우는 증폭형 DoS 차단.
if (strlen($raw) > INGEST_MAX_BODY_BYTES) {
    error_log("[API] 본문 과대(" . strlen($raw) . "B > " . INGEST_MAX_BODY_BYTES . "B) from "
        . ($_SERVER['REMOTE_ADDR'] ?? '?'));
    json_response(['status' => 'error', 'message' => '본문이 너무 큽니다'], 413);
}

$data = json_decode($raw, true);

if (!$data) {
    json_response(['status' => 'error', 'message' => '잘못된 JSON 형식'], 400);
}

// ── 서명 검증 ─────────────────────────────────────────────
// X-Signature 헤더가 있으면 raw-body HMAC 서명(STM32 펌웨어)으로 검증한다.
// 레거시 본문 필드 서명(키-접두 sha256)은 기본 비활성(ALLOW_LEGACY_BODY_SIGNATURE).
// 운영 클라이언트(STM32)는 HMAC 만 쓰므로 약한 다운그레이드 경로를 닫아 둔다.
$has_raw_sig = isset($_SERVER['HTTP_X_SIGNATURE']) && $_SERVER['HTTP_X_SIGNATURE'] !== '';
if ($has_raw_sig) {
    $sig_ok   = verify_signature_raw($raw);
    $sig_mode = 'raw';
} elseif (ALLOW_LEGACY_BODY_SIGNATURE) {
    $sig_ok   = verify_signature($data);
    $sig_mode = 'legacy';
} else {
    $sig_ok   = false;          // X-Signature 헤더 필수 (레거시 비활성)
    $sig_mode = 'none';
}
if (!$sig_ok) {
    error_log("[API] 서명 불일치 from " . ($_SERVER['REMOTE_ADDR'] ?? '?') . " (mode={$sig_mode})");
    json_response(['status' => 'error', 'message' => '인증 실패'], 403);
}

// ── 필수 필드 검증 ────────────────────────────────────────
$required = ['device_id', 'sensor_id', 'event_type', 'timestamp'];
foreach ($required as $field) {
    if (!isset($data[$field])) {
        json_response(['status' => 'error', 'message' => "필드 누락: {$field}"], 400);
    }
}

// ── 값 추출 및 타입 변환 ──────────────────────────────────
$device_id       = api_sanitize_str(substr(trim($data['device_id']), 0, 64));
$device_location = api_sanitize_str(substr(trim($data['device_location'] ?? ''), 0, 100));
$sensor_id       = (int)$data['sensor_id'];
$sensor_name     = api_sanitize_str(substr(trim($data['sensor_name'] ?? "센서{$sensor_id}"), 0, 100));
$event_type      = in_array($data['event_type'], ['startup', 'periodic'], true)
                    ? $data['event_type'] : 'periodic';
$ts              = (int)$data['timestamp'];

// ── 재전송(replay) 방어: timestamp 신선도 검증 ────────────
// 캡처한 유효 서명 요청의 무한 재사용을 막는다. 펌웨어는 SNTP 동기 후 정확한
// Unix 시각을 싣는다(reporter.c). 허용 오차를 벗어나면 거부.
$now = time();
if ($ts <= 0 || abs($now - $ts) > INGEST_MAX_CLOCK_SKEW_SEC) {
    error_log("[API] timestamp 신선도 위반 (ts={$ts}, now={$now}, max=" . INGEST_MAX_CLOCK_SKEW_SEC . "s) from "
        . ($_SERVER['REMOTE_ADDR'] ?? '?'));
    json_response(['status' => 'error', 'message' => '요청 시각이 허용 범위를 벗어났습니다'], 400);
}

$sensor_dt       = date('Y-m-d H:i:s', $ts);
$metric_type     = api_sanitize_str(substr(trim($data['metric_type'] ?? ''), 0, 30));
$temperature_c   = isset($data['temperature_c']) && is_numeric($data['temperature_c']) ? round((float)$data['temperature_c'], 2) : null;
$humidity_percent = isset($data['humidity_percent']) && is_numeric($data['humidity_percent']) ? round((float)$data['humidity_percent'], 2) : null;

// 센서 물리 범위를 벗어난 값은 결측(null)으로 처리(센서 오류 방어)
if ($temperature_c !== null && ($temperature_c < -40 || $temperature_c > 125)) {
    $temperature_c = null;
}
if ($humidity_percent !== null && ($humidity_percent < 0 || $humidity_percent > 100)) {
    $humidity_percent = null;
}

$has_metric_payload = ($metric_type !== '') && ($temperature_c !== null || $humidity_percent !== null);

// ── 서버측 임계 판정 (config.php METRIC_*) ─────────────────
// 펌웨어 metric_status 는 물리 plausibility 판정이라 운영 경보로는 신뢰하지 않고,
// 서버가 운영 임계로 다시 판정하여 sensor_metric.metric_status 에 저장한다.
$metric_eval = $has_metric_payload
    ? evaluate_metric_thresholds($temperature_c, $humidity_percent)
    : ['breaches' => [], 'status' => 'normal'];
$metric_status = $metric_eval['status'];

$metric_saved = false;

// ── DB 저장 ───────────────────────────────────────────────
// 주기(periodic) 측정 보고는 sensor_log 에 남기지 않고 sensor_metric/상태만 갱신
// (대시보드 로그 정리). startup 은 1회 sensor_log 기록.
$skip_log = ($event_type === 'periodic' && $has_metric_payload);
$log_id   = null;

try {
    $db = get_db();

    if (!$skip_log) {
        $stmt = $db->prepare("
            INSERT INTO sensor_log
                (device_id, device_location, sensor_id, sensor_name,
                 event_type, sensor_time, created_at)
            VALUES
                (:device_id, :device_location, :sensor_id, :sensor_name,
                 :event_type, :sensor_time, NOW())
        ");
        $stmt->execute([
            ':device_id'       => $device_id,
            ':device_location' => $device_location,
            ':sensor_id'       => $sensor_id,
            ':sensor_name'     => $sensor_name,
            ':event_type'      => $event_type,
            ':sensor_time'     => $sensor_dt,
        ]);

        $log_id = $db->lastInsertId();
    }

    // ── 오프라인 복구 감지: UPSERT 전에 offline_alerted 확인 ──
    $was_offline = false;
    $chk = $db->prepare("SELECT offline_alerted FROM sensor_status WHERE sensor_id = :sensor_id");
    $chk->execute([':sensor_id' => $sensor_id]);
    $offline_flag = $chk->fetchColumn();
    if ($offline_flag === '1' || $offline_flag === 1) {
        $was_offline = true;
    }

    // ── 최신 센서 상태 업서트 (항상 갱신, 보고 = 온라인) ──
    $stmt2 = $db->prepare("
        INSERT INTO sensor_status
            (sensor_id, device_id, sensor_name, device_location,
             last_event_type, last_seen, offline_alerted)
        VALUES
            (:sensor_id, :device_id, :sensor_name, :device_location,
             :event_type, NOW(), 0)
        ON DUPLICATE KEY UPDATE
            device_id        = VALUES(device_id),
            sensor_name      = VALUES(sensor_name),
            device_location  = VALUES(device_location),
            last_event_type  = VALUES(last_event_type),
            last_seen        = NOW(),
            offline_alerted  = 0
    ");
    $stmt2->execute([
        ':sensor_id'       => $sensor_id,
        ':device_id'       => $device_id,
        ':sensor_name'     => $sensor_name,
        ':device_location' => $device_location,
        ':event_type'      => $event_type,
    ]);

    if ($has_metric_payload && api_table_exists($db, 'sensor_metric')) {
        $metric_stmt = $db->prepare("
            INSERT INTO sensor_metric
                (device_id, device_location, sensor_id, sensor_name,
                 metric_type, temperature_c, humidity_percent, metric_status,
                 measured_at, created_at)
            VALUES
                (:device_id, :device_location, :sensor_id, :sensor_name,
                 :metric_type, :temperature_c, :humidity_percent, :metric_status,
                 :measured_at, NOW())
        ");
        $metric_stmt->execute([
            ':device_id' => $device_id,
            ':device_location' => $device_location,
            ':sensor_id' => $sensor_id,
            ':sensor_name' => $sensor_name,
            ':metric_type' => $metric_type,
            ':temperature_c' => $temperature_c,
            ':humidity_percent' => $humidity_percent,
            ':metric_status' => $metric_status,
            ':measured_at' => $sensor_dt,
        ]);
        $metric_saved = true;
    }

    // ── 오프라인→온라인 복구 SMS 발송 ─────────────────────
    if ($was_offline) {
        $online_info = [
            'sensor_id'       => $sensor_id,
            'sensor_name'     => $sensor_name,
            'device_id'       => $device_id,
            'device_location' => $device_location,
        ];
        send_device_online_sms($online_info);
        error_log("[API] 장비 온라인 복구 - 센서{$sensor_id}");
    }

} catch (PDOException $e) {
    error_log("[API] DB 오류: " . $e->getMessage());
    json_response(['status' => 'error', 'message' => 'DB 저장 실패'], 500);
}

// ── 온습도 임계 경보 / 정상 복귀 SMS ───────────────────────
$sms_sent = false;
$sensor_info = [
    'sensor_id'       => $sensor_id,
    'sensor_name'     => $sensor_name,
    'device_id'       => $device_id,
    'device_location' => $device_location,
];

if ($has_metric_payload) {
    if (!empty($metric_eval['breaches'])) {
        // 임계 초과: 종류별로 SMS 발송(각자 쿨다운 적용)
        foreach ($metric_eval['breaches'] as $kind) {
            $value = in_array($kind, ['high_temp', 'low_temp'], true)
                ? $temperature_c : $humidity_percent;
            if ($value !== null && send_metric_alert_sms($sensor_info, $kind, (float)$value)) {
                $sms_sent = true;
            }
        }
    } elseif (metric_was_alerting($db, $sensor_id) && metric_is_recovered($temperature_c, $humidity_percent)) {
        // 직전까지 경보 상태였고 이제 (히스테리시스 포함) 정상으로 복귀 → 복귀 SMS 1회
        $sms_sent = send_metric_recovery_sms(
            $sensor_info,
            (float)($temperature_c ?? 0.0),
            (float)($humidity_percent ?? 0.0)
        );
    }
}

// ── Heartbeat: 다른 센서 오프라인 여부 체크 (5분 쓰로틀) ──
check_offline_sensors_throttled();

// ── 응답 ──────────────────────────────────────────────────
json_response([
    'status'        => 'ok',
    'message'       => $skip_log ? '상태 업데이트 완료' : '저장 완료',
    'log_id'        => $log_id ? (int)$log_id : null,
    'metric_saved'  => $metric_saved,
    'metric_status' => $metric_status,
    'sms_sent'      => $sms_sent,
]);
