#!/usr/bin/env python3
"""BME280 sensor logger.

Samples temperature/humidity/pressure every `sensors.interval_seconds` and
appends to hourly CSV segments (`sensors_%Y%m%dT%H%M%S.csv`, rotated
relative to this process's start time -- same convention as the camera
supervisor, so neither depends on wall-clock alignment). Every row is
fsync'd immediately: at a 10s cadence the cost is negligible, and it buys
near-zero sensor data loss even on the sudden power-off that ends every
mission (unlike video, which is NOT fsync'd per-write -- see
capture_supervisor.py; Matroska's incremental cluster writes are the
safety net there instead).
"""

from __future__ import annotations

import csv
import os
import signal
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bubblecam import config, state_lib  # noqa: E402
from bubblecam.hardware.bme280 import BME280Reader, SensorError  # noqa: E402
from bubblecam.hardware.ina219 import INA219Reader, PowerSensorError  # noqa: E402

CSV_HEADER = [
    "timestamp_utc",
    "temperature_c",
    "humidity_pct",
    "pressure_hpa",
    "lumen_brightness_pct",
    "ina219_bus_v",
    "ina219_current_a",
    "ina219_power_w",
]

# Consecutive failed reads before this is treated as a hard error (exit
# non-zero, let systemd restart -- which re-inits the I2C bus and can clear
# a transient wedge) rather than a one-off glitch worth just retrying.
FAILURE_LIMIT = 5

_shutdown_requested = False


def _handle_sigterm(signum, frame):
    global _shutdown_requested
    _shutdown_requested = True


def lumen_brightness_pct() -> str:
    """Current Lumen brightness, read from the lumen service's own state file.

    Returns "" (empty CSV cell) rather than 0 when the reading isn't
    trustworthy -- lumen service absent, crashed, or its heartbeat stale.
    An empty cell is honestly "unknown"; a 0 would falsely assert the lights
    were off, which matters when correlating plume imagery against lighting.
    """
    state = state_lib.read_state("lumen")
    if not state_lib.is_fresh(state):
        return ""
    value = state.get("brightness_pct")
    if not isinstance(value, (int, float)):
        return ""
    return f"{value:.1f}"


def ina219_fields(reader) -> list:
    """Three CSV cells for the INA219, empty when the reading isn't
    trustworthy (chip absent or a read fault) -- same "blank means unknown"
    convention as the lumen column: never log a fake 0V/0A.

    The INA219 is deliberately optional: it's power *diagnostics*, not
    mission data, so its absence must never stop BME280 logging.
    """
    if reader is None:
        return ["", "", ""]
    try:
        r = reader.read()
    except PowerSensorError:
        return ["", "", ""]
    return [r["bus_v"], r["current_a"], r["power_w"]]


def open_new_segment(sensor_dir: Path):
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
    path = sensor_dir / f"sensors_{timestamp}.csv"
    f = open(path, "w", newline="")
    writer = csv.writer(f)
    writer.writerow(CSV_HEADER)
    f.flush()
    os.fsync(f.fileno())
    return f, writer, path


def main() -> int:
    signal.signal(signal.SIGTERM, _handle_sigterm)

    cfg = config.load()
    sensors_cfg = cfg["sensors"]
    sensor_dir = Path(cfg["paths"]["sensor_dir"])
    sensor_dir.mkdir(parents=True, exist_ok=True)

    interval = sensors_cfg["interval_seconds"]
    segment_seconds = sensors_cfg["segment_seconds"]

    state_lib.write_state("sensors", "starting")

    try:
        reader = BME280Reader(i2c_bus=sensors_cfg["i2c_bus"], i2c_address=sensors_cfg["i2c_address"])
    except SensorError as exc:
        state_lib.write_state("sensors", "error", detail=str(exc))
        return 1

    try:
        power_reader = INA219Reader(
            i2c_bus=sensors_cfg["i2c_bus"],
            i2c_address=sensors_cfg["ina219_address"],
            shunt_ohms=sensors_cfg["ina219_shunt_ohms"],
        )
    except PowerSensorError:
        power_reader = None  # optional diagnostics -- log blanks, don't fail

    f, writer, path = open_new_segment(sensor_dir)
    segment_start = time.monotonic()
    consecutive_failures = 0

    try:
        while not _shutdown_requested:
            loop_start = time.monotonic()

            if time.monotonic() - segment_start >= segment_seconds:
                f.close()
                f, writer, path = open_new_segment(sensor_dir)
                segment_start = time.monotonic()

            try:
                reading = reader.read()
            except SensorError as exc:
                consecutive_failures += 1
                state_lib.write_state(
                    "sensors",
                    "starting" if consecutive_failures < FAILURE_LIMIT else "error",
                    detail=str(exc),
                    consecutive_failures=consecutive_failures,
                    segment=path.name,
                )
                if consecutive_failures >= FAILURE_LIMIT:
                    return 1
            else:
                consecutive_failures = 0
                timestamp = datetime.now(timezone.utc).isoformat()
                writer.writerow(
                    [
                        timestamp,
                        reading["temperature_c"],
                        reading["humidity_pct"],
                        reading["pressure_hpa"],
                        lumen_brightness_pct(),
                    ]
                    + ina219_fields(power_reader)
                )
                f.flush()
                os.fsync(f.fileno())
                state_lib.write_state("sensors", "running", segment=path.name, **reading)

            elapsed = time.monotonic() - loop_start
            time.sleep(max(0.0, interval - elapsed))
    finally:
        f.close()

    return 0


if __name__ == "__main__":
    sys.exit(main())
