#!/usr/bin/env python3
# Local server for realtime.html:
#   - serves the page + static files from this folder
#   - proxies POST /api/getData -> getVisualData.php with your session
#
# Two auth modes (auto-detected):
#   * LOGIN  mode  : if login.txt has real credentials, the proxy logs in on
#                    startup (fresh session -> first getData includes live
#                    ch99000) and RE-LOGS-IN automatically when the session
#                    expires. No more pasting cookies.
#   * COOKIE mode  : otherwise it uses the PHPSESSID from session.txt (manual).
import http.server, socketserver, urllib.request, urllib.error, urllib.parse
import http.cookiejar, os, threading, functools, json
print = functools.partial(print, flush=True)  # show status immediately in the console

PORT   = int(os.environ.get("PORT", "80"))
HOST   = os.environ.get("HOST", "0.0.0.0")
HERE   = os.path.dirname(os.path.abspath(__file__))
BASE   = "https://agregator.e-management.cz/lookdet"
LOGIN_GET  = BASE + "/common/login/"
LOGIN_POST = BASE + "/common/login/index.php"
VERIFY     = BASE + "/zalozky/"
TARGET     = BASE + "/common/visual/php/getVisualData.php"
UA         = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"

_lock   = threading.Lock()
_opener = None      # urllib opener w/ cookie jar (LOGIN mode holds the session here)
_mode   = "cookie"  # "login" | "cookie"

# optional: log every live poll into the history DB (best-effort, never fatal)
try:
    import db_ingest as _ing
    _db = _ing.connect()
    _labels = _ing._load_labels()
except Exception:
    _ing = None; _db = None; _labels = {}

def _read(name):
    p = os.path.join(HERE, name)
    try:
        return open(p, encoding="utf-8").read()
    except OSError:
        return ""

def session_cookie():
    return _read("session.txt").strip()

def credentials():
    """Return (user, password) from login.txt, or None if not filled in."""
    lines = [l.strip() for l in _read("login.txt").splitlines()
             if l.strip() and not l.strip().startswith("#")]
    if len(lines) >= 2 and lines[1] and not lines[1].startswith("<"):
        return lines[0], lines[1]
    return None

def post_body():
    return open(os.path.join(HERE, "body.txt"), "rb").read()

def _common(req):
    req.add_header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
    req.add_header("X-Requested-With", "XMLHttpRequest")
    req.add_header("Origin", "https://agregator.e-management.cz")
    req.add_header("Referer", VERIFY)
    req.add_header("User-Agent", UA)

def do_login():
    """Fresh login -> returns an opener carrying an authenticated session, or None."""
    c = credentials()
    if not c:
        return None
    user, pw = c
    cj = http.cookiejar.CookieJar()
    op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
    op.addheaders = [("User-Agent", UA)]
    try:
        op.open(LOGIN_GET, timeout=15).read()                      # sets PHPSESSID
        data = urllib.parse.urlencode(
            {"control": "login", "logLogin": user, "logPassword": pw}).encode()
        try:
            op.open(LOGIN_POST, data=data, timeout=15).read()
        except urllib.error.HTTPError as e:
            if e.code == 401:
                return None                                        # bad credentials
        if "login" in op.open(VERIFY, timeout=15).geturl():        # bounced back -> failed
            return None
        return op
    except Exception:
        return None

def _looks_json(b):
    return b[:1].lstrip() == b"{"

def fetch_getData():
    """Proxy one getData call, re-logging-in once if the session died (login mode)."""
    global _opener, _mode
    with _lock:
        body = post_body()
        def once():
            req = urllib.request.Request(TARGET, data=body, method="POST")
            _common(req)
            if _mode == "login" and _opener is not None:
                return _opener.open(req, timeout=15).read()
            req.add_header("Cookie", "PHPSESSID=" + session_cookie())
            return urllib.request.urlopen(req, timeout=15).read()
        try:
            data = once()
        except Exception as e:
            if _mode != "login":
                raise
            data = b""
        if _mode == "login" and not _looks_json(data):
            op = do_login()                                        # session expired -> refresh
            if op:
                _opener = op
                data = once()
        if _db is not None and _looks_json(data):
            try:
                _ing.ingest_json(_db, data.decode("utf-8", "replace"), "realtime", _labels)
            except Exception:
                pass
        return data

