from collections import deque
from datetime import datetime
from threading import Lock

import requests
from flask import Flask, jsonify, render_template_string, request

app = Flask(__name__)

# =========================================================
# KONFIGURASI
# =========================================================
ESP32_IP = "192.168.1.15"  # Ganti sesuai IP ESP32
ESP32_URL = f"http://{ESP32_IP}"
REQUEST_TIMEOUT = 2
HISTORY_LIMIT = 300          # Menyimpan 300 titik data terakhir
DEFAULT_MIN_LEVEL = 20       # Pompa ON saat level <= batas minimum (mode AUTO)
DEFAULT_MAX_LEVEL = 90       # Pompa OFF saat level >= batas maksimum (mode AUTO)

history = deque(maxlen=HISTORY_LIMIT)
history_lock = Lock()
last_device_data = {
    "level": 0,
    "distance": 0,
    "pump": False,
    "auto": False,
    "connected": False,
    "error": "Belum ada komunikasi dengan ESP32",
}
thresholds = {
    "min": DEFAULT_MIN_LEVEL,
    "max": DEFAULT_MAX_LEVEL,
}


def esp32_get(endpoint, params=None):
    """Mengirim request ke ESP32 dan mengembalikan response JSON/text."""
    response = requests.get(
        f"{ESP32_URL}{endpoint}",
        params=params,
        timeout=REQUEST_TIMEOUT,
    )
    response.raise_for_status()
    return response


def normalize_data(raw):
    """Memastikan format dan tipe data dari ESP32 sesuai kebutuhan dashboard."""
    level = max(0, min(100, float(raw.get("level", 0))))
    distance = float(raw.get("distance", 0))
    pump_state = raw.get("pump", False)
    auto_state = raw.get("auto", False)

    if isinstance(pump_state, str):
        pump_state = pump_state.lower() in ("1", "true", "on", "yes")
    if isinstance(auto_state, str):
        auto_state = auto_state.lower() in ("1", "true", "auto", "yes")

    return {
        "level": round(level, 1),
        "distance": round(distance, 1),
        "pump": bool(pump_state),
        "auto": bool(auto_state),
        "connected": True,
        "error": None,
    }


