#!/usr/bin/env python3
"""
Strona slubna Weronika & Rafal — backend galerii (VPS, czesc publiczna).

Architektura store-and-forward (zdjecia trzymane na DOMOWYM serwerze):
  1. Gosc skanuje QR -> /upload -> wgrywa zdjecia na VPS (zawsze publiczny).
  2. VPS przetwarza (HEIC->JPEG, orientacja EXIF), tworzy miniature,
     zapisuje pelne zdjecie do lokalnego SPOOLA i oznacza forwarded=0.
  3. Osobny proces forwarder.py dopycha pelne zdjecia na domowy serwer
     przez WireGuard. Po potwierdzeniu: forwarded=1 i (domyslnie) kasuje
     lokalna pelna kopie z VPS — zostaje tylko miniatura + metadane.
  4. Podglad pelnego zdjecia: jesli jest jeszcze lokalnie -> serwuj lokalnie;
     w przeciwnym razie proxuj z domowego serwera przez WireGuard.
     Gdy dom offline i brak lokalnej kopii -> 503 (chwilowo niedostepne).

Dzieki temu: dom = trwale miejsce przechowywania, VPS = bufor + cache
miniatur + metadane, a galeria dziala nawet gdy dom jest offline.

Zmienne srodowiskowe — patrz DEPLOY.md.
"""

import os
import io
import uuid
import sqlite3
from datetime import datetime, timezone
from pathlib import Path

from flask import (
    Flask, request, jsonify, session,
    send_from_directory, send_file, abort, Response,
)
from PIL import Image, ImageOps
import pillow_heif
import qrcode

pillow_heif.register_heif_opener()

# --- sciezki -----------------------------------------------------------------
BASE_DIR   = Path(__file__).resolve().parent
SITE_DIR   = BASE_DIR / "site"
UPLOAD_DIR = BASE_DIR / "uploads"          # spool pelnych zdjec (bufor)
THUMB_DIR  = UPLOAD_DIR / "thumbs"         # miniatury (zostaja na VPS)
DB_PATH    = BASE_DIR / "wr.db"
UPLOAD_DIR.mkdir(exist_ok=True)
THUMB_DIR.mkdir(exist_ok=True)

# --- konfiguracja ------------------------------------------------------------
BASE_URL    = os.environ.get("WR_BASE_URL", "https://weronikairafal.pl").rstrip("/")
ADMIN_PASS  = os.environ.get("WR_ADMIN_PASSWORD", "zmien-to-haslo")
MODERATE    = os.environ.get("WR_MODERATE", "0") == "1"

# domowy serwer (przez WireGuard), np. http://10.50.0.2:8021
HOME_URL    = os.environ.get("WR_HOME_URL", "").rstrip("/")
HOME_SECRET = os.environ.get("WR_HOME_SECRET", "")

MAX_FULL   = 2200
MAX_THUMB  = 600
ALLOWED_MIME_PREFIX = "image/"

app = Flask(__name__, static_folder=str(SITE_DIR), static_url_path="")
app.config.update(
    SECRET_KEY=os.environ.get("WR_SECRET_KEY", os.urandom(32).hex()),
    MAX_CONTENT_LENGTH=30 * 1024 * 1024,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    SESSION_COOKIE_SECURE=BASE_URL.startswith("https://"),
)


# --- baza danych -------------------------------------------------------------
def db():
    conn = sqlite3.connect(DB_PATH, timeout=10)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    with db() as conn:
        conn.execute("PRAGMA journal_mode=WAL")   # wspoldzielenie z forwarderem
        conn.execute(
            """CREATE TABLE IF NOT EXISTS photos (
                id        INTEGER PRIMARY KEY AUTOINCREMENT,
                filename  TEXT NOT NULL,
                thumb     TEXT NOT NULL,
                kto       TEXT,
                approved  INTEGER NOT NULL DEFAULT 1,
                forwarded INTEGER NOT NULL DEFAULT 0,
                created   TEXT NOT NULL
            )"""
        )
        conn.execute(
            "CREATE TABLE IF NOT EXISTS pending_deletes (name TEXT PRIMARY KEY)"
        )


init_db()


