#!/usr/bin/env python3
"""
Launch27 cancellation zap receiver.

Accepts webhook/zap POSTs for cancellation events and posts a structured alert to Slack.
Designed to be simple, stdlib-only, and easy to run behind the existing preview/tailscale setup.

Endpoints:
- GET  /health
- HEAD /launch27-cancellation-webhook
- POST /launch27-cancellation-webhook

Expected payload:
The receiver is intentionally forgiving. It accepts either a flat JSON body or a nested
payload under `data` / `booking` / `event` keys.

Useful fields it will try to extract:
- booking_id / bookingId / id
- customer_name / customer / full_name / name
- address / service_address
- scheduled_at / booking_date / date + time
- cancellation_reason / reason / notes
- cancellation_scope / scope
- cancelled_by / actor / source
- event / event_type / zap / trigger

Slack target:
- defaults to #cancellations (CLXFQ8NBW)
"""

import http.server
import json
import os
import urllib.request
from datetime import datetime

PORT = int(os.environ.get("L27_CANCEL_WEBHOOK_PORT", "8782"))
WORKSPACE = os.path.expanduser("~/.openclaw/workspace")
EVENTS_FILE = os.path.join(WORKSPACE, "memory", "launch27-cancellation-events.jsonl")
LOG_FILE = "/tmp/openclaw/launch27-cancellation-webhook.log"
SLACK_CHANNEL = "CLXFQ8NBW"  # #cancellations
SLACK_TOKEN_FILE = os.path.expanduser("~/.openclaw/secrets/slack-token.txt")


class ReusableTCPServer(http.server.HTTPServer):
    allow_reuse_address = True


def log(msg: str) -> None:
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
        with open(LOG_FILE, "a") as f:
            f.write(line + "\n")
    except Exception:
        pass


def read_secret(path: str) -> str:
    with open(path) as f:
        return f.read().strip()


def append_event(payload: dict, summary: dict) -> None:
    os.makedirs(os.path.dirname(EVENTS_FILE), exist_ok=True)
    row = {
        "received_at": datetime.utcnow().isoformat() + "Z",
        "summary": summary,
        "payload": payload,
    }
    with open(EVENTS_FILE, "a") as f:
        f.write(json.dumps(row) + "\n")


def flatten_payload(payload: dict) -> dict:
    out = dict(payload)
    for key in ("data", "booking", "event"):
        value = payload.get(key)
        if isinstance(value, dict):
            for k, v in value.items():
                out.setdefault(k, v)
    return out


def first_value(data: dict, keys, default=""):
    for key in keys:
        value = data.get(key)
        if value not in (None, ""):
            return value
    return default


def summarize(payload: dict) -> dict:
    data = flatten_payload(payload)
    date = first_value(data, ["scheduled_at", "booking_date", "date", "service_date"])
    time = first_value(data, ["time", "booking_time", "start_time"])
    when = date if not time else f"{date} {time}".strip()

    return {
        "booking_id": first_value(data, ["booking_id", "bookingId", "id"]),
        "customer": first_value(data, ["customer_name", "customer", "full_name", "name"]),
        "address": first_value(data, ["service_address", "address"]),
        "when": when,
        "reason": first_value(data, ["cancellation_reason", "reason", "notes", "comment"]),
        "scope": first_value(data, ["cancellation_scope", "scope"]),
        "cancelled_by": first_value(data, ["cancelled_by", "actor", "source"]),
        "event_type": first_value(data, ["event", "event_type", "trigger", "zap"], "launch27_cancellation"),
    }


def post_to_slack(summary: dict) -> dict:
    token = read_secret(SLACK_TOKEN_FILE)
    title = ":no_entry_sign: Launch27 cancellation alert"
    lines = [title]
    if summary["customer"]:
        lines.append(f"*Customer:* {summary['customer']}")
    if summary["booking_id"]:
        lines.append(f"*Booking ID:* `{summary['booking_id']}`")
    if summary["when"]:
        lines.append(f"*When:* {summary['when']}")
    if summary["address"]:
        lines.append(f"*Address:* {summary['address']}")
    if summary["scope"]:
        lines.append(f"*Scope:* {summary['scope']}")
    if summary["reason"]:
        lines.append(f"*Reason:* {summary['reason']}")
    if summary["cancelled_by"]:
        lines.append(f"*Cancelled by:* {summary['cancelled_by']}")
    lines.append(f"*Event:* `{summary['event_type']}`")
    lines.append("\nPlease confirm reason + scope if missing, and handle in thread.")

    payload = {
        "channel": SLACK_CHANNEL,
        "text": "\n".join(lines),
        "unfurl_links": False,
        "unfurl_media": False,
    }

    req = urllib.request.Request(
        "https://slack.com/api/chat.postMessage",
        data=json.dumps(payload).encode(),
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "User-Agent": "Harvey Launch27 Cancel Receiver/1.0",
        },
    )
    with urllib.request.urlopen(req, timeout=20) as resp:
        return json.loads(resp.read().decode())


class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        pass

    def do_HEAD(self):
        if self.path == "/launch27-cancellation-webhook":
            self.send_response(200)
            self.end_headers()
        else:
            self.send_response(404)
            self.end_headers()

    def do_GET(self):
        if self.path == "/health":
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"Launch27 cancellation receiver OK")
        else:
            self.send_response(404)
            self.end_headers()

    def do_POST(self):
        if self.path != "/launch27-cancellation-webhook":
            self.send_response(404)
            self.end_headers()
            return

        try:
            content_length = int(self.headers.get("Content-Length", 0))
            raw = self.rfile.read(content_length) if content_length > 0 else b"{}"
            payload = json.loads(raw.decode("utf-8", errors="replace"))
            summary = summarize(payload)
            append_event(payload, summary)
            slack_result = post_to_slack(summary)
            ok = bool(slack_result.get("ok"))
            log(f"Cancellation event received, booking={summary.get('booking_id') or '?'} slack_ok={ok}")

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"ok": True, "slack_ok": ok, "summary": summary}).encode())
        except Exception as e:
            log(f"POST error: {e}")
            self.send_response(500)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"ok": False, "error": str(e)}).encode())


def main():
    os.makedirs("/tmp/openclaw", exist_ok=True)
    log(f"Starting Launch27 cancellation receiver on port {PORT}")
    server = ReusableTCPServer(("0.0.0.0", PORT), Handler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        log("Shutting down")
        server.shutdown()


if __name__ == "__main__":
    main()
