# -*- coding: utf-8 -*-
"""OK ITHAKI V249389 secure direct-message foundation.

This module is additive and stores only public device keys, security settings,
message expiry metadata and wake-token hashes in a separate SQLite database.
It never stores plaintext message content or private keys.
"""
from __future__ import annotations

from pathlib import Path
from typing import Any
import base64
import hashlib
import hmac
import json
import secrets
import sqlite3
import threading
import time

VERSION = "V249389-secure-dm-android-foundation"
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DB_PATH = DATA_DIR / "secure_messaging_v249389.sqlite3"
MAX_PUBLIC_KEY_CHARS = 8192
MAX_ENVELOPE_CHARS = 2_500_000
ALLOWED_TTLS = {0, 1, 10, 30, 60, 300, 3600, 86400, 259200}
ALLOWED_STORAGE = {"encrypted_queue", "zero_online_only"}
ALLOWED_VOICE_MASK = {"none", "light", "deep", "high", "robotic"}
ZERO_QUEUE_TTL_SECONDS = 120
UNREAD_QUEUE_TTL_SECONDS = 7 * 24 * 60 * 60

_ZERO_LOCK = threading.RLock()
_ZERO_QUEUE: dict[tuple[str, str, str], list[dict[str, Any]]] = {}


class SecureMessagingError(RuntimeError):
    pass


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


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


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


def _ensure_schema(con: sqlite3.Connection) -> None:
    con.executescript(
        """
        CREATE TABLE IF NOT EXISTS secure_device_keys_v249389(
            room_id TEXT NOT NULL,
            device_id TEXT NOT NULL,
            public_key_b64 TEXT NOT NULL,
            key_fingerprint TEXT NOT NULL,
            algorithm TEXT NOT NULL,
            created_at REAL NOT NULL,
            updated_at REAL NOT NULL,
            revoked_at REAL NOT NULL DEFAULT 0,
            PRIMARY KEY(room_id, device_id)
        );
        CREATE TABLE IF NOT EXISTS secure_settings_v249389(
            room_id TEXT NOT NULL,
            device_id TEXT NOT NULL,
            auto_delete_seconds INTEGER NOT NULL DEFAULT 86400,
            storage_mode TEXT NOT NULL DEFAULT 'encrypted_queue',
            screenshot_block INTEGER NOT NULL DEFAULT 1,
            recording_block INTEGER NOT NULL DEFAULT 1,
            voice_mask_mode TEXT NOT NULL DEFAULT 'light',
            voice_mask_live_calls INTEGER NOT NULL DEFAULT 0,
            updated_at REAL NOT NULL,
            PRIMARY KEY(room_id, device_id)
        );
        CREATE TABLE IF NOT EXISTS secure_message_meta_v249389(
            message_id TEXT PRIMARY KEY,
            room_id TEXT NOT NULL,
            channel TEXT NOT NULL,
            sender_id TEXT NOT NULL,
            recipient_id TEXT NOT NULL,
            message_type TEXT NOT NULL DEFAULT 'message',
            auto_delete_seconds INTEGER NOT NULL DEFAULT 86400,
            storage_mode TEXT NOT NULL DEFAULT 'encrypted_queue',
            created_at REAL NOT NULL,
            delivered_at REAL NOT NULL DEFAULT 0,
            read_at REAL NOT NULL DEFAULT 0,
            expires_at REAL NOT NULL DEFAULT 0,
            unread_queue_expires_at REAL NOT NULL DEFAULT 0,
            wake_token_hash TEXT NOT NULL DEFAULT ''
        );
        CREATE INDEX IF NOT EXISTS idx_secure_meta_recipient_v249389
          ON secure_message_meta_v249389(room_id, recipient_id, channel, created_at);
        CREATE INDEX IF NOT EXISTS idx_secure_meta_expiry_v249389
          ON secure_message_meta_v249389(expires_at, unread_queue_expires_at);
        CREATE TABLE IF NOT EXISTS secure_call_wake_v249389(
            event_id TEXT PRIMARY KEY,
            room_id TEXT NOT NULL,
            recipient_id TEXT NOT NULL,
            caller_id TEXT NOT NULL,
            session_id TEXT NOT NULL,
            call_type TEXT NOT NULL,
            wake_token_hash TEXT NOT NULL,
            created_at REAL NOT NULL,
            expires_at REAL NOT NULL
        );
        """
    )
    con.commit()


