#!/usr/bin/env python3
import os
import tempfile
import threading
from flask import Flask, jsonify, request, abort, render_template_string

app = Flask(__name__)

# --- Configuration ---
VALUE_FILE = os.environ.get("VALUE_FILE", "value.txt")
POLL_INTERVAL_MS = int(os.environ.get("POLL_INTERVAL_MS", "20000"))  # client refresh rate
_write_lock = threading.Lock()


# --- Helpers ---
def _clamp_to_range(val: int) -> int:
    return max(0, min(99, int(val)))


def _ensure_file_exists():
    """Create the value file with default '0' if missing or invalid."""
    if not os.path.exists(VALUE_FILE):
        _atomic_write("0")
        return
    try:
        _ = _read_value()
    except Exception:
        _atomic_write("0")


def _read_value() -> int:
    """Read the current numeric value from disk."""
    with open(VALUE_FILE, "r", encoding="utf-8") as f:
        raw = f.read().strip()
    # Allow blank/new files to recover to 0
    if raw == "":
        return 0
    n = int(raw)
    return _clamp_to_range(n)


def _atomic_write(text: str):
    """Atomically write text to VALUE_FILE."""
    dir_name = os.path.dirname(os.path.abspath(VALUE_FILE)) or "."
    os.makedirs(dir_name, exist_ok=True)
    fd, tmp_path = tempfile.mkstemp(prefix=".value-", dir=dir_name, text=True)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as tmp:
            tmp.write(text)
            tmp.flush()
            os.fsync(tmp.fileno())
        os.replace(tmp_path, VALUE_FILE)  # atomic on POSIX/Windows
    finally:
        # If replace succeeded, tmp_path no longer exists; ignore errors.
        try:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)
        except Exception:
            pass


def _get_mtime() -> float:
    try:
        return os.path.getmtime(VALUE_FILE)
    except FileNotFoundError:
        return 0.0