# --- przetwarzanie obrazu ----------------------------------------------------
def process_image(file_storage):
    raw = file_storage.read()
    img = Image.open(io.BytesIO(raw))
    img = ImageOps.exif_transpose(img)

    if img.mode in ("RGBA", "LA", "P"):
        bg = Image.new("RGB", img.size, (255, 255, 255))
        img = img.convert("RGBA")
        bg.paste(img, mask=img.split()[-1])
        img = bg
    else:
        img = img.convert("RGB")

    name = uuid.uuid4().hex + ".jpg"

    full = img.copy()
    full.thumbnail((MAX_FULL, MAX_FULL), Image.LANCZOS)
    full.save(UPLOAD_DIR / name, "JPEG", quality=85, optimize=True)

    thumb = img.copy()
    thumb.thumbnail((MAX_THUMB, MAX_THUMB), Image.LANCZOS)
    thumb.save(THUMB_DIR / name, "JPEG", quality=80, optimize=True)

    return name


# --- strony statyczne --------------------------------------------------------
@app.route("/")
def home():
    return send_from_directory(SITE_DIR, "index.html")


@app.route("/upload")
def upload_page():
    return send_from_directory(SITE_DIR, "upload.html")


@app.route("/admin")
def admin_page():
    return send_from_directory(SITE_DIR, "admin.html")


# --- serwowanie zdjec --------------------------------------------------------
@app.route("/t/<path:name>")
def serve_thumb(name):
    if "/" in name or "\\" in name:
        abort(404)
    return send_from_directory(THUMB_DIR, name, max_age=86400)


@app.route("/u/<path:name>")
def serve_full(name):
    """Pelne zdjecie: lokalnie jesli jest w spoolu, inaczej proxy z domu."""
    if "/" in name or "\\" in name:
        abort(404)
    local = UPLOAD_DIR / name
    if local.exists():
        return send_from_directory(UPLOAD_DIR, name, max_age=86400)
    if not HOME_URL:
        abort(404)
    try:
        import requests
        r = requests.get(
            f"{HOME_URL}/get/{name}",
            headers={"X-WR-Secret": HOME_SECRET},
            stream=True, timeout=8,
        )
        if r.status_code != 200:
            abort(404)
        return Response(
            r.iter_content(chunk_size=64 * 1024),
            content_type=r.headers.get("Content-Type", "image/jpeg"),
            headers={"Cache-Control": "public, max-age=86400"},
        )
    except Exception:
        return Response("Zdjecie chwilowo niedostepne.", status=503)


# --- kod QR ------------------------------------------------------------------
@app.route("/qr.png")
def qr_png():
    qr = qrcode.QRCode(box_size=10, border=2)
    qr.add_data(f"{BASE_URL}/upload")
    qr.make(fit=True)
    img = qr.make_image(fill_color="#1C1C1A", back_color="white").convert("RGB")
    buf = io.BytesIO()
    img.save(buf, "PNG")
    buf.seek(0)
    return send_file(buf, mimetype="image/png")


# --- API: zdjecia ------------------------------------------------------------
@app.route("/api/photos")
def api_photos():
    show_all = session.get("admin") or not MODERATE
    with db() as conn:
        q = "SELECT * FROM photos"
        if not show_all:
            q += " WHERE approved=1"
        q += " ORDER BY id DESC"
        rows = conn.execute(q).fetchall()
    return jsonify([
        {
            "id": r["id"],
            "url": f"/u/{r['filename']}",
            "thumb": f"/t/{r['thumb']}",
            "kto": r["kto"] or "",
        }
        for r in rows
    ])


@app.route("/api/upload", methods=["POST"])
def api_upload():
    file = request.files.get("photo")
    if not file or not file.filename:
        return jsonify(error="brak pliku"), 400
    if not (file.mimetype or "").startswith(ALLOWED_MIME_PREFIX):
        return jsonify(error="dozwolone tylko obrazy"), 400

    kto = (request.form.get("kto") or "").strip()[:60]
    try:
        name = process_image(file)
    except Exception:
        return jsonify(error="nie udalo sie przetworzyc zdjecia"), 400

    approved = 0 if MODERATE else 1
    with db() as conn:
        conn.execute(
            "INSERT INTO photos (filename, thumb, kto, approved, forwarded, created) "
            "VALUES (?,?,?,?,0,?)",
            (name, name, kto, approved,
             datetime.now(timezone.utc).isoformat()),
        )
    return jsonify(ok=True)