def _public_key_fingerprint(public_key_b64: str) -> str:
    try:
        raw = base64.b64decode(public_key_b64.encode("ascii"), validate=True)
    except Exception as exc:
        raise SecureMessagingError("Μη έγκυρο δημόσιο κλειδί συσκευής.") from exc
    if len(raw) < 200 or len(raw) > 4096:
        raise SecureMessagingError("Μη αποδεκτό μέγεθος δημόσιου κλειδιού.")
    return hashlib.sha256(raw).hexdigest()


def register_device_key(payload: dict[str, Any]) -> dict[str, Any]:
    room_id = _clean(payload.get("room_id") or "ithaki-phone-test", 120)
    device_id = _clean(payload.get("device_id"), 180)
    public_key_b64 = _clean(payload.get("public_key_b64"), MAX_PUBLIC_KEY_CHARS)
    algorithm = _clean(payload.get("algorithm") or "RSA-OAEP-256+A256GCM", 80)
    if not device_id:
        raise SecureMessagingError("Λείπει η ταυτότητα συσκευής.")
    if algorithm != "RSA-OAEP-256+A256GCM":
        raise SecureMessagingError("Μη υποστηριζόμενος αλγόριθμος.")
    fingerprint = _public_key_fingerprint(public_key_b64)
    t = _now()
    with _connect() as con:
        con.execute(
            """
            INSERT INTO secure_device_keys_v249389(
                room_id,device_id,public_key_b64,key_fingerprint,algorithm,created_at,updated_at,revoked_at
            ) VALUES(?,?,?,?,?,?,?,0)
            ON CONFLICT(room_id,device_id) DO UPDATE SET
              public_key_b64=excluded.public_key_b64,
              key_fingerprint=excluded.key_fingerprint,
              algorithm=excluded.algorithm,
              updated_at=excluded.updated_at,
              revoked_at=0
            """,
            (room_id, device_id, public_key_b64, fingerprint, algorithm, t, t),
        )
        con.commit()
    return {
        "ok": True,
        "version": VERSION,
        "registered": True,
        "algorithm": algorithm,
        "key_fingerprint": fingerprint,
    }


def get_device_key(room_id: str, device_id: str) -> dict[str, Any] | None:
    room_id = _clean(room_id or "ithaki-phone-test", 120)
    device_id = _clean(device_id, 180)
    with _connect() as con:
        row = con.execute(
            """
            SELECT public_key_b64,key_fingerprint,algorithm,updated_at
            FROM secure_device_keys_v249389
            WHERE room_id=? AND device_id=? AND revoked_at=0
            """,
            (room_id, device_id),
        ).fetchone()
    return dict(row) if row else None


def normalize_settings(payload: dict[str, Any]) -> dict[str, Any]:
    try:
        ttl = int(payload.get("auto_delete_seconds", 86400))
    except Exception:
        ttl = 86400
    if ttl not in ALLOWED_TTLS:
        ttl = 86400
    storage_mode = _clean(payload.get("storage_mode") or "encrypted_queue", 40)
    if storage_mode not in ALLOWED_STORAGE:
        storage_mode = "encrypted_queue"
    voice_mask_mode = _clean(payload.get("voice_mask_mode") or "light", 20)
    if voice_mask_mode not in ALLOWED_VOICE_MASK:
        voice_mask_mode = "light"
    return {
        "auto_delete_seconds": ttl,
        "storage_mode": storage_mode,
        "screenshot_block": bool(payload.get("screenshot_block", True)),
        "recording_block": bool(payload.get("recording_block", True)),
        "voice_mask_mode": voice_mask_mode,
        # Live-call voice masking is intentionally false until a native realtime pipeline is proven.
        "voice_mask_live_calls": False,
    }


