"""Backend for the KKVS Alumni 30th Anniversary booking system.

Serves the static site from public/ and provides a small JSON API backed by
SQLite for creating bookings, uploading payment slips, and (for the admin)
reviewing bookings and updating their status.
"""

import io
import os
import re
import smtplib
import time
import sqlite3
from datetime import datetime, timezone
from email.mime.text import MIMEText
from pathlib import Path

from flask import Flask, request, jsonify, session, send_from_directory, send_file, abort, g
from openpyxl import Workbook
from openpyxl.styles import Font

BASE_DIR = Path(__file__).resolve().parent
PUBLIC_DIR = BASE_DIR / "public"
DATA_DIR = BASE_DIR / "data"
SLIPS_DIR = DATA_DIR / "slips"
RECEIPTS_DIR = DATA_DIR / "receipts"
DB_PATH = DATA_DIR / "bookings.db"

DATA_DIR.mkdir(exist_ok=True)
SLIPS_DIR.mkdir(exist_ok=True)
RECEIPTS_DIR.mkdir(exist_ok=True)

EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

app = Flask(__name__, static_folder=str(PUBLIC_DIR), static_url_path="")

# --- Config: override via environment variables before going live ---
app.secret_key = os.environ.get("KKVS_FLASK_SECRET_KEY", "kkvs-30th-anniversary-admin-secret-CHANGE-ME")
ADMIN_PASSWORD = os.environ.get("KKVS_ADMIN_PASSWORD", "kkvs2026admin")

# Static PromptPay QR image supplied by the organizer (public/promptpay-qr.jpg).
# It's a fixed "receive money" QR with no amount encoded, so the amount is
# shown as text next to it in the booking flow.
PRICES = {"table": 8000.00, "seat": 1000.00}
PRICE_LABELS = {"table": "8,000.00 THB", "seat": "1,000.00 THB"}
ALLOWED_SLIP_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".pdf"}

LOCKED_TABLES = set(range(1, 9))
SEATS_PER_TABLE = 8

# --- Email notifications ---
# The booking forms only collect a phone number, not the payer's email, so
# this notifies the ADMIN whenever a new booking comes in (so they know to
# check the payment slip). The app password is read from an environment
# variable rather than hardcoded here — never commit or paste the raw
# password into this file. Set it before starting the server:
#   export KKVS_SMTP_APP_PASSWORD="your-16-char-app-password"
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
SMTP_USERNAME = "johan.vmsch@gmail.com"
SMTP_PASSWORD = os.environ.get("KKVS_SMTP_APP_PASSWORD", "")
ADMIN_NOTIFY_EMAIL = "johan.vmsch@gmail.com"
EMAIL_NOTIFICATIONS_ENABLED = bool(SMTP_PASSWORD)


def _send_email(to_address: str, subject: str, body: str) -> None:
    if not EMAIL_NOTIFICATIONS_ENABLED:
        print(f"[email] Notifications disabled — would have sent to {to_address}:\nSubject: {subject}\n\n{body}\n", flush=True)
        return

    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = SMTP_USERNAME
    msg["To"] = to_address

    try:
        with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as server:
            server.starttls()
            server.login(SMTP_USERNAME, SMTP_PASSWORD)
            server.sendmail(SMTP_USERNAME, [to_address], msg.as_string())
        print(f"[email] Sent to {to_address}: {subject}", flush=True)
    except Exception as exc:
        # Never let an email failure break the booking/status update itself.
        print(f"[email] Failed to send to {to_address}: {exc}", flush=True)


def send_booking_notification_email(booking: dict) -> None:
    lines = [
        f"New booking received: {booking['booking_number']}",
        "",
        f"Table: {booking['table_number']}",
        f"Booking type: {'Table' if booking['booking_type'] == 'table' else 'Seat'}",
        f"Price: {booking['price_label']}",
        f"Name: {booking['first_name']} {booking['last_name']}",
        f"Phone: {booking['phone']}",
        f"Email: {booking.get('email', '')}",
    ]
    if booking.get("batch"):
        lines.append(f"Batch: {booking['batch']}")
    if booking.get("relationship"):
        lines.append(f"Relationship to alumni partner: {booking['relationship']}")
        lines.append(
            f"Alumni partner: {booking.get('partner_first_name', '')} {booking.get('partner_last_name', '')}"
        )
    if booking.get("allergies"):
        lines.append(f"Allergies: {booking['allergies']}")
    lines.append(f"Status: {booking['status']}")
    lines.append("")
    lines.append("Log into the admin page to review the payment slip and confirm.")
    body = "\n".join(lines)

    _send_email(
        ADMIN_NOTIFY_EMAIL,
        f"New booking {booking['booking_number']} — KKVS Alumni 30th Anniversary",
        body,
    )


