#!/usr/bin/env python3
"""
Auto-Pickwave Control Panel  -  Passenger WSGI app (stdlib only, Python 3.6+)
============================================================================

Hosted at https://pickwave.bestoy.co.uk via cPanel Application Manager (Passenger).
Reads/writes the SAME control.json + waves.json the pickwave engine uses, so the
controls are live. Never touches config.json (credentials).

Auth: HTTP Basic for direct browser access. Set these as Application Manager
environment variables:
    LW_PANEL_USER   = <username>
    LW_PANEL_PASS   = <password>
(If unset, the panel refuses all access.)

Embedded inside Linnworks (iframe), a Basic-auth popup is awkward, so the panel
also accepts a secret passed in the URL query string:
    LW_PANEL_EMBED_KEY = <long random secret>
The Linnworks app module URL is https://pickwave.bestoy.co.uk/?key=<that secret>.
The panel only allows itself to be framed by *.linnworks.net (clickjacking guard).
"""
import base64
import html
import json
import os
import urllib.parse
from datetime import datetime, timezone

PICKWAVE_DIR = os.environ.get("LW_PICKWAVE_DIR", "/home/bestoyadmin/pickwave")
CONTROL = os.path.join(PICKWAVE_DIR, "control.json")
WAVES = os.path.join(PICKWAVE_DIR, "waves.json")
CRON_LOG = os.path.join(PICKWAVE_DIR, "cron.log")
USER = os.environ.get("LW_PANEL_USER", "")
PASS = os.environ.get("LW_PANEL_PASS", "")
# Secret in the URL query string, used when the panel is embedded in Linnworks.
EMBED_KEY = os.environ.get("LW_PANEL_EMBED_KEY", "")
# Origins allowed to embed the panel in an iframe (clickjacking protection).
FRAME_ANCESTORS = os.environ.get("LW_PANEL_FRAME_ANCESTORS",
                                 "https://*.linnworks.net https://linnworks.net")
DAYS = [(1, "Mon"), (2, "Tue"), (3, "Wed"), (4, "Thu"), (5, "Fri"), (6, "Sat"), (7, "Sun")]


def load(path, default):
    try:
        with open(path, "r", encoding="utf-8") as fh:
            return json.load(fh)
    except Exception:
        return default


def save(path, data):
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as fh:
        json.dump(data, fh, indent=2)
    os.replace(tmp, path)


def recent_runs(n=6):
    try:
        with open(CRON_LOG, "r", encoding="utf-8", errors="replace") as fh:
            lines = [l.rstrip() for l in fh if "RUN SUMMARY" in l or "CREATED" in l or "matched=" in l]
        return lines[-n:] or ["(no runs logged yet)"]
    except Exception:
        return ["(no cron.log yet)"]


def authed(environ):
    # Embed access: ?key=<secret> in the URL (used by the Linnworks iframe).
    if EMBED_KEY:
        qs = urllib.parse.parse_qs(environ.get("QUERY_STRING", ""))
        vals = qs.get("key", [])
        if vals and vals[0] == EMBED_KEY:
            return True
    if not (USER and PASS):
        return False
    hdr = environ.get("HTTP_AUTHORIZATION", "")
    if not hdr.startswith("Basic "):
        return False
    try:
        u, p = base64.b64decode(hdr[6:]).decode("utf-8", "replace").split(":", 1)
    except Exception:
        return False
    return u == USER and p == PASS