def save_settings(payload: dict[str, Any]) -> dict[str, Any]:
    room_id = _clean(payload.get("room_id") or "ithaki-phone-test", 120)
    device_id = _clean(payload.get("device_id"), 180)
    if not device_id:
        raise SecureMessagingError("Λείπει η ταυτότητα συσκευής.")
    settings = normalize_settings(payload)
    t = _now()
    with _connect() as con:
        con.execute(
            """
            INSERT INTO secure_settings_v249389(
              room_id,device_id,auto_delete_seconds,storage_mode,screenshot_block,
              recording_block,voice_mask_mode,voice_mask_live_calls,updated_at
            ) VALUES(?,?,?,?,?,?,?,?,?)
            ON CONFLICT(room_id,device_id) DO UPDATE SET
              auto_delete_seconds=excluded.auto_delete_seconds,
              storage_mode=excluded.storage_mode,
              screenshot_block=excluded.screenshot_block,
              recording_block=excluded.recording_block,
              voice_mask_mode=excluded.voice_mask_mode,
              voice_mask_live_calls=excluded.voice_mask_live_calls,
              updated_at=excluded.updated_at
            """,
            (
                room_id,
                device_id,
                settings["auto_delete_seconds"],
                settings["storage_mode"],
                1 if settings["screenshot_block"] else 0,
                1 if settings["recording_block"] else 0,
                settings["voice_mask_mode"],
                0,
                t,
            ),
        )
        con.commit()
    return {"ok": True, "version": VERSION, "settings": settings, "updated_at": t}


def get_settings(room_id: str, device_id: str) -> dict[str, Any]:
    room_id = _clean(room_id or "ithaki-phone-test", 120)
    device_id = _clean(device_id, 180)
    with _connect() as con:
        row = con.execute(
            """
            SELECT auto_delete_seconds,storage_mode,screenshot_block,recording_block,
                   voice_mask_mode,voice_mask_live_calls,updated_at
            FROM secure_settings_v249389 WHERE room_id=? AND device_id=?
            """,
            (room_id, device_id),
        ).fetchone()
    if not row:
        settings = normalize_settings({})
        return {"ok": True, "version": VERSION, "settings": settings, "updated_at": 0}
    return {
        "ok": True,
        "version": VERSION,
        "settings": {
            "auto_delete_seconds": int(row["auto_delete_seconds"] or 0),
            "storage_mode": str(row["storage_mode"] or "encrypted_queue"),
            "screenshot_block": bool(row["screenshot_block"]),
            "recording_block": bool(row["recording_block"]),
            "voice_mask_mode": str(row["voice_mask_mode"] or "light"),
            "voice_mask_live_calls": bool(row["voice_mask_live_calls"]),
        },
        "updated_at": float(row["updated_at"] or 0),
    }


def validate_envelope(envelope: str, room_id: str, channel: str, sender_id: str, recipient_id: str) -> dict[str, Any]:
    if not envelope or len(envelope) > MAX_ENVELOPE_CHARS:
        raise SecureMessagingError("Το κρυπτογραφημένο πακέτο είναι κενό ή πολύ μεγάλο.")
    try:
        data = json.loads(envelope)
    except Exception as exc:
        raise SecureMessagingError("Μη έγκυρο κρυπτογραφημένο πακέτο.") from exc
    required = ("v", "alg", "nonce", "ciphertext", "wrapped_peer", "wrapped_self", "aad")
    if any(not data.get(key) for key in required):
        raise SecureMessagingError("Το κρυπτογραφημένο πακέτο είναι ελλιπές.")
    if int(data.get("v") or 0) != 1 or str(data.get("alg") or "") != "RSA-OAEP-256+A256GCM":
        raise SecureMessagingError("Μη υποστηριζόμενη έκδοση κρυπτογράφησης.")
    try:
        aad = json.loads(str(data.get("aad") or "{}"))
    except Exception as exc:
        raise SecureMessagingError("Μη έγκυρα μεταδεδομένα κρυπτογράφησης.") from exc
    expected = {
        "room_id": _clean(room_id, 120),
        "channel": _clean(channel, 400),
        "sender_id": _clean(sender_id, 180),
        "recipient_id": _clean(recipient_id, 180),
    }
    for key, value in expected.items():
        if _clean(aad.get(key), 400) != value:
            raise SecureMessagingError("Τα μεταδεδομένα κρυπτογράφησης δεν συμφωνούν με τη συνομιλία.")
    return data


def new_wake_token() -> tuple[str, str]:
    token = secrets.token_urlsafe(32)
    return token, hashlib.sha256(token.encode("utf-8")).hexdigest()