def send_confirmation_email(booking: dict) -> None:
    if not booking.get("email"):
        print(f"[email] No payer email on file for {booking['booking_number']}; skipping confirmation email.", flush=True)
        return

    lines = [
        f"Hi {booking['first_name']},",
        "",
        f"Your booking {booking['booking_number']} has been confirmed!",
        "",
        f"Table: {booking['table_number']}",
        f"Booking type: {'Table' if booking['booking_type'] == 'table' else 'Seat'}",
        f"Price: {booking['price_label']}",
        "",
        "You can check your reservation status and download your receipt (once uploaded) "
        "anytime from the \"Check your reservation status\" link on the event website, "
        "using the phone number you booked with.",
        "",
        "See you at the KKVS Alumni 30th Anniversary!",
    ]
    body = "\n".join(lines)

    _send_email(
        booking["email"],
        f"Booking Confirmed — {booking['booking_number']} — KKVS Alumni 30th Anniversary",
        body,
    )


def allowed_booking_types(table_number: int):
    if table_number in LOCKED_TABLES:
        return set()
    if 9 <= table_number <= 24:
        return {"table"}
    if 25 <= table_number <= 32:
        return {"table", "seat"}
    return set()


def get_table_summary(db, table_number: int):
    rows = db.execute(
        "SELECT booking_type, COUNT(*) AS c FROM bookings "
        "WHERE table_number = ? AND status != 'Cancelled' GROUP BY booking_type",
        (table_number,),
    ).fetchall()
    table_booked = False
    seats_booked = 0
    for row in rows:
        if row["booking_type"] == "table":
            table_booked = True
        elif row["booking_type"] == "seat":
            seats_booked = row["c"]
    return {"table_booked": table_booked, "seats_booked": seats_booked}


# --- Database ---

def get_db():
    db = getattr(g, "_db", None)
    if db is None:
        db = g._db = sqlite3.connect(DB_PATH)
        db.row_factory = sqlite3.Row
    return db


@app.teardown_appcontext
def close_db(_exception):
    db = getattr(g, "_db", None)
    if db is not None:
        db.close()


def _ensure_column(conn, table, column, coltype):
    cols = [row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()]
    if column not in cols:
        conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {coltype}")


def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS bookings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            booking_number TEXT UNIQUE,
            table_number INTEGER NOT NULL,
            booking_type TEXT NOT NULL,
            price_label TEXT NOT NULL,
            first_name TEXT NOT NULL,
            last_name TEXT NOT NULL,
            phone TEXT NOT NULL,
            email TEXT,
            batch TEXT NOT NULL,
            birth_date TEXT NOT NULL,
            allergies TEXT,
            slip_filename TEXT,
            receipt_filename TEXT,
            status TEXT NOT NULL DEFAULT 'Awaiting Payment Confirmation',
            created_at TEXT NOT NULL,
            seat_holder_type TEXT,
            relationship TEXT,
            partner_first_name TEXT,
            partner_last_name TEXT
        )
        """
    )
    # Existing databases created before these columns existed.
    _ensure_column(conn, "bookings", "seat_holder_type", "TEXT")
    _ensure_column(conn, "bookings", "relationship", "TEXT")
    _ensure_column(conn, "bookings", "partner_first_name", "TEXT")
    _ensure_column(conn, "bookings", "partner_last_name", "TEXT")
    _ensure_column(conn, "bookings", "email", "TEXT")
    _ensure_column(conn, "bookings", "receipt_filename", "TEXT")

    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS alumni_roster (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            full_name TEXT NOT NULL,
            normalized_name TEXT NOT NULL UNIQUE
        )
        """
    )
    conn.commit()
    conn.close()


init_db()


def generate_booking_number(row_id: int) -> str:
    return f"KKVS-{row_id:05d}"


def require_admin():
    if not session.get("is_admin"):
        abort(401)


