#!/usr/bin/env python3
"""
The local server for the cockpit page. Serves this project folder and, unlike a
plain file, accepts the page's save actions so Steven's hide and bid choices
persist to the local database on the Mac Studio.

It reads screens and serves a page only. It never places a bid. The bid list is
Steven's own shortlist.

Run with:  python3 serve.py
Then open: http://localhost:8765/cockpit.html

Important: the page must be opened at this address (or the private link), never
as a file, or the hide and bid buttons cannot save.
"""

import http.server
import functools
import json
import os
import sys
import time
import subprocess
import threading

from bidbrain import db

HERE = os.path.dirname(os.path.abspath(__file__))
PORT = 8765
LAST_RUN = os.path.join(HERE, "data", "last_run.json")
PROGRESS = os.path.join(HERE, "data", "run_progress.json")
MANUAL_LOG = os.path.join(HERE, "data", "logs", "manual_run.log")

# Tracks a manually triggered daily run so two cannot overlap and the page can
# poll for completion. Set when the Run now button starts a run.
_run_lock = threading.Lock()
_run_proc = None


def _run_status():
    """Whether a manual run is in progress, and when the last run finished
    (the mtime of the run cache the daily run rewrites)."""
    running = _run_proc is not None and _run_proc.poll() is None
    last = None
    try:
        last = int(os.path.getmtime(LAST_RUN))
    except OSError:
        pass
    progress = None
    try:
        with open(PROGRESS, encoding="utf-8") as f:
            progress = json.load(f)
    except (OSError, ValueError):
        pass
    return {"running": running, "last_run": last, "progress": progress, "now": time.time()}


class CockpitHandler(http.server.SimpleHTTPRequestHandler):
    """Serves files as normal, plus a small JSON API for the page's actions."""

    def end_headers(self):
        # Tell browsers never to reuse a cached copy. The page changes whenever a
        # daily run or a design tweak rewrites cockpit.html, and phones were
        # showing a stale version. The page is tiny, so refetching every time is
        # cheap and saves the constant hard refresh confusion.
        self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
        self.send_header("Pragma", "no-cache")
        self.send_header("Expires", "0")
        super().end_headers()

    def _json(self, code, obj):
        payload = json.dumps(obj).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def do_GET(self):
        if self.path == "/api/hidden":
            try:
                return self._json(200, {"ok": True, "hidden": db.hidden_notes(only_new=False)})
            except Exception as e:
                return self._json(400, {"ok": False, "error": str(e)})
        if self.path == "/api/run-status":
            return self._json(200, {"ok": True, **_run_status()})
        return super().do_GET()

    def do_POST(self):
        routes = {"/api/hide": self._hide, "/api/unhide": self._unhide,
                  "/api/bid": self._bid, "/api/run": self._run}
        fn = routes.get(self.path)
        if not fn:
            return self._json(404, {"ok": False, "error": "unknown endpoint"})
        try:
            length = int(self.headers.get("Content-Length", 0) or 0)
            body = self.rfile.read(length) if length else b"{}"
            data = json.loads(body or b"{}")
            return self._json(200, {"ok": True, **fn(data)})
        except Exception as e:
            return self._json(400, {"ok": False, "error": str(e)})

    def _hide(self, d):
        db.hide_car(d.get("reg", ""), d.get("reason", ""), d.get("make", ""),
                    d.get("model", ""), d.get("name", ""))
        return {"reg": d.get("reg", "")}

    def _unhide(self, d):
        db.unhide_car(d.get("reg", ""))
        return {"reg": d.get("reg", "")}

    def _bid(self, d):
        db.set_bid(d.get("reg", ""), d.get("sale_date", ""), bool(d.get("on", True)),
                   d.get("make", ""), d.get("model", ""), d.get("name", ""),
                   d.get("max_bid"), d.get("reserve"))
        return {"reg": d.get("reg", ""), "on": bool(d.get("on", True))}

    def _run(self, d):
        """Start a full daily run in the background, the same as the 5.10pm
        scheduled job. Refuses to start a second one while one is in progress."""
        global _run_proc
        with _run_lock:
            if _run_proc is not None and _run_proc.poll() is None:
                return {"started": False, "running": True}
            os.makedirs(os.path.dirname(MANUAL_LOG), exist_ok=True)
            logf = open(MANUAL_LOG, "ab")
            _run_proc = subprocess.Popen(
                [sys.executable, "daily_run.py"],
                cwd=HERE, stdout=logf, stderr=subprocess.STDOUT,
                start_new_session=True,
            )
            return {"started": True, "running": True}

    def log_message(self, *args):
        pass  # keep the console quiet


def main():
    db.init_db()
    handler = functools.partial(CockpitHandler, directory=HERE)
    with http.server.ThreadingHTTPServer(("127.0.0.1", PORT), handler) as httpd:
        print(f"BidBrain cockpit on http://localhost:{PORT}/cockpit.html")
        print(f"Hidden cars page on http://localhost:{PORT}/hidden.html")
        httpd.serve_forever()


if __name__ == "__main__":
    main()