def record_persistent_message(
    message_id: str,
    room_id: str,
    channel: str,
    sender_id: str,
    recipient_id: str,
    message_type: str,
    auto_delete_seconds: int,
    storage_mode: str,
    wake_token_hash: str,
) -> None:
    t = _now()
    with _connect() as con:
        con.execute(
            """
            INSERT OR REPLACE INTO secure_message_meta_v249389(
              message_id,room_id,channel,sender_id,recipient_id,message_type,
              auto_delete_seconds,storage_mode,created_at,delivered_at,read_at,
              expires_at,unread_queue_expires_at,wake_token_hash
            ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
            """,
            (
                _clean(message_id, 180),
                _clean(room_id, 120),
                _clean(channel, 400),
                _clean(sender_id, 180),
                _clean(recipient_id, 180),
                _clean(message_type or "message", 40),
                int(auto_delete_seconds),
                _clean(storage_mode, 40),
                t,
                0,
                0,
                0,
                t + UNREAD_QUEUE_TTL_SECONDS,
                _clean(wake_token_hash, 128),
            ),
        )
        con.commit()


def queue_zero_message(item: dict[str, Any]) -> None:
    key = (_clean(item.get("room_id"), 120), _clean(item.get("channel"), 400), _clean(item.get("recipient_id"), 180))
    now = _now()
    prepared = dict(item)
    prepared["zero_expires_at"] = now + ZERO_QUEUE_TTL_SECONDS
    with _ZERO_LOCK:
        _cleanup_zero_locked(now)
        _ZERO_QUEUE.setdefault(key, []).append(prepared)


def _cleanup_zero_locked(now_value: float) -> None:
    dead = []
    for key, items in _ZERO_QUEUE.items():
        alive = [x for x in items if float(x.get("zero_expires_at") or 0) > now_value]
        if alive:
            _ZERO_QUEUE[key] = alive
        else:
            dead.append(key)
    for key in dead:
        _ZERO_QUEUE.pop(key, None)


def pop_zero_messages(room_id: str, channel: str, recipient_id: str) -> list[dict[str, Any]]:
    key = (_clean(room_id, 120), _clean(channel, 400), _clean(recipient_id, 180))
    with _ZERO_LOCK:
        _cleanup_zero_locked(_now())
        items = list(_ZERO_QUEUE.pop(key, []))
    return items


def mark_read(message_id: str, reader_id: str) -> dict[str, Any]:
    message_id = _clean(message_id, 180)
    reader_id = _clean(reader_id, 180)
    t = _now()
    with _connect() as con:
        row = con.execute(
            "SELECT recipient_id,auto_delete_seconds,read_at FROM secure_message_meta_v249389 WHERE message_id=?",
            (message_id,),
        ).fetchone()
        if not row:
            return {"ok": True, "found": False}
        if str(row["recipient_id"] or "") != reader_id:
            raise SecureMessagingError("Μόνο ο παραλήπτης μπορεί να σημειώσει ανάγνωση.")
        ttl = int(row["auto_delete_seconds"] or 0)
        read_at = float(row["read_at"] or 0) or t
        expires_at = read_at + ttl if ttl > 0 else 0
        con.execute(
            """
            UPDATE secure_message_meta_v249389
            SET delivered_at=CASE WHEN delivered_at=0 THEN ? ELSE delivered_at END,
                read_at=?,expires_at=?
            WHERE message_id=?
            """,
            (t, read_at, expires_at, message_id),
        )
        con.commit()
    return {"ok": True, "found": True, "read_at": read_at, "expires_at": expires_at, "auto_delete_seconds": ttl}


def expired_message_ids() -> list[str]:
    t = _now()
    with _connect() as con:
        rows = con.execute(
            """
            SELECT message_id FROM secure_message_meta_v249389
            WHERE (expires_at>0 AND expires_at<=?)
               OR (read_at=0 AND unread_queue_expires_at>0 AND unread_queue_expires_at<=?)
            LIMIT 500
            """,
            (t, t),
        ).fetchall()
    return [str(row["message_id"]) for row in rows]