def row_to_dict(row):
    d = dict(row)
    if d.get("slip_filename"):
        d["slip_url"] = f"/api/slips/{d['slip_filename']}"
    if d.get("receipt_filename"):
        d["receipt_url"] = f"/api/receipts/{d['receipt_filename']}"
    return d


def normalize_name(name: str) -> str:
    return re.sub(r"\s+", " ", name.strip().lower())


def normalize_phone(phone: str) -> str:
    return re.sub(r"\D", "", phone or "")


def phone_already_used(db, phone: str) -> bool:
    normalized = normalize_phone(phone)
    if not normalized:
        return False
    rows = db.execute("SELECT phone FROM bookings WHERE status != 'Cancelled'").fetchall()
    return any(normalize_phone(row["phone"]) == normalized for row in rows)


def parse_roster_file(raw_text: str):
    """Each line is either a full name, or 'Last,First' / 'First,Last' CSV."""
    names = []
    for line in raw_text.splitlines():
        line = line.strip().strip("﻿")
        if not line:
            continue
        if "," in line:
            parts = [p.strip() for p in line.split(",") if p.strip()]
            line = " ".join(parts)
        names.append(line)
    return names


def get_roster_set(db):
    rows = db.execute("SELECT normalized_name FROM alumni_roster").fetchall()
    return {r["normalized_name"] for r in rows}


def is_grad(first_name: str, last_name: str, roster_set: set) -> bool:
    forward = normalize_name(f"{first_name} {last_name}")
    reverse = normalize_name(f"{last_name} {first_name}")
    return forward in roster_set or reverse in roster_set


# --- Pages ---

@app.route("/")
def home():
    return send_from_directory(str(PUBLIC_DIR), "index.html")


# --- API: bookings ---

VALID_RELATIONSHIPS = {"Spouse", "Family"}


