#!/usr/bin/env python3
"""Enterprise Lighthouse foundation: registry, preflight, and audit only.

This service is deliberately not a remote shell.  It accepts node admission
requests and validates fixed operation plans; execution remains with a future
GLSV node connector after human authorization.
"""
import hmac
import json
import os
import sqlite3
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from ipaddress import ip_address

DB = os.environ.get("LIGHTHOUSE_DB", "/var/lib/guanghu-enterprise-lighthouse/lighthouse.db")
TOKEN = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "")
HOST = os.environ.get("LIGHTHOUSE_BIND", "127.0.0.1")
PORT = int(os.environ.get("LIGHTHOUSE_PORT", "8031"))
FIXED_ACTIONS = {"health_check", "backup", "deploy_release", "restart_service", "rollback"}
DOMAINS = {
    "DOMAIN-ZS": "零感域", "DOMAIN-MAIN": "光湖主域", "DOMAIN-SUB": "光湖分域",
    "DOMAIN-ZERO": "光湖零域", "DOMAIN-FIFTH": "第五域",
}
ENTERPRISE_MANAGED_DOMAINS = {"DOMAIN-ZS", "DOMAIN-MAIN", "DOMAIN-SUB", "DOMAIN-ZERO"}
EXTERNAL_FOUNDATION_DOMAINS = {"DOMAIN-FIFTH"}


def now():
    return int(time.time())


def connection():
    os.makedirs(os.path.dirname(DB), exist_ok=True)
    db = sqlite3.connect(DB)
    db.row_factory = sqlite3.Row
    db.executescript("""
      CREATE TABLE IF NOT EXISTS domains (
        id TEXT PRIMARY KEY, name TEXT NOT NULL, state TEXT NOT NULL, created_at INTEGER NOT NULL
      );
      CREATE TABLE IF NOT EXISTS intakes (
        id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, state TEXT NOT NULL,
        human_name TEXT NOT NULL, email TEXT NOT NULL, server_ip TEXT NOT NULL,
        domain_id TEXT NOT NULL, persona_ids TEXT NOT NULL, repository_urls TEXT NOT NULL,
        hosting_mode TEXT NOT NULL, notes TEXT NOT NULL
      );
      CREATE TABLE IF NOT EXISTS nodes (
        id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, intake_id TEXT, state TEXT NOT NULL,
        display_name TEXT NOT NULL, server_ip TEXT NOT NULL, allowed_actions TEXT NOT NULL,
        public_key_fingerprint TEXT, last_heartbeat INTEGER, created_at INTEGER NOT NULL
      );
      CREATE TABLE IF NOT EXISTS audit (
        id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL
      );
    """)
    for domain_id, name in DOMAINS.items():
        state = "EXTERNAL_PRIVATE_FOUNDATION" if domain_id in EXTERNAL_FOUNDATION_DOMAINS else "PENDING_ENTRY_NODE"
        db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, ?, ?)", (domain_id, name, state, now()))
    db.commit()
    return db


def audit(db, kind, payload):
    db.execute("INSERT INTO audit VALUES (?, ?, ?, ?)", (str(uuid.uuid4()), now(), kind, json.dumps(payload, ensure_ascii=False)))
    db.commit()


def parse_json(handler):
    length = int(handler.headers.get("Content-Length", "0"))
    if not 0 < length <= 50_000:
        raise ValueError("request body must be between 1 and 50000 bytes")
    return json.loads(handler.rfile.read(length).decode("utf-8"))


def valid_email(value):
    return isinstance(value, str) and len(value) <= 254 and value.count("@") == 1


def require_admin(handler):
    received = handler.headers.get("X-Lighthouse-Admin-Token", "")
    return bool(TOKEN) and hmac.compare_digest(received, TOKEN)