# --- API: admin --------------------------------------------------------------
@app.route("/api/login", methods=["POST"])
def api_login():
    data = request.get_json(silent=True) or {}
    if data.get("password") == ADMIN_PASS:
        session["admin"] = True
        return jsonify(ok=True)
    return jsonify(error="bledne haslo"), 401


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


@app.route("/api/me")
def api_me():
    return (jsonify(ok=True) if session.get("admin")
            else (jsonify(error="nie zalogowano"), 401))


@app.route("/api/delete", methods=["POST"])
def api_delete():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    data = request.get_json(silent=True) or {}
    pid = data.get("id")
    with db() as conn:
        row = conn.execute("SELECT * FROM photos WHERE id=?", (pid,)).fetchone()
        if not row:
            return jsonify(error="nie ma takiego zdjecia"), 404
        name = row["filename"]
        for p in (UPLOAD_DIR / name, THUMB_DIR / row["thumb"]):
            try:
                p.unlink()
            except FileNotFoundError:
                pass
        conn.execute("DELETE FROM photos WHERE id=?", (pid,))
        if HOME_URL:
            if not _home_delete(name):
                conn.execute(
                    "INSERT OR IGNORE INTO pending_deletes (name) VALUES (?)",
                    (name,),
                )
    return jsonify(ok=True)


@app.route("/api/approve", methods=["POST"])
def api_approve():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    data = request.get_json(silent=True) or {}
    with db() as conn:
        conn.execute("UPDATE photos SET approved=1 WHERE id=?", (data.get("id"),))
    return jsonify(ok=True)


def _home_delete(name):
    try:
        import requests
        r = requests.post(
            f"{HOME_URL}/del/{name}",
            headers={"X-WR-Secret": HOME_SECRET},
            timeout=5,
        )
        return r.status_code in (200, 404)
    except Exception:
        return False




# --- ANKIETA (RSVP / zabawa) -------------------------------------------------
with db() as _c:
    _c.execute("CREATE TABLE IF NOT EXISTS ankiety ("
               "id INTEGER PRIMARY KEY AUTOINCREMENT, dane TEXT, created TEXT)")


@app.route("/api/ankieta", methods=["POST"])
def api_ankieta():
    import json
    data = request.get_json(silent=True) or {}
    if not isinstance(data, dict) or not data:
        return jsonify(error="brak danych"), 400
    with db() as conn:
        conn.execute(
            "INSERT INTO ankiety (dane, created) VALUES (?,?)",
            (json.dumps(data, ensure_ascii=False),
             datetime.now(timezone.utc).isoformat()),
        )
    return jsonify(ok=True)


@app.route("/api/ankieta-list")
def api_ankieta_list():
    import json
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    with db() as conn:
        rows = conn.execute("SELECT * FROM ankiety ORDER BY id DESC").fetchall()
    return jsonify([
        {"id": r["id"], "created": r["created"], "dane": json.loads(r["dane"])}
        for r in rows
    ])


@app.route("/wyniki")
def wyniki_page():
    return send_from_directory(SITE_DIR, "wyniki.html")




@app.route("/api/ankieta-delete", methods=["POST"])
def api_ankieta_delete():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    data = request.get_json(silent=True) or {}
    with db() as conn:
        conn.execute("DELETE FROM ankiety WHERE id=?", (data.get("id"),))
    return jsonify(ok=True)




# --- BINGO (czasy, ranking, podglad admina) ----------------------------------
BINGO_TOTAL = 16

with db() as _c:
    _c.execute("CREATE TABLE IF NOT EXISTS bingo_players ("
               "player_id TEXT PRIMARY KEY, name TEXT, started TEXT, finished TEXT)")
    _c.execute("CREATE TABLE IF NOT EXISTS bingo_events ("
               "id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT, "
               "cell INTEGER, action TEXT, ts TEXT)")


def _bingo_count(conn, pid):
    rows = conn.execute(
        "SELECT cell, action FROM bingo_events WHERE player_id=? ORDER BY id ASC",
        (pid,)).fetchall()
    done = set()
    for r in rows:
        if r["action"] == "check":
            done.add(r["cell"])
        elif r["action"] == "uncheck":
            done.discard(r["cell"])
    return len(done)