# --- Routes ---
@app.route("/", methods=["GET"])
def index():
    _ensure_file_exists()
    val = _read_value()
    mtime = _get_mtime()

    # Single-file template for convenience
    html = """
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Tiny Value Server</title>
  <style>
    :root { --accent: #2b7cff; --bg:#0b0d12; --card:#121722; --text:#e8eefc; --muted:#9db0d7; }
    body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color:var(--text); background:linear-gradient(180deg, #0b0d12 0%, #0e1320 100%); }
    .wrap { max-width: 880px; margin: 40px auto; padding: 24px; background: var(--card); border-radius: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.35); }
    h1 { margin: 0 0 8px; font-size: 1.5rem; }
    .sub { color: var(--muted); margin-bottom: 24px; }
    .value-display { font-size: clamp(48px, 9vw, 96px); font-weight: 800; text-align:center; letter-spacing: 2px; margin: 14px 0 6px; }
    .mtime { text-align:center; color: var(--muted); font-size: 0.9rem; margin-bottom: 18px; }
    .buttons { display:grid; grid-template-columns: repeat(4, minmax(100px,1fr)); gap:12px; margin: 12px 0 22px;}
    button.big {
      font-size: 24px; font-weight: 700; padding: 18px 8px; border-radius: 12px; border:1px solid #2a3245; background:#172033; color:var(--text);
      cursor:pointer; transition: transform 0.06s ease, background 0.2s ease, box-shadow 0.2s ease;
      box-shadow: inset 0 0 0 1px #2a3245, 0 8px 20px rgba(0,0,0,0.35);
    }
    button.big:hover { transform: translateY(-1px); background:#1a2742; }
    button.primary { background: linear-gradient(180deg, #2b7cff, #2a5bff); border:none; color:white; }
    .control-row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
    input[type="number"] {
      width: 100px; font-size: 18px; padding: 10px 12px; border-radius: 10px; border:1px solid #2a3245; background:#0d121c; color:var(--text);
    }
    input[type="range"] { width: min(100%, 520px); }
    .save { padding: 12px 16px; border-radius: 10px; border:0; background: var(--accent); color:white; font-weight:700; cursor:pointer; }
    .status { min-height: 1.4em; color: var(--muted); }
    .footer { margin-top: 24px; color: var(--muted); font-size: 0.9rem; }
    .grid { display:grid; grid-template-columns: 1fr; gap:18px; }
    @media (min-width: 720px) {
      .grid { grid-template-columns: 1.2fr 1fr; align-items: center; }
    }
  </style>
</head>
<body>
  <div class="wrap">
    <h1>Tiny Value Server</h1>
    <div class="sub">Stores a single integer 0–99 in <code>{{value_file}}</code>. Updates live if the file changes.</div>

    <div class="grid">
      <div>
        <div class="value-display" id="valueDisplay">{{value}}</div>
        <div class="mtime" id="mtime">last updated: {{mtime_human}}</div>

        <div class="buttons">
          <button class="big" data-value="0">0</button>
          <button class="big" data-value="7">7</button>
          <button class="big" data-value="33">33</button>
          <button class="big primary" data-value="99">99</button>
        </div>
      </div>

      <div>
        <label for="valueRange">Set a custom value (0–99)</label>
        <div class="control-row">
          <input id="valueNumber" type="number" inputmode="numeric" min="0" max="99" value="{{value}}">
          <input id="valueRange" type="range" min="0" max="99" value="{{value}}">
          <button id="saveBtn" class="save">Save</button>
        </div>
        <div id="status" class="status"></div>
        <div class="footer">Auto-refresh every {{poll_ms}} ms.</div>
      </div>
    </div>
  </div>

<script>
const POLL_MS = {{poll_ms}};
const valueDisplay = document.getElementById("valueDisplay");
const mtimeEl = document.getElementById("mtime");
const statusEl = document.getElementById("status");
const num = document.getElementById("valueNumber");
const rng = document.getElementById("valueRange");
const saveBtn = document.getElementById("saveBtn");

function clamp(n) {
  n = Number(n);
  if (!Number.isFinite(n)) return 0;
  return Math.max(0, Math.min(99, Math.trunc(n)));
}

function setStatus(msg, ok=true) {
  statusEl.textContent = msg || "";
  statusEl.style.color = ok ? "#9db0d7" : "#ff9aa2";
}

function syncInputs(v) {
  num.value = v;
  rng.value = v;
}

async function fetchValue() {
  try {
    const r = await fetch("/value", { cache: "no-store" });
    if (!r.ok) throw new Error("HTTP " + r.status);
    const data = await r.json();
    valueDisplay.textContent = data.value;
    mtimeEl.textContent = "last updated: " + data.mtime_human;
    // Only pull slider if server changed
    if (Number(num.value) !== data.value) {
      syncInputs(data.value);
    }
  } catch (e) {
    setStatus("Failed to refresh: " + e.message, false);
  }
}

async function postValue(v) {
  try {
    const r = await fetch("/set", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ value: v })
    });
    if (!r.ok) {
      const t = await r.text();
      throw new Error("HTTP " + r.status + " " + t);
    }
    const data = await r.json();
    setStatus("Saved.");
    valueDisplay.textContent = data.value;
    mtimeEl.textContent = "last updated: " + data.mtime_human;
    syncInputs(data.value);
  } catch (e) {
    setStatus("Save failed: " + e.message, false);
  }
}

document.querySelectorAll("button.big").forEach(btn => {
  btn.addEventListener("click", () => {
    const v = clamp(btn.dataset.value);
    syncInputs(v);
    postValue(v);
  });
});

num.addEventListener("input", () => {
  syncInputs(clamp(num.value));
});
rng.addEventListener("input", () => {
  syncInputs(clamp(rng.value));
});

saveBtn.addEventListener("click", () => {
  const v = clamp(num.value);
  syncInputs(v);
  postValue(v);
});

// initial and polling
fetchValue();
setInterval(fetchValue, POLL_MS);
</script>
</body>
</html>
    """
    return render_template_string(
        html,
        value_file=os.path.abspath(VALUE_FILE),
        value=val,
        mtime_human=_fmt_time(mtime),
        poll_ms=POLL_INTERVAL_MS,
    )


@app.route("/value", methods=["GET"])
def get_value():
    _ensure_file_exists()
    try:
        val = _read_value()
    except Exception:
        # If the file is corrupt or unreadable, reset to 0
        with _write_lock:
            _atomic_write("0")
        val = 0
    mtime = _get_mtime()
    return jsonify({"value": val, "mtime": mtime, "mtime_human": _fmt_time(mtime)})


@app.route("/set", methods=["POST"])
def set_value():
    if not request.is_json:
        abort(400, "Expected application/json body")
    body = request.get_json(silent=True) or {}
    if "value" not in body:
        abort(400, "Missing 'value'")
    try:
        new_val = _clamp_to_range(int(body["value"]))
    except Exception:
        abort(400, "Value must be an integer 0–99")

    with _write_lock:
        _atomic_write(str(new_val))

    mtime = _get_mtime()
    return jsonify({"ok": True, "value": new_val, "mtime": mtime, "mtime_human": _fmt_time(mtime)})


# --- Utilities ---
import time
def _fmt_time(mtime: float) -> str:
    if not mtime:
        return "never"
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))


# --- Entry point ---
if __name__ == "__main__":
    _ensure_file_exists()
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8087")), debug=True)