class Handler(BaseHTTPRequestHandler):
    server_version = "GuanghuEnterpriseLighthouse/1.0"

    def log_message(self, fmt, *args):
        print("[lighthouse] " + fmt % args)

    def respond(self, status, body):
        encoded = json.dumps(body, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(encoded)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(encoded)

    def do_GET(self):
        db = connection()
        try:
            if self.path == "/health":
                return self.respond(200, {"ok": True, "service": "guanghu-enterprise-lighthouse", "mode": "registry-and-preflight-only", "execution": "disabled"})
            if self.path == "/v1/status":
                counts = {row["state"]: row["count"] for row in db.execute("SELECT state, COUNT(*) AS count FROM nodes GROUP BY state")}
                return self.respond(200, {"ok": True, "domains": [dict(row) for row in db.execute("SELECT id,name,state FROM domains ORDER BY id")], "node_counts": counts, "fixed_actions": sorted(FIXED_ACTIONS), "raw_shell": "rejected"})
            if self.path == "/v1/nodes":
                return self.respond(200, {"ok": True, "nodes": [dict(row) for row in db.execute("SELECT id,domain_id,state,display_name,allowed_actions,last_heartbeat,created_at FROM nodes ORDER BY created_at DESC")]})
            return self.respond(404, {"ok": False, "error": "not found"})
        finally:
            db.close()

    def do_POST(self):
        if not require_admin(self):
            return self.respond(401, {"ok": False, "error": "admin authorization required"})
        try:
            payload = parse_json(self)
        except (ValueError, json.JSONDecodeError) as error:
            return self.respond(400, {"ok": False, "error": str(error)})
        if "cmd" in payload or "shell" in payload or "command" in payload:
            return self.respond(400, {"ok": False, "error": "raw commands are never accepted by the lighthouse"})
        db = connection()
        try:
            if self.path == "/v1/intakes":
                required = ("human_name", "email", "server_ip", "domain_id")
                if any(not payload.get(field) for field in required) or payload["domain_id"] not in DOMAINS or not valid_email(payload["email"]):
                    return self.respond(400, {"ok": False, "error": "human_name, valid email, server_ip, and known domain_id are required"})
                if payload["domain_id"] not in ENTERPRISE_MANAGED_DOMAINS:
                    return self.respond(403, {"ok": False, "error": "this domain is not managed by the enterprise lighthouse; a separate explicit sovereign authorization is required"})
                try:
                    ip_address(payload["server_ip"])
                except ValueError:
                    return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"})
                intake_id = "INTAKE-" + uuid.uuid4().hex[:12].upper()
                db.execute("INSERT INTO intakes VALUES (?, ?, 'PENDING_REVIEW', ?, ?, ?, ?, ?, ?, ?, ?)", (
                    intake_id, now(), payload["human_name"].strip(), payload["email"].strip(), payload["server_ip"], payload["domain_id"],
                    json.dumps(payload.get("persona_ids", [])), json.dumps(payload.get("repository_urls", [])),
                    payload.get("hosting_mode", "own"), payload.get("notes", ""),
                ))
                audit(db, "intake_created", {"intake_id": intake_id, "domain_id": payload["domain_id"]})
                return self.respond(201, {"ok": True, "intake_id": intake_id, "state": "PENDING_REVIEW", "next": "sovereign approval, human email confirmation, then node connector enrollment"})
            if self.path == "/v1/nodes/bootstrap":
                """Register a manually verified routing node without enabling execution.

                This exists for the controlled migration of an already verified
                domain entry.  It deliberately cannot make a node ACTIVE: the
                GLSV connector, server-local key enrollment, and human email
                confirmation remain required before any operation can proceed.
                """
                required = ("id", "domain_id", "display_name", "server_ip")
                if any(not isinstance(payload.get(field), str) or not payload[field].strip() for field in required):
                    return self.respond(400, {"ok": False, "error": "id, domain_id, display_name, and server_ip are required"})
                if payload["domain_id"] not in ENTERPRISE_MANAGED_DOMAINS:
                    return self.respond(403, {"ok": False, "error": "this domain cannot be registered by the enterprise lighthouse"})
                try:
                    ip_address(payload["server_ip"])
                except ValueError:
                    return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"})
                node_id = payload["id"].strip()
                if db.execute("SELECT 1 FROM nodes WHERE id=?", (node_id,)).fetchone():
                    return self.respond(409, {"ok": False, "error": "node id is already registered"})
                db.execute("INSERT INTO nodes VALUES (?, ?, NULL, 'CONNECTED_PENDING_CONNECTOR', ?, ?, ?, NULL, ?, ?)", (
                    node_id, payload["domain_id"], payload["display_name"].strip(), payload["server_ip"],
                    json.dumps(["health_check"]), now(), now(),
                ))
                db.execute("UPDATE domains SET state='ENTRY_NODE_CONNECTED' WHERE id=?", (payload["domain_id"],))
                audit(db, "node_bootstrap_connected", {"node_id": node_id, "domain_id": payload["domain_id"], "mode": "manual_verified_routing_only"})
                return self.respond(201, {"ok": True, "node_id": node_id, "state": "CONNECTED_PENDING_CONNECTOR", "execution": "disabled until GLSV connector enrollment and human authorization"})
            if self.path == "/v1/nodes/revoke":
                node_id = payload.get("node_id")
                if not isinstance(node_id, str) or not node_id.strip():
                    return self.respond(400, {"ok": False, "error": "node_id is required"})
                node = db.execute("SELECT id, domain_id, state FROM nodes WHERE id=?", (node_id.strip(),)).fetchone()
                if not node:
                    return self.respond(404, {"ok": False, "error": "node is not registered"})
                if node["state"] == "ACTIVE":
                    return self.respond(409, {"ok": False, "error": "ACTIVE nodes require a separate migration or retirement procedure"})
                db.execute("DELETE FROM nodes WHERE id=?", (node["id"],))
                if node["domain_id"] in EXTERNAL_FOUNDATION_DOMAINS:
                    db.execute("UPDATE domains SET state='EXTERNAL_PRIVATE_FOUNDATION' WHERE id=?", (node["domain_id"],))
                audit(db, "node_revoked", {"node_id": node["id"], "domain_id": node["domain_id"], "reason": "administrative revocation"})
                return self.respond(200, {"ok": True, "node_id": node["id"], "state": "REVOKED", "execution": "disabled"})
            if self.path == "/v1/preflight":
                action, node_id = payload.get("action"), payload.get("target_node_id")
                if action not in FIXED_ACTIONS or not node_id:
                    return self.respond(400, {"ok": False, "error": "fixed action and target_node_id are required", "allowed_actions": sorted(FIXED_ACTIONS)})
                node = db.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone()
                if not node:
                    return self.respond(404, {"ok": False, "decision": "REJECT", "reason": "target node is not registered"})
                allowed = json.loads(node["allowed_actions"])
                reasons = []
                if node["state"] != "ACTIVE": reasons.append("target node is not ACTIVE")
                if action not in allowed: reasons.append("action is not in the node allowlist")
                if action in {"deploy_release", "restart_service", "rollback"} and not payload.get("rollback_plan"): reasons.append("rollback_plan is required")
                if action in {"deploy_release", "restart_service"} and not payload.get("backup_reference"): reasons.append("backup_reference is required")
                decision = "ALLOW_FOR_AUTHORIZATION" if not reasons else "REJECT"
                result = {"ok": not reasons, "decision": decision, "reasons": reasons, "execution": "not performed; GLSV human authorization still required"}
                audit(db, "preflight", {"target_node_id": node_id, "action": action, "decision": decision, "reasons": reasons})
                return self.respond(200, result)
            return self.respond(404, {"ok": False, "error": "not found"})
        finally:
            db.close()


if __name__ == "__main__":
    print(f"Enterprise Lighthouse listening on {HOST}:{PORT}")
    ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