# =========================================================
# HALAMAN DASHBOARD
# =========================================================
HTML = r"""
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Water Tank HMI</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
  --bg:#eef3f8; --panel:#fff; --ink:#172033; --muted:#64748b;
  --blue:#1677d2; --navy:#0b3155; --green:#16a34a;
  --red:#dc2626; --amber:#d97706; --line:#e5eaf0;
}
* { box-sizing:border-box; }
body {
  margin:0; font-family:Inter,Segoe UI,Arial,sans-serif;
  background:var(--bg); color:var(--ink);
}
header {
  background:linear-gradient(120deg,#082f49,#075985,#0e7490);
  color:white; padding:22px 28px;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  box-shadow:0 5px 18px #0b315522;
}
header h1 { margin:0; font-size:clamp(20px,3vw,29px); letter-spacing:.4px; }
header p { margin:6px 0 0; color:#dbeafe; font-size:13px; }
.connection {
  border:1px solid #ffffff55; border-radius:30px; padding:9px 14px;
  font-size:13px; font-weight:700; white-space:nowrap;
}
.dot { display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:7px; background:#f87171; }
.connection.online .dot { background:#4ade80; }
main { max-width:1250px; margin:auto; padding:24px; }
.grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:20px; }
.card {
  background:var(--panel); border:1px solid var(--line); border-radius:18px;
  padding:22px; box-shadow:0 5px 18px #0f172a0a;
}
.card h2 { margin:0 0 18px; font-size:18px; }
.tank-wrap { display:flex; justify-content:center; align-items:center; gap:25px; flex-wrap:wrap; }
.tank {
  width:190px; height:330px; position:relative; overflow:hidden;
  border:7px solid #334155; border-radius:18px 18px 25px 25px;
  background:linear-gradient(90deg,#e2e8f0,#f8fafc,#e2e8f0);
  box-shadow:inset 0 0 15px #64748b33;
}
.water {
  position:absolute; bottom:0; left:0; width:100%; height:0%;
  background:linear-gradient(180deg,#38bdf8,#0284c7);
  transition:height .6s ease;
}
.water:before {
  content:""; position:absolute; top:-7px; left:-10%; width:120%; height:14px;
  border-radius:50%; background:#7dd3fc; opacity:.8;
}
.level-overlay {
  position:absolute; inset:0; display:flex; justify-content:center; align-items:center;
  z-index:2; font-size:34px; font-weight:800; color:#082f49;
  text-shadow:0 1px 3px white;
}
.metrics { min-width:190px; flex:1; max-width:300px; }
.metric {
  display:flex; justify-content:space-between; gap:10px; padding:13px 0;
  border-bottom:1px solid var(--line); color:var(--muted); font-size:14px;
}
.metric strong { color:var(--ink); font-size:17px; }
.pump-state {
  padding:15px; text-align:center; border-radius:12px; margin:17px 0;
  font-weight:800; letter-spacing:.5px;
}
.pump-on { background:#dcfce7; color:#166534; }
.pump-off { background:#fee2e2; color:#991b1b; }
.tank-status { font-weight:700; text-align:center; margin-top:16px; color:var(--muted); }
.button-row { display:flex; gap:10px; margin:10px 0 18px; }
button {
  border:0; border-radius:10px; padding:13px 15px; color:white;
  font-size:14px; font-weight:750; cursor:pointer; flex:1;
  transition:filter .15s,transform .15s;
}
button:hover { filter:brightness(.94); transform:translateY(-1px); }
button:disabled { opacity:.45; cursor:not-allowed; transform:none; }
.btn-auto { background:#2563eb; } .btn-manual { background:#64748b; }
.btn-start { background:#16a34a; } .btn-stop { background:#dc2626; }
.btn-save { background:#0f766e; }
button.selected { outline:3px solid #93c5fd; outline-offset:2px; }
.thresholds { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.thresholds label { font-size:13px; color:var(--muted); }
.thresholds input {
  display:block; width:100%; margin-top:6px; padding:11px;
  border:1px solid #cbd5e1; border-radius:9px; font-size:16px;
}
.note { color:var(--muted); font-size:12px; line-height:1.5; margin-top:12px; }
.chart-box { height:280px; position:relative; }
.footer { text-align:center; color:var(--muted); padding:20px; font-size:12px; }
.toast { min-height:20px; color:var(--muted); font-size:13px; margin-top:8px; }
.full { grid-column:1/-1; }
@media(max-width:800px) {
  main { padding:14px; } .grid { grid-template-columns:1fr; }
  header { padding:18px; align-items:flex-start; flex-direction:column; }
  .full { grid-column:auto; }
}
</style>
</head>
<body>
<header>
  <div>
    <h1>WATER TANK CONTROL HMI</h1>
    <p>Monitoring level air dan kendali pompa berbasis ESP32</p>
  </div>
  <div id="connection" class="connection"><span class="dot"></span><span id="connectionText">MENGHUBUNGKAN...</span></div>
</header>

<main>
<div class="grid">
  <section class="card">
    <h2>Monitoring Tangki</h2>
    <div class="tank-wrap">
      <div class="tank">
        <div id="water" class="water"></div>
        <div id="levelText" class="level-overlay">--%</div>
      </div>
      <div class="metrics">
        <div class="metric"><span>Level air</span><strong><span id="level">--</span>%</strong></div>
        <div class="metric"><span>Jarak sensor</span><strong><span id="distance">--</span> cm</strong></div>
        <div class="metric"><span>Mode operasi</span><strong id="mode">--</strong></div>
        <div class="metric"><span>Koneksi</span><strong id="deviceState">--</strong></div>
      </div>
    </div>
    <div id="tankStatus" class="tank-status">Menunggu data sensor...</div>
  </section>

  <section class="card">
    <h2>Kontrol Sistem</h2>
    <div id="pumpStatus" class="pump-state pump-off">POMPA --</div>
    <h3>Mode Operasi</h3>
    <div class="button-row">
      <button id="autoBtn" class="btn-auto" onclick="setMode('auto')">AUTO</button>
      <button id="manualBtn" class="btn-manual" onclick="setMode('manual')">MANUAL</button>
    </div>
    <h3>Kontrol Pompa Manual</h3>
    <div class="button-row">
      <button id="startBtn" class="btn-start" onclick="controlPump('on')">START</button>
      <button id="stopBtn" class="btn-stop" onclick="controlPump('off')">STOP</button>
    </div>
    <p class="note">Kontrol START/STOP hanya aktif saat mode MANUAL. Pada mode AUTO, keputusan pompa harus dijalankan oleh program ESP32 berdasarkan batas level.</p>
    <div id="message" class="toast"></div>
  </section>

  <section class="card">
    <h2>Pengaturan Batas Level Otomatis</h2>
    <div class="thresholds">
      <label>Level minimum (%)<input id="minLevel" type="number" min="0" max="99" value="20"></label>
      <label>Level maksimum (%)<input id="maxLevel" type="number" min="1" max="100" value="90"></label>
    </div>
    <div class="button-row">
      <button class="btn-save" onclick="saveThresholds()">SIMPAN BATAS LEVEL</button>
    </div>
    <p class="note">Logika yang disarankan: pompa ON ketika level mencapai atau di bawah batas minimum, lalu OFF ketika level mencapai atau di atas batas maksimum. ESP32 perlu mendukung endpoint <code>/thresholds?min=20&amp;max=90</code>.</p>
  </section>

  <section class="card">
    <h2>Status Sistem</h2>
    <div class="metric"><span>Status tangki</span><strong id="tankCondition">--</strong></div>
    <div class="metric"><span>Status pompa</span><strong id="pumpCondition">--</strong></div>
    <div class="metric"><span>IP ESP32</span><strong>{{ esp32_ip }}</strong></div>
    <div class="metric"><span>Pembaruan terakhir</span><strong id="lastUpdate">--</strong></div>
    <p class="note">Jika koneksi terputus, periksa Wi-Fi, alamat IP ESP32, dan endpoint HTTP pada program ESP32.</p>
  </section>

  <section class="card full">
    <h2>Grafik Riwayat Level Air</h2>
    <div class="chart-box"><canvas id="levelChart"></canvas></div>
    <p class="note">Grafik menampilkan maksimal 300 sampel terakhir selama server Flask berjalan. Data riwayat ini disimpan sementara di memori komputer.</p>
  </section>
</div>
</main>
<div class="footer">WATER TANK HMI · Flask + ESP32</div>

<script>
let currentAuto = false;
let chart;

function showMessage(text, isError=false) {
  const el = document.getElementById('message');
  el.textContent = text;
  el.style.color = isError ? '#b91c1c' : '#0f766e';
}

function initChart() {
  const ctx = document.getElementById('levelChart').getContext('2d');
  chart = new Chart(ctx, {
    type: 'line',
    data: {
      labels: [],
      datasets: [{
        label: 'Level air (%)', data: [], borderColor: '#0284c7',
        backgroundColor: 'rgba(2,132,199,.12)', fill: true,
        tension: .25, pointRadius: 1.5, borderWidth: 2
      }]
    },
    options: {
      responsive: true, maintainAspectRatio: false, animation: false,
      scales: {
        y: { min: 0, max: 100, title: { display: true, text: 'Level (%)' } },
        x: { ticks: { maxTicksLimit: 8 } }
      },
      plugins: { legend: { display: true } }
    }
  });
}

async function updateData() {
  try {
    const response = await fetch('/data', {cache:'no-store'});
    const data = await response.json();

    const connected = response.ok && data.connected;
    document.getElementById('connection').classList.toggle('online', connected);
    document.getElementById('connectionText').textContent = connected ? 'ESP32 TERHUBUNG' : 'ESP32 TERPUTUS';
    document.getElementById('deviceState').textContent = connected ? 'ONLINE' : 'OFFLINE';

    if (!connected) {
      showMessage(data.error || 'ESP32 tidak dapat dihubungi.', true);
      return;
    }

    const level = Number(data.level);
    document.getElementById('level').textContent = level.toFixed(1);
    document.getElementById('levelText').textContent = level.toFixed(0) + '%';
    document.getElementById('distance').textContent = Number(data.distance).toFixed(1);
    document.getElementById('water').style.height = Math.max(0, Math.min(100, level)) + '%';

    currentAuto = Boolean(data.auto);
    document.getElementById('mode').textContent = currentAuto ? 'AUTO' : 'MANUAL';
    document.getElementById('autoBtn').classList.toggle('selected', currentAuto);
    document.getElementById('manualBtn').classList.toggle('selected', !currentAuto);
    document.getElementById('startBtn').disabled = currentAuto;
    document.getElementById('stopBtn').disabled = currentAuto;

    const pumpEl = document.getElementById('pumpStatus');
    pumpEl.textContent = data.pump ? 'POMPA ON' : 'POMPA OFF';
    pumpEl.className = 'pump-state ' + (data.pump ? 'pump-on' : 'pump-off');
    document.getElementById('pumpCondition').textContent = data.pump ? 'MENYALA' : 'MATI';

    let condition = 'NORMAL';
    if (level <= Number(document.getElementById('minLevel').value)) condition = 'LEVEL RENDAH';
    if (level >= Number(document.getElementById('maxLevel').value)) condition = 'TANGKI PENUH';
    document.getElementById('tankStatus').textContent = condition;
    document.getElementById('tankCondition').textContent = condition;
    document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('id-ID');

    await updateHistory();
  } catch (error) {
    document.getElementById('connection').classList.remove('online');
    document.getElementById('connectionText').textContent = 'ESP32 TERPUTUS';
    document.getElementById('deviceState').textContent = 'OFFLINE';
    showMessage('Gagal mengambil data: ' + error.message, true);
  }
}

async function updateHistory() {
  try {
    const response = await fetch('/history', {cache:'no-store'});
    const result = await response.json();
    if (!chart) return;
    chart.data.labels = result.labels;
    chart.data.datasets[0].data = result.levels;
    chart.update('none');
  } catch (e) {
    console.warn('Riwayat grafik tidak dapat diperbarui:', e);
  }
}

async function setMode(mode) {
  try {
    const response = await fetch('/mode?value=' + encodeURIComponent(mode));
    const result = await response.json();
    if (!response.ok || !result.ok) throw new Error(result.error || 'Perintah mode gagal');
    showMessage('Perintah mode ' + mode.toUpperCase() + ' berhasil dikirim.');
    await updateData();
  } catch (e) { showMessage(e.message, true); }
}

async function controlPump(state) {
  if (currentAuto) {
    showMessage('Ubah ke mode MANUAL sebelum mengontrol pompa.', true);
    return;
  }
  try {
    const response = await fetch('/pump?state=' + encodeURIComponent(state));
    const result = await response.json();
    if (!response.ok || !result.ok) throw new Error(result.error || 'Perintah pompa gagal');
    showMessage('Perintah pompa ' + state.toUpperCase() + ' berhasil dikirim.');
    await updateData();
  } catch (e) { showMessage(e.message, true); }
}

async function saveThresholds() {
  const min = Number(document.getElementById('minLevel').value);
  const max = Number(document.getElementById('maxLevel').value);
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max > 100 || min >= max) {
    showMessage('Batas harus bilangan bulat 0–100 dan minimum harus lebih kecil dari maksimum.', true);
    return;
  }
  try {
    const response = await fetch(`/thresholds?min=${min}&max=${max}`);
    const result = await response.json();
    if (!response.ok || !result.ok) throw new Error(result.error || 'Gagal menyimpan batas. Pastikan firmware ESP32 mendukung /thresholds.');
    showMessage(`Batas otomatis tersimpan: ON ≤ ${min}% dan OFF ≥ ${max}%.`);
  } catch (e) { showMessage(e.message, true); }
}

initChart();
updateData();
setInterval(updateData, 2000);
</script>
</body>
</html>
"""