class Handler(http.server.SimpleHTTPRequestHandler):
    def _send(self, code, data, ctype="application/json; charset=utf-8"):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(data)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(data)

    def _json(self, obj):
        self._send(200, json.dumps(obj).encode("utf-8"))

    def do_GET(self):
        if self.path.startswith("/api/series"):
            return self.api_series()
        if self.path.startswith("/api/top"):
            return self.api_top()
        return super().do_GET()

    def api_series(self):
        import sqlite3
        from urllib.parse import urlparse, parse_qs
        q = parse_qs(urlparse(self.path).query)
        keys = [k for k in q.get("keys", [""])[0].split(",") if k][:40]
        maxp = min(int(q.get("max", ["800"])[0]), 5000)
        out = {}
        try:
            con = sqlite3.connect(_ing.DB)
            for k in keys:
                row = con.execute("SELECT point_id,name,unit,mult FROM points WHERE key=?", (k,)).fetchone()
                if not row:
                    out[k] = {"name": k, "unit": "", "points": []}; continue
                pid, name, unit, mult = row
                cnt = con.execute("SELECT COUNT(*) FROM readings WHERE point_id=?", (pid,)).fetchone()[0]
                step = max(1, cnt // maxp)   # even downsample across the whole range
                rows = con.execute(
                    """SELECT ts_epoch, value FROM (
                         SELECT CAST(strftime('%s', ts) AS INTEGER) AS ts_epoch,
                                raw * COALESCE(?, 1) AS value,
                                ROW_NUMBER() OVER (ORDER BY ts) AS rn
                         FROM readings WHERE point_id = ?
                       ) WHERE rn % ? = 0 ORDER BY ts_epoch""",
                    (mult, pid, step)).fetchall()
                out[k] = {"name": name or k, "unit": unit or "", "points": rows}
            con.close()
        except Exception as e:
            return self._json({"error": str(e)})
        self._json(out)

    def api_top(self):
        import sqlite3
        from urllib.parse import urlparse, parse_qs
        q = parse_qs(urlparse(self.path).query)
        n = min(int(q.get("n", ["8"])[0]), 40)
        try:
            con = sqlite3.connect(_ing.DB)
            rows = con.execute(
                """SELECT p.key, p.name, p.unit, COUNT(DISTINCT r.raw) AS variety
                   FROM readings r JOIN points p USING(point_id)
                   WHERE p.name IS NOT NULL AND p.name<>'' AND p.typ='analog'
                   GROUP BY p.point_id HAVING COUNT(*) > 2
                   ORDER BY variety DESC LIMIT ?""", (n,)).fetchall()
            con.close()
        except Exception as e:
            return self._json({"error": str(e)})
        self._json([{"key": r[0], "name": r[1], "unit": r[2]} for r in rows])

    def do_POST(self):
        if not self.path.startswith("/api/getData"):
            self._send(404, b'{"error":"unknown endpoint"}'); return
        if _mode == "cookie" and not session_cookie():
            self._send(200, b'{"error":"no session (fill login.txt or session.txt)"}'); return
        try:
            self._send(200, fetch_getData())
        except Exception as e:
            self._send(200, ('{"error":%r}' % str(e)).encode())

    def log_message(self, *a):
        pass

def main():
    global _opener, _mode
    os.chdir(HERE)
    if credentials():
        _opener = do_login()
        if _opener:
            _mode = "login"
            print("Logged in (fresh session) - live data + auto re-login on expiry.")
        else:
            _mode = "cookie"
            print("!! login.txt present but login FAILED - falling back to session.txt cookie.")
    else:
        _mode = "cookie"
        print("Using session.txt cookie (no credentials in login.txt).")
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    with socketserver.ThreadingTCPServer((HOST, PORT), Handler) as httpd:
        print("Realtime proxy running  [mode: %s]" % _mode)
        print("  Open:  http://localhost:%d/realtime.html" % PORT)
        print("  Stop:  close this window (or Ctrl+C)")
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            pass

if __name__ == "__main__":
    main()