@app.route("/api/bingo/click", methods=["POST"])
def api_bingo_click():
    d = request.get_json(silent=True) or {}
    pid = (d.get("player_id") or "").strip()[:64]
    name = (d.get("name") or "").strip()[:60]
    cell = d.get("cell")
    action = d.get("action")
    if not pid or action not in ("check", "uncheck") or not isinstance(cell, int):
        return jsonify(error="zle dane"), 400
    now = datetime.now(timezone.utc).isoformat()
    with db() as conn:
        row = conn.execute("SELECT * FROM bingo_players WHERE player_id=?", (pid,)).fetchone()
        if not row:
            conn.execute("INSERT INTO bingo_players (player_id, name, started, finished) "
                         "VALUES (?,?,?,?)", (pid, name, None, None))
        elif name:
            conn.execute("UPDATE bingo_players SET name=? WHERE player_id=?", (name, pid))
        conn.execute("INSERT INTO bingo_events (player_id, cell, action, ts) "
                     "VALUES (?,?,?,?)", (pid, cell, action, now))
        prow = conn.execute("SELECT started, finished FROM bingo_players WHERE player_id=?",
                            (pid,)).fetchone()
        started, finished = prow["started"], prow["finished"]
        if action == "check" and not started:
            conn.execute("UPDATE bingo_players SET started=? WHERE player_id=?", (now, pid))
            started = now
        count = _bingo_count(conn, pid)
        if count >= BINGO_TOTAL and not finished:
            conn.execute("UPDATE bingo_players SET finished=? WHERE player_id=?", (now, pid))
            finished = now
        elif count < BINGO_TOTAL and finished:
            conn.execute("UPDATE bingo_players SET finished=NULL WHERE player_id=?", (pid,))
            finished = None
    return jsonify(count=count, total=BINGO_TOTAL, started=started, finished=finished)


@app.route("/api/bingo/name", methods=["POST"])
def api_bingo_name():
    d = request.get_json(silent=True) or {}
    pid = (d.get("player_id") or "").strip()[:64]
    name = (d.get("name") or "").strip()[:60]
    if not pid:
        return jsonify(error="brak id"), 400
    with db() as conn:
        conn.execute("UPDATE bingo_players SET name=? WHERE player_id=?", (name, pid))
    return jsonify(ok=True)


@app.route("/api/bingo/standings")
def api_bingo_standings():
    with db() as conn:
        players = conn.execute("SELECT * FROM bingo_players").fetchall()
        out = []
        for p in players:
            cnt = _bingo_count(conn, p["player_id"])
            if cnt == 0:
                continue
            out.append({"name": p["name"] or "Gość", "count": cnt, "total": BINGO_TOTAL,
                        "started": p["started"], "finished": p["finished"]})
    return jsonify(out)


@app.route("/api/bingo/detail")
def api_bingo_detail():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    with db() as conn:
        players = conn.execute("SELECT * FROM bingo_players").fetchall()
        res = []
        for p in players:
            evs = conn.execute("SELECT cell, action, ts FROM bingo_events "
                               "WHERE player_id=? ORDER BY id ASC",
                               (p["player_id"],)).fetchall()
            res.append({"player_id": p["player_id"], "name": p["name"] or "Gość", "started": p["started"],
                        "finished": p["finished"], "count": _bingo_count(conn, p["player_id"]),
                        "total": BINGO_TOTAL,
                        "events": [{"cell": e["cell"], "action": e["action"], "ts": e["ts"]}
                                   for e in evs]})
    return jsonify(res)


@app.route("/api/bingo/reset", methods=["POST"])
def api_bingo_reset():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    with db() as conn:
        conn.execute("DELETE FROM bingo_events")
        conn.execute("DELETE FROM bingo_players")
    return jsonify(ok=True)


@app.route("/bingo-wyniki")
def bingo_wyniki_page():
    return send_from_directory(SITE_DIR, "bingo-wyniki.html")




@app.route("/api/bingo/delete-player", methods=["POST"])
def api_bingo_delete_player():
    if not session.get("admin"):
        return jsonify(error="brak dostepu"), 403
    d = request.get_json(silent=True) or {}
    pid = d.get("player_id")
    if not pid:
        return jsonify(error="brak id"), 400
    with db() as conn:
        conn.execute("DELETE FROM bingo_events WHERE player_id=?", (pid,))
        conn.execute("DELETE FROM bingo_players WHERE player_id=?", (pid,))
    return jsonify(ok=True)


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8011, debug=True)