# =========================================================
# ROUTE FLASK
# =========================================================
@app.route("/")
def index():
    return render_template_string(HTML, esp32_ip=ESP32_IP)


@app.route("/data")
def data():
    global last_device_data
    try:
        response = esp32_get("/data")
        raw = response.json()
        normalized = normalize_data(raw)
        last_device_data = normalized

        with history_lock:
            history.append({
                "time": datetime.now().strftime("%H:%M:%S"),
                "level": normalized["level"],
            })

        return jsonify(normalized)
    except (requests.RequestException, ValueError, TypeError) as exc:
        last_device_data = {
            **last_device_data,
            "connected": False,
            "error": f"Tidak dapat membaca ESP32: {exc}",
        }
        return jsonify(last_device_data), 503


@app.route("/history")
def get_history():
    with history_lock:
        samples = list(history)
    return jsonify({
        "labels": [item["time"] for item in samples],
        "levels": [item["level"] for item in samples],
    })


@app.route("/pump")
def pump():
    state = request.args.get("state", "").lower()
    if state not in ("on", "off"):
        return jsonify({"ok": False, "error": "state harus on atau off"}), 400

    # Pengaman: pompa manual hanya boleh dikendalikan pada mode MANUAL.
    if last_device_data.get("auto"):
        return jsonify({"ok": False, "error": "Ubah ESP32 ke mode MANUAL terlebih dahulu."}), 409

    try:
        response = esp32_get("/pump", {"state": state})
        return jsonify({"ok": True, "esp32_response": response.text})
    except requests.RequestException as exc:
        return jsonify({"ok": False, "error": str(exc)}), 502