def delete_meta(message_ids: list[str]) -> None:
    ids = [_clean(x, 180) for x in message_ids if _clean(x, 180)]
    if not ids:
        return
    with _connect() as con:
        con.executemany("DELETE FROM secure_message_meta_v249389 WHERE message_id=?", [(x,) for x in ids])
        con.commit()


def meta_for_messages(message_ids: list[str]) -> dict[str, dict[str, Any]]:
    ids = [_clean(x, 180) for x in message_ids if _clean(x, 180)]
    if not ids:
        return {}
    marks = ",".join(["?"] * len(ids))
    with _connect() as con:
        rows = con.execute(
            f"""
            SELECT message_id,recipient_id,message_type,auto_delete_seconds,storage_mode,
                   created_at,delivered_at,read_at,expires_at
            FROM secure_message_meta_v249389 WHERE message_id IN ({marks})
            """,
            tuple(ids),
        ).fetchall()
    return {str(row["message_id"]): dict(row) for row in rows}


def verify_wake_token(event_id: str, recipient_id: str, wake_token: str) -> dict[str, Any] | None:
    event_id = _clean(event_id, 180)
    recipient_id = _clean(recipient_id, 180)
    token_hash = hashlib.sha256(str(wake_token or "").encode("utf-8")).hexdigest()
    with _connect() as con:
        row = con.execute(
            """
            SELECT message_id,room_id,channel,sender_id,recipient_id,message_type,created_at
            FROM secure_message_meta_v249389
            WHERE message_id=? AND recipient_id=? AND wake_token_hash=?
            """,
            (event_id, recipient_id, token_hash),
        ).fetchone()
    return dict(row) if row else None


def create_call_wake(room_id: str, recipient_id: str, caller_id: str, session_id: str, call_type: str) -> tuple[str, str]:
    event_id = "scall_" + secrets.token_hex(12)
    token, token_hash = new_wake_token()
    t = _now()
    with _connect() as con:
        con.execute(
            """
            INSERT INTO secure_call_wake_v249389(
              event_id,room_id,recipient_id,caller_id,session_id,call_type,wake_token_hash,created_at,expires_at
            ) VALUES(?,?,?,?,?,?,?,?,?)
            """,
            (
                event_id,
                _clean(room_id, 120),
                _clean(recipient_id, 180),
                _clean(caller_id, 180),
                _clean(session_id, 180),
                "voice" if str(call_type) == "voice" else "video",
                token_hash,
                t,
                t + 120,
            ),
        )
        con.execute("DELETE FROM secure_call_wake_v249389 WHERE expires_at<?", (t,))
        con.commit()
    return event_id, token


def verify_call_wake(event_id: str, recipient_id: str, wake_token: str) -> dict[str, Any] | None:
    event_id = _clean(event_id, 180)
    recipient_id = _clean(recipient_id, 180)
    token_hash = hashlib.sha256(str(wake_token or "").encode("utf-8")).hexdigest()
    t = _now()
    with _connect() as con:
        row = con.execute(
            """
            SELECT event_id,room_id,recipient_id,caller_id,session_id,call_type,created_at,expires_at
            FROM secure_call_wake_v249389
            WHERE event_id=? AND recipient_id=? AND wake_token_hash=? AND expires_at>?
            """,
            (event_id, recipient_id, token_hash, t),
        ).fetchone()
    return dict(row) if row else None


def diagnostics() -> dict[str, Any]:
    with _connect() as con:
        keys = int(con.execute("SELECT COUNT(*) FROM secure_device_keys_v249389 WHERE revoked_at=0").fetchone()[0])
        settings = int(con.execute("SELECT COUNT(*) FROM secure_settings_v249389").fetchone()[0])
        pending = int(con.execute("SELECT COUNT(*) FROM secure_message_meta_v249389").fetchone()[0])
    with _ZERO_LOCK:
        _cleanup_zero_locked(_now())
        zero_pending = sum(len(v) for v in _ZERO_QUEUE.values())
    return {
        "ok": True,
        "version": VERSION,
        "device_public_keys": keys,
        "device_settings": settings,
        "secure_message_meta": pending,
        "zero_storage_pending_memory_only": zero_pending,
        "private_keys_on_server": False,
        "plaintext_content_on_server": False,
        "voice_mask_live_calls_active": False,
    }
