#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""OK ITHAKI V249375 public experience service.

Provides:
- professional anonymous Wall,
- public-surface soft moderation,
- explicit exemption for 1:1 personal DMs whose channel starts with ``dm:``.

The moderation audit never stores the original raw text. It stores only a
SHA-256 hash, the masked text and the matched rule names.
"""
from __future__ import annotations

from pathlib import Path
from typing import Any, Iterable
import hashlib
import json
import re
import sqlite3
import time
import unicodedata
import uuid

BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DB_PATH = DATA_DIR / "community_suite_v249375.sqlite3"
MAIN_DB = DATA_DIR / "ithaki_full_relay_pro.sqlite3"
VERSION = "V249431-wall-thread-replies"

DEFAULT_RULES = [
    "μαλακ*", "ηλιθ*", "βλακ*", "χαζ*", "σκατ*", "γαμω", "γαμησ*",
    "πουστ*", "καριολ*", "μουν*", "παπαρ*", "κοπαν*",
    "idiot", "stupid", "moron", "shut up", "fuck*", "shit*",
    "asshole", "bitch", "bastard",
]

PUBLIC_WRITE_PATHS = {
    "/api/flow/send",
    "/api/polls/create",
    "/api/profile/register",
    "/api/profile/public/save",
    "/api/groups/create", "/api/groups/rename",
    "/api/rooms/create", "/api/rooms/rename",
    "/api/broadcast/create", "/api/broadcast/send",
    "/api/bottle/drop", "/api/bottle/reply", "/api/bottle/thread/send",
    "/api/lighthouse/light", "/api/lighthouse/light_v249342",
    "/api/lighthouse/light_v249344", "/api/lighthouse/light_v249345",
    "/api/feedback/send",
    "/api/admin/flow/post", "/api/admin/host/post",
    "/api/live/server_relay/comment_v249289",
    "/api/feed/v24863/comments",
    "/api/wall/post_v249342", "/api/wall/post_v249344",
    "/api/wall/post_v249345",
}

TEXT_KEYS = {
    "text", "body", "content", "caption", "comment", "comments", "title",
    "question", "answer", "answers", "option", "options", "name",
    "nickname", "bio", "description", "reason", "message", "note",
    "notes", "summary", "subject",
}


def _now() -> float:
    return time.time()


def _clean(value: Any, limit: int = 5000) -> str:
    return str(value or "").replace("\x00", " ")[:limit]


def _hash(value: Any) -> str:
    return hashlib.sha256(_clean(value, 10000).encode("utf-8", "ignore")).hexdigest()


def _conn() -> sqlite3.Connection:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    con = sqlite3.connect(str(DB_PATH), timeout=30)
    con.row_factory = sqlite3.Row
    con.execute("PRAGMA journal_mode=WAL")
    con.execute("PRAGMA synchronous=NORMAL")
    con.execute("PRAGMA busy_timeout=30000")
    return con


def init_db() -> None:
    with _conn() as con:
        con.executescript(
            """
            CREATE TABLE IF NOT EXISTS wall_posts_v249375(
              post_id TEXT PRIMARY KEY,
              room_id TEXT NOT NULL,
              owner_hash TEXT NOT NULL,
              text TEXT NOT NULL,
              moderation_changed INTEGER NOT NULL DEFAULT 0,
              moderation_hits TEXT NOT NULL DEFAULT '[]',
              created_at REAL NOT NULL,
              deleted_at REAL NOT NULL DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_wall375_room_time
              ON wall_posts_v249375(room_id,created_at DESC);

            CREATE TABLE IF NOT EXISTS wall_comments_v249375(
              comment_id TEXT PRIMARY KEY,
              post_id TEXT NOT NULL,
              parent_id TEXT NOT NULL DEFAULT '',
              owner_hash TEXT NOT NULL,
              text TEXT NOT NULL,
              moderation_changed INTEGER NOT NULL DEFAULT 0,
              moderation_hits TEXT NOT NULL DEFAULT '[]',
              created_at REAL NOT NULL,
              deleted_at REAL NOT NULL DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_wall375_comments
              ON wall_comments_v249375(post_id,created_at ASC);

            CREATE TABLE IF NOT EXISTS wall_reactions_v249375(
              post_id TEXT NOT NULL,
              actor_hash TEXT NOT NULL,
              reaction TEXT NOT NULL DEFAULT 'heart',
              created_at REAL NOT NULL,
              PRIMARY KEY(post_id,actor_hash,reaction)
            );

            CREATE TABLE IF NOT EXISTS wall_reports_v249375(
              report_id TEXT PRIMARY KEY,
              post_id TEXT NOT NULL,
              reporter_hash TEXT NOT NULL,
              reason TEXT NOT NULL DEFAULT '',
              created_at REAL NOT NULL
            );

            CREATE TABLE IF NOT EXISTS moderation_events_v249375(
              event_id TEXT PRIMARY KEY,
              surface TEXT NOT NULL,
              room_id TEXT NOT NULL DEFAULT '',
              target_id TEXT NOT NULL DEFAULT '',
              actor_hash TEXT NOT NULL DEFAULT '',
              original_hash TEXT NOT NULL,
              masked_text TEXT NOT NULL,
              hits_json TEXT NOT NULL,
              created_at REAL NOT NULL
            );
            CREATE INDEX IF NOT EXISTS idx_mod375_time
              ON moderation_events_v249375(created_at DESC);
            """
        )
        # Safe migration for existing V249375 databases. No rows are deleted.
        columns = {str(row[1]) for row in con.execute("PRAGMA table_info(wall_comments_v249375)").fetchall()}
        if "parent_id" not in columns:
            con.execute("ALTER TABLE wall_comments_v249375 ADD COLUMN parent_id TEXT NOT NULL DEFAULT ''")
        con.execute("CREATE INDEX IF NOT EXISTS idx_wall375_comment_parent ON wall_comments_v249375(post_id,parent_id,created_at ASC)")
        con.commit()


def _fold(value: str) -> str:
    s = unicodedata.normalize("NFD", str(value or ""))
    s = "".join(ch for ch in s if unicodedata.category(ch) != "Mn")
    s = s.lower().replace("ς", "σ")
    table = str.maketrans(
        {"0": "o", "1": "i", "3": "e", "4": "a", "5": "s",
         "7": "t", "@": "a", "$": "s"}
    )
    s = s.translate(table)
    return re.sub(r"[^0-9a-zα-ω]+", "", s)


def _rules(room_id: str = "ithaki-phone-test") -> list[str]:
    out: list[str] = []
    try:
        if MAIN_DB.exists():
            con = sqlite3.connect(f"file:{MAIN_DB}?mode=ro", uri=True, timeout=5)
            try:
                exists = con.execute(
                    "SELECT 1 FROM sqlite_master WHERE type='table' AND name='moderation_words'"
                ).fetchone()
                if exists:
                    query = (
                        "SELECT word FROM moderation_words "
                        "WHERE room_id=? AND active=1 ORDER BY length(word) DESC"
                    )
                    for (word,) in con.execute(query, (room_id,)):
                        w = _clean(word, 100).strip()
                        if w:
                            out.append(w)
            finally:
                con.close()
    except Exception:
        pass

    out.extend(DEFAULT_RULES)
    seen: set[str] = set()
    clean: list[str] = []
    for word in out:
        key = _fold(word.rstrip("*"))
        if key and key not in seen:
            seen.add(key)
            clean.append(word)
    return clean


def _mask_token(token: str) -> str:
    chars = list(token)
    visible = [i for i, ch in enumerate(chars) if ch.isalnum() or ("Α" <= ch <= "ώ")]
    if len(visible) <= 2:
        return "".join("@" if i in visible else ch for i, ch in enumerate(chars))
    keep = {visible[0], visible[-1]}
    return "".join(ch if i in keep or i not in visible else "@" for i, ch in enumerate(chars))


def _match_token(token: str, rules: Iterable[str]) -> list[str]:
    folded = _fold(token)
    if len(folded) < 3:
        return []
    hits: list[str] = []
    for rule in rules:
        raw = str(rule or "").strip()
        stem = raw.endswith("*")
        core = _fold(raw[:-1] if stem else raw)
        if not core:
            continue
        matched = folded.startswith(core) if stem else folded == core
        if not matched and len(core) >= 5 and abs(len(folded) - len(core)) <= 1:
            diffs = sum(1 for a, b in zip(folded, core) if a != b)
            diffs += abs(len(folded) - len(core))
            matched = diffs <= 1
        if matched:
            hits.append(raw)
    return hits


def moderate_public_text(
    text: Any,
    room_id: str = "ithaki-phone-test",
    surface: str = "public",
    target_id: str = "",
    actor: str = "",
    record: bool = True,
) -> dict[str, Any]:
    raw = _clean(text, 20000)
    if not raw:
        return {"text": "", "changed": False, "hits": []}

    rules = _rules(room_id)
    hits: list[str] = []
    token_re = re.compile(r"[A-Za-zΑ-Ωα-ωΆ-ώΪΫϊϋΐΰ0-9@*$!.#_-]+", re.UNICODE)

    def repl(match: re.Match[str]) -> str:
        token = match.group(0)
        found = _match_token(token, rules)
        if not found:
            return token
        for hit in found:
            if hit not in hits:
                hits.append(hit)
        return _mask_token(token)

    masked = token_re.sub(repl, raw)
    changed = masked != raw

    if changed and record:
        try:
            init_db()
            with _conn() as con:
                con.execute(
                    """
                    INSERT INTO moderation_events_v249375(
                      event_id,surface,room_id,target_id,actor_hash,
                      original_hash,masked_text,hits_json,created_at
                    ) VALUES(?,?,?,?,?,?,?,?,?)
                    """,
                    (
                        "mod375_" + uuid.uuid4().hex,
                        _clean(surface, 60),
                        _clean(room_id, 120),
                        _clean(target_id, 160),
                        _hash(actor) if actor else "",
                        _hash(raw),
                        masked,
                        json.dumps(hits, ensure_ascii=False),
                        _now(),
                    ),
                )
                con.commit()
        except Exception:
            pass

    return {"text": masked, "changed": changed, "hits": hits}


def _moderate_json_string(
    value: str, room_id: str, surface: str, actor: str
) -> tuple[str, bool, list[str]]:
    raw = str(value or "")
    stripped = raw.strip()
    if stripped[:1] in ("{", "["):
        try:
            obj = json.loads(raw)
            obj2, changed, hits = moderate_payload(obj, room_id, surface, actor)
            if changed:
                return json.dumps(obj2, ensure_ascii=False, separators=(",", ":")), True, hits
        except Exception:
            pass
    result = moderate_public_text(raw, room_id, surface, actor=actor)
    return result["text"], bool(result["changed"]), list(result["hits"])


def moderate_payload(
    value: Any,
    room_id: str = "ithaki-phone-test",
    surface: str = "public",
    actor: str = "",
    parent_key: str = "",
) -> tuple[Any, bool, list[str]]:
    changed = False
    hits: list[str] = []

    if isinstance(value, dict):
        out: dict[Any, Any] = {}
        for key_raw, item in value.items():
            key = str(key_raw).lower()
            if key in TEXT_KEYS:
                if isinstance(item, str):
                    new_item, item_changed, item_hits = _moderate_json_string(
                        item, room_id, surface, actor
                    )
                elif isinstance(item, (list, dict)):
                    new_item, item_changed, item_hits = moderate_payload(
                        item, room_id, surface, actor, key
                    )
                else:
                    new_item, item_changed, item_hits = item, False, []
            elif isinstance(item, (dict, list)):
                new_item, item_changed, item_hits = moderate_payload(
                    item, room_id, surface, actor, key
                )
            else:
                new_item, item_changed, item_hits = item, False, []

            out[key_raw] = new_item
            changed = changed or item_changed
            for hit in item_hits:
                if hit not in hits:
                    hits.append(hit)
        return out, changed, hits

    if isinstance(value, list):
        out_list: list[Any] = []
        for item in value:
            if isinstance(item, str) and parent_key in TEXT_KEYS:
                new_item, item_changed, item_hits = _moderate_json_string(
                    item, room_id, surface, actor
                )
            elif isinstance(item, (dict, list)):
                new_item, item_changed, item_hits = moderate_payload(
                    item, room_id, surface, actor, parent_key
                )
            else:
                new_item, item_changed, item_hits = item, False, []
            out_list.append(new_item)
            changed = changed or item_changed
            for hit in item_hits:
                if hit not in hits:
                    hits.append(hit)
        return out_list, changed, hits

    return value, False, []


def is_public_write_path(path: str) -> bool:
    normalized = str(path or "").rstrip("/") or "/"
    if normalized == "/api/messages/send":
        return True
    return normalized in PUBLIC_WRITE_PATHS


class PublicModerationMiddlewareV249375:
    """Mask public JSON text while leaving personal DMs exactly untouched."""

    def __init__(self, app: Any):
        self.app = app

    async def __call__(self, scope: dict, receive: Any, send: Any) -> None:
        if scope.get("type") != "http":
            await self.app(scope, receive, send)
            return
        if scope.get("method", "GET").upper() not in {"POST", "PUT", "PATCH"}:
            await self.app(scope, receive, send)
            return

        path = scope.get("path", "")
        if not is_public_write_path(path):
            await self.app(scope, receive, send)
            return

        headers = {k.lower(): v for k, v in scope.get("headers", [])}
        content_type = headers.get(b"content-type", b"").decode("latin1", "ignore").lower()
        if "application/json" not in content_type:
            await self.app(scope, receive, send)
            return

        chunks: list[bytes] = []
        more = True
        while more:
            message = await receive()
            chunks.append(message.get("body", b""))
            more = bool(message.get("more_body"))
        body = b"".join(chunks)

        async def replay(payload: bytes):
            sent = False

            async def receiver() -> dict:
                nonlocal sent
                if sent:
                    return {"type": "http.request", "body": b"", "more_body": False}
                sent = True
                return {"type": "http.request", "body": payload, "more_body": False}

            return receiver

        if not body or len(body) > 2_000_000:
            receiver = await replay(body)
            await self.app(scope, receiver, send)
            return

        try:
            data = json.loads(body.decode("utf-8"))
        except Exception:
            receiver = await replay(body)
            await self.app(scope, receiver, send)
            return

        # Absolute exemption requested by the owner: 1:1 personal DM only.
        if path == "/api/messages/send" and str((data or {}).get("channel", "")).startswith("dm:"):
            receiver = await replay(body)
            await self.app(scope, receiver, send)
            return

        room_id = "ithaki-phone-test"
        actor = ""
        if isinstance(data, dict):
            room_id = str(data.get("room_id") or room_id)
            actor = str(data.get("sender_id") or data.get("device_id") or "")

        new_data, changed, hits = moderate_payload(data, room_id, path, actor)
        new_body = (
            json.dumps(new_data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
            if changed
            else body
        )

        new_scope = dict(scope)
        new_headers: list[tuple[bytes, bytes]] = []
        for key, value in scope.get("headers", []):
            if key.lower() != b"content-length":
                new_headers.append((key, value))
        new_headers.append((b"content-length", str(len(new_body)).encode("ascii")))
        new_scope["headers"] = new_headers
        receiver = await replay(new_body)

        async def send_wrapper(message: dict) -> None:
            if changed and message.get("type") == "http.response.start":
                response_headers = list(message.get("headers", []))
                response_headers.append((b"x-ithaki-moderated", b"1"))
                response_headers.append(
                    (b"x-ithaki-moderation-hits", str(len(hits)).encode("ascii"))
                )
                message = dict(message)
                message["headers"] = response_headers
            await send(message)

        await self.app(new_scope, receiver, send_wrapper)


def install_public_moderation(app: Any) -> bool:
    try:
        existing = [
            getattr(item, "cls", None).__name__
            for item in getattr(app, "user_middleware", [])
            if getattr(item, "cls", None)
        ]
        if "PublicModerationMiddlewareV249375" not in existing:
            app.add_middleware(PublicModerationMiddlewareV249375)
        return True
    except Exception:
        return False


def _owner_hash(token: Any) -> str:
    return _hash("wall-owner-v249375|" + _clean(token, 500))


def _room(value: Any) -> str:
    return _clean(value, 120).strip() or "ithaki-phone-test"


def _online_estimate() -> int:
    try:
        if not MAIN_DB.exists():
            return 0
        con = sqlite3.connect(f"file:{MAIN_DB}?mode=ro", uri=True, timeout=5)
        try:
            columns = [row[1] for row in con.execute("PRAGMA table_info(people)").fetchall()]
            time_column = next(
                (name for name in ("last_seen", "updated_at", "heartbeat_at") if name in columns),
                None,
            )
            if not time_column:
                return 0
            query = f"SELECT COUNT(*) FROM people WHERE {time_column}>=?"
            return int(con.execute(query, (_now() - 180,)).fetchone()[0])
        finally:
            con.close()
    except Exception:
        return 0


def wall_status() -> dict[str, Any]:
    init_db()
    with _conn() as con:
        posts = int(con.execute(
            "SELECT COUNT(*) FROM wall_posts_v249375 WHERE deleted_at=0"
        ).fetchone()[0])
        comments = int(con.execute(
            "SELECT COUNT(*) FROM wall_comments_v249375 WHERE deleted_at=0"
        ).fetchone()[0])
        reactions = int(con.execute(
            "SELECT COUNT(*) FROM wall_reactions_v249375"
        ).fetchone()[0])
        moderation = int(con.execute(
            "SELECT COUNT(*) FROM moderation_events_v249375"
        ).fetchone()[0])
    return {
        "ok": True,
        "version": VERSION,
        "posts": posts,
        "comments": comments,
        "reactions": reactions,
        "moderation_events": moderation,
        "online": _online_estimate(),
        "personal_dm_moderation": False,
        "public_moderation": True,
        "db": str(DB_PATH),
    }


def wall_list(
    room_id: Any = "ithaki-phone-test",
    owner_token: Any = "",
    limit: int = 100,
) -> dict[str, Any]:
    init_db()
    room_value = _room(room_id)
    owner = _owner_hash(owner_token)
    limit = max(1, min(int(limit or 100), 200))
    with _conn() as con:
        items_raw = con.execute(
            """
            SELECT p.*,
              (SELECT COUNT(*) FROM wall_reactions_v249375 r
               WHERE r.post_id=p.post_id AND r.reaction='heart') hearts,
              (SELECT COUNT(*) FROM wall_comments_v249375 c
               WHERE c.post_id=p.post_id AND c.deleted_at=0) comments
            FROM wall_posts_v249375 p
            WHERE p.room_id=? AND p.deleted_at=0
            ORDER BY p.created_at DESC LIMIT ?
            """,
            (room_value, limit),
        ).fetchall()

    items: list[dict[str, Any]] = []
    for row in items_raw:
        item = dict(row)
        item["mine"] = bool(owner_token) and item.pop("owner_hash", "") == owner
        try:
            item["moderation_hits"] = json.loads(item.get("moderation_hits") or "[]")
        except Exception:
            item["moderation_hits"] = []
        items.append(item)

    return {
        "ok": True,
        "items": items,
        "online": _online_estimate(),
        "room_id": room_value,
        "no_samples": True,
    }


def wall_post(payload: dict[str, Any]) -> dict[str, Any]:
    init_db()
    room_value = _room(payload.get("room_id"))
    owner_token = _clean(payload.get("owner_token"), 500)
    raw = _clean(payload.get("text"), 3000).strip()
    if len(owner_token) < 12:
        return {"ok": False, "note": "Λείπει το ασφαλές τοπικό κλειδί της ανώνυμης δημοσίευσης."}
    if not raw:
        return {"ok": False, "note": "Γράψε πρώτα τη σκέψη σου."}

    moderation = moderate_public_text(
        raw, room_value, "anonymous_wall", actor=owner_token
    )
    post_id = "wall375_" + uuid.uuid4().hex
    created_at = _now()
    with _conn() as con:
        con.execute(
            """
            INSERT INTO wall_posts_v249375(
              post_id,room_id,owner_hash,text,moderation_changed,
              moderation_hits,created_at
            ) VALUES(?,?,?,?,?,?,?)
            """,
            (
                post_id,
                room_value,
                _owner_hash(owner_token),
                moderation["text"],
                1 if moderation["changed"] else 0,
                json.dumps(moderation["hits"], ensure_ascii=False),
                created_at,
            ),
        )
        con.commit()
    return {
        "ok": True,
        "post_id": post_id,
        "text": moderation["text"],
        "moderated": moderation["changed"],
        "hits": moderation["hits"],
        "created_at": created_at,
    }


def wall_react(payload: dict[str, Any]) -> dict[str, Any]:
    init_db()
    post_id = _clean(payload.get("post_id"), 100)
    owner_token = _clean(payload.get("owner_token"), 500)
    reaction = _clean(payload.get("reaction"), 20) or "heart"
    if not post_id or len(owner_token) < 12:
        return {"ok": False, "note": "Λείπει δημοσίευση ή τοπικό κλειδί."}

    actor = _owner_hash(owner_token)
    with _conn() as con:
        exists = con.execute(
            "SELECT 1 FROM wall_reactions_v249375 "
            "WHERE post_id=? AND actor_hash=? AND reaction=?",
            (post_id, actor, reaction),
        ).fetchone()
        if exists:
            con.execute(
                "DELETE FROM wall_reactions_v249375 "
                "WHERE post_id=? AND actor_hash=? AND reaction=?",
                (post_id, actor, reaction),
            )
            active = False
        else:
            con.execute(
                "INSERT OR IGNORE INTO wall_reactions_v249375("
                "post_id,actor_hash,reaction,created_at) VALUES(?,?,?,?)",
                (post_id, actor, reaction, _now()),
            )
            active = True
        con.commit()
        count = int(con.execute(
            "SELECT COUNT(*) FROM wall_reactions_v249375 "
            "WHERE post_id=? AND reaction=?",
            (post_id, reaction),
        ).fetchone()[0])
    return {"ok": True, "active": active, "count": count}


def wall_comments(
    post_id: Any, owner_token: Any = "", limit: int = 200
) -> dict[str, Any]:
    init_db()
    post_id = _clean(post_id, 100)
    owner = _owner_hash(owner_token)
    limit = max(1, min(int(limit or 200), 300))
    with _conn() as con:
        rows = con.execute(
            "SELECT * FROM wall_comments_v249375 "
            "WHERE post_id=? AND deleted_at=0 ORDER BY created_at ASC LIMIT ?",
            (post_id, limit),
        ).fetchall()

    items: list[dict[str, Any]] = []
    for row in rows:
        item = dict(row)
        item["mine"] = bool(owner_token) and item.pop("owner_hash", "") == owner
        try:
            item["moderation_hits"] = json.loads(item.get("moderation_hits") or "[]")
        except Exception:
            item["moderation_hits"] = []
        items.append(item)
    return {"ok": True, "items": items, "post_id": post_id}


def wall_comment(payload: dict[str, Any]) -> dict[str, Any]:
    init_db()
    post_id = _clean(payload.get("post_id"), 100)
    owner_token = _clean(payload.get("owner_token"), 500)
    raw = _clean(payload.get("text"), 1500).strip()
    parent_id = _clean(payload.get("parent_id"), 100)
    if not post_id or len(owner_token) < 12 or not raw:
        return {"ok": False, "note": "Λείπει δημοσίευση, σχόλιο ή τοπικό κλειδί."}

    # Legacy wall posts remain commentable. The post_id is an opaque thread key;
    # V249431 intentionally does not require that it belongs to the newer table.
    if parent_id:
        with _conn() as con:
            parent = con.execute(
                "SELECT 1 FROM wall_comments_v249375 WHERE comment_id=? AND post_id=? AND deleted_at=0",
                (parent_id, post_id),
            ).fetchone()
        if not parent:
            return {"ok": False, "note": "Η απάντηση αναφέρεται σε σχόλιο που δεν υπάρχει."}

    moderation = moderate_public_text(
        raw,
        "ithaki-phone-test",
        "anonymous_wall_comment",
        target_id=post_id,
        actor=owner_token,
    )
    comment_id = "wallc375_" + uuid.uuid4().hex
    created_at = _now()
    with _conn() as con:
        con.execute(
            """
            INSERT INTO wall_comments_v249375(
              comment_id,post_id,parent_id,owner_hash,text,moderation_changed,
              moderation_hits,created_at
            ) VALUES(?,?,?,?,?,?,?,?)
            """,
            (
                comment_id,
                post_id,
                parent_id,
                _owner_hash(owner_token),
                moderation["text"],
                1 if moderation["changed"] else 0,
                json.dumps(moderation["hits"], ensure_ascii=False),
                created_at,
            ),
        )
        con.commit()
    return {
        "ok": True,
        "comment_id": comment_id,
        "parent_id": parent_id,
        "text": moderation["text"],
        "moderated": moderation["changed"],
        "hits": moderation["hits"],
        "created_at": created_at,
    }


def wall_delete(payload: dict[str, Any]) -> dict[str, Any]:
    init_db()
    kind = _clean(payload.get("kind"), 20) or "post"
    target_id = _clean(
        payload.get("target_id") or payload.get("post_id") or payload.get("comment_id"),
        100,
    )
    owner_token = _clean(payload.get("owner_token"), 500)
    if not target_id or len(owner_token) < 12:
        return {"ok": False, "note": "Λείπει στόχος ή τοπικό κλειδί."}

    if kind == "comment":
        table, id_column = "wall_comments_v249375", "comment_id"
    else:
        table, id_column = "wall_posts_v249375", "post_id"

    with _conn() as con:
        cursor = con.execute(
            f"UPDATE {table} SET deleted_at=? "
            f"WHERE {id_column}=? AND owner_hash=? AND deleted_at=0",
            (_now(), target_id, _owner_hash(owner_token)),
        )
        con.commit()
    return {
        "ok": bool(cursor.rowcount),
        "deleted": bool(cursor.rowcount),
        "kind": kind,
        "target_id": target_id,
    }


def wall_report(payload: dict[str, Any]) -> dict[str, Any]:
    init_db()
    post_id = _clean(payload.get("post_id"), 100)
    owner_token = _clean(payload.get("owner_token"), 500)
    reason = _clean(payload.get("reason"), 500).strip() or "Αναφορά μέλους"
    if not post_id:
        return {"ok": False, "note": "Λείπει η δημοσίευση."}

    report_id = "wallrep375_" + uuid.uuid4().hex
    with _conn() as con:
        con.execute(
            "INSERT INTO wall_reports_v249375("
            "report_id,post_id,reporter_hash,reason,created_at) VALUES(?,?,?,?,?)",
            (report_id, post_id, _owner_hash(owner_token), reason, _now()),
        )
        con.commit()
    return {"ok": True, "report_id": report_id, "note": "Η αναφορά καταχωρίστηκε."}


def moderation_check(payload: dict[str, Any]) -> dict[str, Any]:
    text = _clean(payload.get("text"), 20000)
    private = bool(payload.get("private_dm")) or str(payload.get("channel") or "").startswith("dm:")
    if private:
        return {
            "ok": True,
            "text": text,
            "changed": False,
            "hits": [],
            "private_dm_exempt": True,
        }

    result = moderate_public_text(
        text,
        _room(payload.get("room_id")),
        str(payload.get("surface") or "preview"),
        record=False,
    )
    return {"ok": True, **result, "private_dm_exempt": False}


init_db()