@app.route("/mode")
def mode():
    value = request.args.get("value", "").lower()
    if value not in ("auto", "manual"):
        return jsonify({"ok": False, "error": "value harus auto atau manual"}), 400

    try:
        response = esp32_get("/mode", {"value": value})
        return jsonify({"ok": True, "esp32_response": response.text})
    except requests.RequestException as exc:
        return jsonify({"ok": False, "error": str(exc)}), 502


@app.route("/thresholds")
def set_thresholds():
    try:
        minimum = int(request.args.get("min", ""))
        maximum = int(request.args.get("max", ""))
    except ValueError:
        return jsonify({"ok": False, "error": "min dan max harus berupa bilangan bulat."}), 400

    if not (0 <= minimum < maximum <= 100):
        return jsonify({"ok": False, "error": "Gunakan 0 <= min < max <= 100."}), 400

    try:
        # Firmware ESP32 harus menyediakan endpoint ini dan menjalankan
        # logika AUTO secara lokal pada ESP32.
        response = esp32_get("/thresholds", {"min": minimum, "max": maximum})
        thresholds["min"] = minimum
        thresholds["max"] = maximum
        return jsonify({
            "ok": True,
            "min": minimum,
            "max": maximum,
            "esp32_response": response.text,
        })
    except requests.RequestException as exc:
        return jsonify({
            "ok": False,
            "error": (
                "ESP32 belum menerima pengaturan batas level. "
                "Tambahkan endpoint /thresholds pada firmware ESP32. "
                f"Detail: {exc}"
            ),
        }), 502


@app.route("/health")
def health():
    return jsonify({
        "flask": "online",
        "esp32_connected": bool(last_device_data.get("connected")),
        "esp32_ip": ESP32_IP,
    })


if __name__ == "__main__":
    print("=" * 55)
    print(" WATER TANK CONTROL HMI")
    print("=" * 55)
    print("Dashboard: http://127.0.0.1:5000")
    print("Akses LAN: http://IP-KOMPUTER:5000")
    print(f"ESP32:     {ESP32_URL}")
    print("Tekan CTRL+C untuk menghentikan server.")
    app.run(host="0.0.0.0", port=5000, debug=False)