def page():
    ctrl = load(CONTROL, {"enabled": True})
    wc = load(WAVES, {"waves": []})
    master = ctrl.get("enabled", True)
    waves = wc.get("waves", [])
    rows = []
    for i, w in enumerate(waves):
        sch = w.get("schedule", {}) or {}
        days = sch.get("days_of_week", [])
        times = ", ".join(sch.get("times", []))
        on = w.get("enabled", True)
        tags = " ".join('<span class=tag>{0}</span>'.format(html.escape(str(t))) for t in w.get("identifiers", []))
        chips = "".join(
            '<label class="day {0}"><input type=checkbox name=dow value={1} {2} onchange="this.form.submit()" style="display:none">{3}</label>'.format(
                "dayon" if d in days else "", d, "checked" if d in days else "", lbl)
            for d, lbl in DAYS)
        rows.append('''<div class="card {paused}">
  <div class=row><div><span class=name>{name}</span> {tags}</div>
    <form method=post style=margin:0><input type=hidden name=action value=wavetoggle><input type=hidden name=i value={i}>
    <button class="{bcls}">{blabel}</button></form></div>
  <form method=post style=margin-top:8px><input type=hidden name=action value=schedule><input type=hidden name=i value={i}>
    <div class=days>{chips}</div>
    <div class=muted>Run times (HH:MM, comma separated):</div>
    <input type=text name=times value="{times}">
    <button class=sec>Save times</button>
  </form></div>'''.format(
            paused="" if on else "paused", name=html.escape(w.get("name", "?")), tags=tags, i=i,
            bcls="stop" if on else "go", blabel="Pause" if on else "Resume", chips=chips, times=html.escape(times)))

    runs = "\n".join(html.escape(r) for r in recent_runs())
    mstat = '<span class=on>● RUNNING</span>' if master else '<span class=off>● PAUSED (all waves held)</span>'
    mbtn = '<button class=stop name=action value=masteroff>Pause everything</button>' if master else '<button class=go name=action value=masteron>Resume everything</button>'
    return '''<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>Pickwave Control</title><style>
body{{font:15px system-ui,-apple-system,sans-serif;margin:24px auto;max-width:780px;color:#1c2530;padding:0 14px}}
.card{{border:1px solid #dde2e8;border-radius:12px;padding:14px 18px;margin:12px 0}}
.row{{display:flex;justify-content:space-between;align-items:center}}
.on{{color:#0a7d33;font-weight:600}}.off{{color:#b00020;font-weight:600}}
.name{{font-weight:600;font-size:16px}}.tag{{font-size:12px;color:#15448f;background:#eaf1fe;padding:2px 8px;border-radius:6px;margin-left:4px}}
button{{font:14px system-ui;padding:7px 13px;border-radius:8px;border:0;cursor:pointer;color:#fff}}
.go{{background:#0a7d33}}.stop{{background:#b00020}}.sec{{background:#2257c5;margin-top:8px}}
.day{{display:inline-block;padding:4px 10px;margin:2px;border-radius:6px;border:1px solid #cfd6de;font-size:13px;cursor:pointer}}
.dayon{{background:#eaf1fe;border-color:#2257c5;color:#15448f}}
.days{{margin:8px 0}}.muted{{color:#6b7480;font-size:13px}}.paused{{opacity:.55}}
input[type=text]{{font:13px system-ui;padding:7px;width:100%;box-sizing:border-box;border:1px solid #cfd6de;border-radius:7px}}
pre{{background:#0d1117;color:#d6deeb;padding:12px;border-radius:8px;overflow:auto;font-size:12px;line-height:1.6}}
h2{{font-weight:600}}
</style>
<h2>Auto-pickwave control</h2>
<div class="card row"><div>Master: {mstat}</div><form method=post style=margin:0>{mbtn}</form></div>
{rows}
<div class=card><div class=name style=margin-bottom:8px>Recent runs</div><pre>{runs}</pre></div>'''.format(
        mstat=mstat, mbtn=mbtn, rows="".join(rows), runs=runs)


def handle_post(environ):
    size = int(environ.get("CONTENT_LENGTH") or 0)
    body = environ["wsgi.input"].read(size).decode("utf-8", "replace") if size else ""
    form = {k: v[-1] for k, v in urllib.parse.parse_qs(body, keep_blank_values=True).items()}
    multi = urllib.parse.parse_qs(body, keep_blank_values=True)
    action = form.get("action", "")
    if action in ("masteron", "masteroff"):
        ctrl = load(CONTROL, {"enabled": True})
        ctrl["enabled"] = (action == "masteron")
        ctrl["updated_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
        save(CONTROL, ctrl)
        return
    wc = load(WAVES, {"waves": []})
    waves = wc.get("waves", [])
    try:
        i = int(form.get("i", -1))
    except ValueError:
        i = -1
    if not (0 <= i < len(waves)):
        return
    w = waves[i]
    if action == "wavetoggle":
        w["enabled"] = not w.get("enabled", True)
    elif action == "schedule":
        sch = w.setdefault("schedule", {})
        sch["days_of_week"] = sorted(int(x) for x in multi.get("dow", []))
        times = []
        for t in form.get("times", "").split(","):
            t = t.strip()
            if not t:
                continue
            hh, mm = t.split(":")
            if 0 <= int(hh) <= 23 and 0 <= int(mm) <= 59:
                times.append("%02d:%02d" % (int(hh), int(mm)))
        sch["times"] = times
    wc["updated_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
    save(WAVES, wc)


def application(environ, start_response):
    csp = ("Content-Security-Policy", "frame-ancestors " + FRAME_ANCESTORS)
    if not authed(environ):
        start_response("401 Unauthorized",
                       [("WWW-Authenticate", 'Basic realm="Pickwave Control"'),
                        ("Content-Type", "text/plain"), csp])
        return [b"Authentication required"]
    if environ.get("REQUEST_METHOD") == "POST":
        try:
            handle_post(environ)
        except Exception as exc:  # noqa: BLE001
            start_response("500 Internal Server Error", [("Content-Type", "text/plain"), csp])
            return [("Error: %s" % exc).encode("utf-8")]
        # Preserve the query string (e.g. ?key=...) across the post-redirect-get
        # so embed auth survives form submissions inside the Linnworks iframe.
        loc = environ.get("SCRIPT_NAME", "") or "/"
        qs = environ.get("QUERY_STRING", "")
        if qs:
            loc = loc + "?" + qs
        start_response("303 See Other", [("Location", loc), csp])
        return [b""]
    body = page().encode("utf-8")
    start_response("200 OK", [("Content-Type", "text/html; charset=utf-8"),
                              ("Content-Length", str(len(body))), csp])
    return [body]