@app.route("/api/bookings", methods=["POST"])
def create_booking():
    payload = request.get_json(force=True) or {}

    booking_type = payload.get("bookingType")
    if booking_type not in PRICES:
        return jsonify({"error": "Invalid booking type"}), 400

    # Seat bookings split into "alumni" (default, same form as a table booking)
    # and "spouse_family" (no batch number, but a relationship + partner name).
    seat_holder_type = payload.get("seatHolderType") or "alumni"
    is_spouse_family = booking_type == "seat" and seat_holder_type == "spouse_family"

    required = ["tableNumber", "bookingType", "firstName", "lastName", "phone", "email", "birthDate"]
    if is_spouse_family:
        required += ["relationship", "partnerFirstName", "partnerLastName"]
    else:
        required += ["batch"]

    missing = [f for f in required if not str(payload.get(f, "")).strip()]
    if missing:
        return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400

    if not EMAIL_PATTERN.match(payload["email"].strip()):
        return jsonify({"error": "Please enter a valid email address."}), 400

    if is_spouse_family and payload["relationship"] not in VALID_RELATIONSHIPS:
        return jsonify({"error": "Relationship must be Spouse or Family."}), 400

    try:
        table_number = int(payload["tableNumber"])
    except (TypeError, ValueError):
        return jsonify({"error": "Invalid table number"}), 400

    if booking_type not in allowed_booking_types(table_number):
        return jsonify({"error": "This table is not available for that booking type."}), 400

    db = get_db()
    summary = get_table_summary(db, table_number)

    if summary["table_booked"]:
        return jsonify({"error": "This table has already been booked."}), 409

    if booking_type == "table" and summary["seats_booked"] > 0:
        return jsonify({"error": "Some seats at this table are already booked, so the whole table is unavailable."}), 409

    if booking_type == "seat" and summary["seats_booked"] >= SEATS_PER_TABLE:
        return jsonify({"error": "All seats at this table are already booked."}), 409

    if phone_already_used(db, payload["phone"]):
        return jsonify({"error": "This phone number has already been used for another booking."}), 409

    cur = db.execute(
        """
        INSERT INTO bookings (
            booking_number, table_number, booking_type, price_label,
            first_name, last_name, phone, email, batch, birth_date, allergies,
            status, created_at, seat_holder_type, relationship,
            partner_first_name, partner_last_name
        ) VALUES ('', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            table_number,
            booking_type,
            PRICE_LABELS[booking_type],
            payload["firstName"].strip(),
            payload["lastName"].strip(),
            payload["phone"].strip(),
            payload["email"].strip(),
            (payload.get("batch") or "").strip(),
            payload["birthDate"].strip(),
            (payload.get("allergies") or "").strip(),
            "Awaiting Payment Confirmation",
            datetime.now(timezone.utc).isoformat(),
            seat_holder_type if booking_type == "seat" else None,
            payload.get("relationship").strip() if is_spouse_family else None,
            payload.get("partnerFirstName").strip() if is_spouse_family else None,
            payload.get("partnerLastName").strip() if is_spouse_family else None,
        ),
    )
    row_id = cur.lastrowid
    booking_number = generate_booking_number(row_id)
    db.execute("UPDATE bookings SET booking_number = ? WHERE id = ?", (booking_number, row_id))
    db.commit()

    send_booking_notification_email({
        "booking_number": booking_number,
        "table_number": table_number,
        "booking_type": booking_type,
        "price_label": PRICE_LABELS[booking_type],
        "first_name": payload["firstName"].strip(),
        "last_name": payload["lastName"].strip(),
        "phone": payload["phone"].strip(),
        "email": payload["email"].strip(),
        "batch": (payload.get("batch") or "").strip(),
        "allergies": (payload.get("allergies") or "").strip(),
        "relationship": payload.get("relationship", "").strip() if is_spouse_family else "",
        "partner_first_name": payload.get("partnerFirstName", "").strip() if is_spouse_family else "",
        "partner_last_name": payload.get("partnerLastName", "").strip() if is_spouse_family else "",
        "status": "Awaiting Payment Confirmation",
    })

    return jsonify(
        {
            "bookingNumber": booking_number,
            "status": "Awaiting Payment Confirmation",
            "amount": PRICES[booking_type],
            "priceLabel": PRICE_LABELS[booking_type],
        }
    ), 201


@app.route("/api/bookings/<booking_number>/slip", methods=["POST"])
def upload_slip(booking_number):
    db = get_db()
    row = db.execute("SELECT id FROM bookings WHERE booking_number = ?", (booking_number,)).fetchone()
    if not row:
        return jsonify({"error": "Booking not found"}), 404

    file = request.files.get("slip")
    if not file or not file.filename:
        return jsonify({"error": "No file uploaded"}), 400

    ext = Path(file.filename).suffix.lower()
    if ext not in ALLOWED_SLIP_EXTENSIONS:
        return jsonify({"error": "Unsupported file type"}), 400

    safe_name = f"{booking_number}-{int(time.time())}{ext}"
    file.save(str(SLIPS_DIR / safe_name))

    db.execute("UPDATE bookings SET slip_filename = ? WHERE booking_number = ?", (safe_name, booking_number))
    db.commit()

    return jsonify({"ok": True})


@app.route("/api/bookings/<booking_number>", methods=["GET"])
def get_booking(booking_number):
    db = get_db()
    row = db.execute(
        "SELECT booking_number, table_number, booking_type, price_label, status FROM bookings WHERE booking_number = ?",
        (booking_number,),
    ).fetchone()
    if not row:
        return jsonify({"error": "Booking not found"}), 404
    return jsonify(dict(row))


@app.route("/api/bookings/lookup")
def lookup_bookings_by_phone():
    """Public: lets a payer check their own reservation status by phone number."""
    normalized = normalize_phone(request.args.get("phone", ""))
    if not normalized:
        return jsonify({"error": "Please enter a phone number."}), 400

    db = get_db()
    rows = db.execute(
        "SELECT booking_number, table_number, booking_type, price_label, status, "
        "first_name, last_name, phone, receipt_filename, created_at FROM bookings ORDER BY id DESC"
    ).fetchall()

    matches = [dict(row) for row in rows if normalize_phone(row["phone"]) == normalized]
    for m in matches:
        del m["phone"]
        receipt_filename = m.pop("receipt_filename")
        m["has_receipt"] = bool(receipt_filename)
        if receipt_filename:
            m["receipt_url"] = f"/api/receipts/{receipt_filename}?phone={normalized}"

    return jsonify(matches)


@app.route("/api/table-status")
def table_status():
    """Public, no PII: just enough for the floor plan to show reservation accents."""
    db = get_db()
    rows = db.execute(
        "SELECT table_number, booking_type, status FROM bookings WHERE status != 'Cancelled'"
    ).fetchall()
    return jsonify([dict(r) for r in rows])


# --- API: admin ---

@app.route("/api/admin/login", methods=["POST"])
def admin_login():
    payload = request.get_json(force=True) or {}
    if payload.get("password") == ADMIN_PASSWORD:
        session["is_admin"] = True
        return jsonify({"ok": True})
    return jsonify({"error": "Incorrect password"}), 401


@app.route("/api/admin/logout", methods=["POST"])
def admin_logout():
    session.clear()
    return jsonify({"ok": True})


@app.route("/api/admin/session", methods=["GET"])
def admin_session():
    return jsonify({"isAdmin": bool(session.get("is_admin"))})


def get_bookings_with_grad(db):
    roster_set = get_roster_set(db)
    rows = db.execute("SELECT * FROM bookings ORDER BY id DESC").fetchall()

    bookings = []
    for row in rows:
        d = row_to_dict(row)
        if d.get("seat_holder_type") == "spouse_family":
            # The booker isn't the alumnus here — their partner is, so that's
            # whose name we check against the roster.
            d["is_grad"] = is_grad(d.get("partner_first_name") or "", d.get("partner_last_name") or "", roster_set)
        else:
            d["is_grad"] = is_grad(d["first_name"], d["last_name"], roster_set)
        bookings.append(d)
    return bookings


@app.route("/api/bookings", methods=["GET"])
def list_bookings():
    require_admin()
    db = get_db()
    return jsonify(get_bookings_with_grad(db))


@app.route("/api/admin/export", methods=["GET"])
def export_bookings():
    require_admin()
    db = get_db()
    bookings = get_bookings_with_grad(db)

    wb = Workbook()
    ws = wb.active
    ws.title = "Bookings"

    headers = [
        "Booking Number", "Table", "Booking Type", "Guest Type", "Price",
        "First Name", "Last Name", "Phone", "Email", "Batch", "Birth Date", "Allergies",
        "Relationship", "Alumni Partner First Name", "Alumni Partner Last Name",
        "Is Grad", "Status", "Payment Slip File", "Receipt File", "Created At",
    ]
    ws.append(headers)
    for cell in ws[1]:
        cell.font = Font(bold=True)

    for b in bookings:
        guest_type = (
            b.get("relationship") if b.get("seat_holder_type") == "spouse_family"
            else "Alumni" if b["booking_type"] == "seat"
            else ""
        )
        ws.append([
            b["booking_number"],
            b["table_number"],
            "Table" if b["booking_type"] == "table" else "Seat",
            guest_type,
            b["price_label"],
            b["first_name"],
            b["last_name"],
            b["phone"],
            b.get("email") or "",
            b.get("batch") or "",
            b["birth_date"],
            b.get("allergies") or "",
            b.get("relationship") or "",
            b.get("partner_first_name") or "",
            b.get("partner_last_name") or "",
            "Yes" if b["is_grad"] else "No",
            b["status"],
            b.get("slip_filename") or "",
            b.get("receipt_filename") or "",
            b["created_at"],
        ])

    for col_cells in ws.columns:
        max_len = max((len(str(c.value)) for c in col_cells if c.value is not None), default=8)
        ws.column_dimensions[col_cells[0].column_letter].width = min(max_len + 2, 40)

    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)

    filename = f"kkvs-bookings-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.xlsx"
    return send_file(
        buffer,
        as_attachment=True,
        download_name=filename,
        mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )


@app.route("/api/admin/alumni", methods=["GET"])
def get_alumni_roster():
    require_admin()
    db = get_db()
    count = db.execute("SELECT COUNT(*) AS c FROM alumni_roster").fetchone()["c"]
    return jsonify({"count": count})


@app.route("/api/admin/alumni/upload", methods=["POST"])
def upload_alumni_roster():
    require_admin()
    file = request.files.get("file")
    if not file or not file.filename:
        return jsonify({"error": "No file uploaded"}), 400

    try:
        raw_text = file.read().decode("utf-8", errors="ignore")
    except Exception:
        return jsonify({"error": "Could not read file"}), 400

    names = parse_roster_file(raw_text)
    if not names:
        return jsonify({"error": "No names found in file"}), 400

    db = get_db()
    db.execute("DELETE FROM alumni_roster")
    seen = set()
    for full_name in names:
        normalized = normalize_name(full_name)
        if not normalized or normalized in seen:
            continue
        seen.add(normalized)
        db.execute(
            "INSERT INTO alumni_roster (full_name, normalized_name) VALUES (?, ?)",
            (full_name, normalized),
        )
    db.commit()

    return jsonify({"ok": True, "count": len(seen)})


@app.route("/api/admin/alumni", methods=["DELETE"])
def clear_alumni_roster():
    require_admin()
    db = get_db()
    db.execute("DELETE FROM alumni_roster")
    db.commit()
    return jsonify({"ok": True, "count": 0})


@app.route("/api/bookings/<booking_number>/status", methods=["PATCH"])
def update_status(booking_number):
    require_admin()
    payload = request.get_json(force=True) or {}
    new_status = (payload.get("status") or "").strip()
    if not new_status:
        return jsonify({"error": "Status is required"}), 400

    db = get_db()
    row = db.execute("SELECT * FROM bookings WHERE booking_number = ?", (booking_number,)).fetchone()
    if not row:
        return jsonify({"error": "Booking not found"}), 404

    old_status = row["status"]
    db.execute("UPDATE bookings SET status = ? WHERE booking_number = ?", (new_status, booking_number))
    db.commit()

    if new_status == "Payment Confirmed" and old_status != "Payment Confirmed":
        booking = dict(row)
        booking["status"] = new_status
        send_confirmation_email(booking)

    return jsonify({"ok": True, "status": new_status})


@app.route("/api/bookings/<booking_number>", methods=["DELETE"])
def delete_booking(booking_number):
    require_admin()
    db = get_db()
    row = db.execute(
        "SELECT slip_filename, receipt_filename FROM bookings WHERE booking_number = ?", (booking_number,)
    ).fetchone()
    if not row:
        return jsonify({"error": "Booking not found"}), 404

    db.execute("DELETE FROM bookings WHERE booking_number = ?", (booking_number,))
    db.commit()

    if row["slip_filename"]:
        slip_path = SLIPS_DIR / row["slip_filename"]
        if slip_path.exists():
            slip_path.unlink()

    if row["receipt_filename"]:
        receipt_path = RECEIPTS_DIR / row["receipt_filename"]
        if receipt_path.exists():
            receipt_path.unlink()

    return jsonify({"ok": True})


@app.route("/api/bookings/<booking_number>/receipt", methods=["POST"])
def upload_receipt(booking_number):
    require_admin()
    db = get_db()
    row = db.execute("SELECT id, receipt_filename FROM bookings WHERE booking_number = ?", (booking_number,)).fetchone()
    if not row:
        return jsonify({"error": "Booking not found"}), 404

    file = request.files.get("receipt")
    if not file or not file.filename:
        return jsonify({"error": "No file uploaded"}), 400

    ext = Path(file.filename).suffix.lower()
    if ext not in ALLOWED_SLIP_EXTENSIONS:
        return jsonify({"error": "Unsupported file type"}), 400

    # Replace any previous receipt file for this booking.
    if row["receipt_filename"]:
        old_path = RECEIPTS_DIR / row["receipt_filename"]
        if old_path.exists():
            old_path.unlink()

    safe_name = f"{booking_number}-{int(time.time())}{ext}"
    file.save(str(RECEIPTS_DIR / safe_name))

    db.execute("UPDATE bookings SET receipt_filename = ? WHERE booking_number = ?", (safe_name, booking_number))
    db.commit()

    return jsonify({"ok": True})


@app.route("/api/slips/<filename>")
def get_slip(filename):
    require_admin()
    return send_from_directory(str(SLIPS_DIR), filename)


@app.route("/api/receipts/<filename>")
def get_receipt(filename):
    """Admins can view any receipt; payers can download their own by phone."""
    if not session.get("is_admin"):
        normalized = normalize_phone(request.args.get("phone", ""))
        if not normalized:
            abort(401)

        db = get_db()
        row = db.execute(
            "SELECT phone FROM bookings WHERE receipt_filename = ?", (filename,)
        ).fetchone()
        if not row or normalize_phone(row["phone"]) != normalized:
            abort(404)

    return send_from_directory(str(RECEIPTS_DIR), filename)


@app.errorhandler(401)
def unauthorized(_e):
    return jsonify({"error": "Unauthorized"}), 401


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 8934))
    app.run(host="0.0.0.0", port=port, debug=False)
