# -*- coding: utf-8 -*-
"""OK ITHAKI V249396 conversation security modes.

Stores only the selected conversation mode, auto-delete metadata and data-only
wake-token hashes. It does not store shared AES keys or Android private keys.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import hashlib, secrets, sqlite3, threading, time

VERSION = "V249396-professional-security-modes-tor-ready"
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DB_PATH = DATA_DIR / "communication_security_v249396.sqlite3"
ALLOWED_MODES = {"full_e2ee", "standard_aes", "no_encryption"}
ALLOWED_TTLS = {0,1,10,30,60,300,3600,86400,259200}
ALLOWED_STORAGE = {"encrypted_queue", "zero_online_only"}
ZERO_QUEUE_TTL_SECONDS = 120
_ZERO_LOCK = threading.RLock()
_ZERO_QUEUE: dict[tuple[str,str,str], list[dict[str,Any]]] = {}

class CommunicationSecurityError(RuntimeError): pass

def _now() -> float: return time.time()
def _clean(v: Any, limit: int = 500) -> str: return str(v or "").replace("\x00"," ").strip()[:limit]
def normalize_mode(v: Any) -> str:
    v=_clean(v,40)
    return v if v in ALLOWED_MODES else "full_e2ee"
def normalize_ttl(v: Any) -> int:
    try: n=int(v)
    except Exception: n=86400
    return n if n in ALLOWED_TTLS else 86400
def normalize_storage(v: Any) -> str:
    v=_clean(v,40)
    return v if v in ALLOWED_STORAGE else "encrypted_queue"

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")
    con.executescript("""
    CREATE TABLE IF NOT EXISTS conversation_security_v249396(
      room_id TEXT NOT NULL, channel TEXT NOT NULL, mode TEXT NOT NULL,
      changed_by TEXT NOT NULL, plaintext_ack INTEGER NOT NULL DEFAULT 0,
      updated_at REAL NOT NULL, PRIMARY KEY(room_id,channel)
    );
    CREATE TABLE IF NOT EXISTS communication_message_meta_v249396(
      message_id TEXT PRIMARY KEY, room_id TEXT NOT NULL, channel TEXT NOT NULL,
      sender_id TEXT NOT NULL, recipient_id TEXT NOT NULL, mode 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,
      read_at REAL NOT NULL DEFAULT 0, expires_at REAL NOT NULL DEFAULT 0,
      wake_token_hash TEXT NOT NULL DEFAULT ''
    );
    CREATE INDEX IF NOT EXISTS idx_comm_meta_expiry_v249396 ON communication_message_meta_v249396(expires_at);
    CREATE TABLE IF NOT EXISTS communication_wake_v249396(
      event_id TEXT PRIMARY KEY, room_id TEXT NOT NULL, channel TEXT NOT NULL,
      sender_id TEXT NOT NULL, recipient_id TEXT NOT NULL, mode TEXT NOT NULL,
      message_type TEXT NOT NULL, nonce_hash TEXT NOT NULL, created_at REAL NOT NULL,
      expires_at REAL NOT NULL
    );
    """)
    con.commit();return con

def get_mode(room_id: str, channel: str) -> dict[str,Any]:
    room_id=_clean(room_id or 'ithaki-phone-test',120);channel=_clean(channel,400)
    with _connect() as con:
        row=con.execute("SELECT mode,changed_by,plaintext_ack,updated_at FROM conversation_security_v249396 WHERE room_id=? AND channel=?",(room_id,channel)).fetchone()
    if not row:return {"ok":True,"version":VERSION,"mode":"full_e2ee","changed_by":"","plaintext_ack":False,"updated_at":0}
    return {"ok":True,"version":VERSION,"mode":normalize_mode(row['mode']),"changed_by":str(row['changed_by'] or ''),"plaintext_ack":bool(row['plaintext_ack']),"updated_at":float(row['updated_at'] or 0)}

def save_mode(room_id: str, channel: str, device_id: str, mode: str, plaintext_ack: bool=False) -> dict[str,Any]:
    room_id=_clean(room_id or 'ithaki-phone-test',120);channel=_clean(channel,400);device_id=_clean(device_id,180);mode=normalize_mode(mode)
    if not device_id:raise CommunicationSecurityError('Λείπει η ταυτότητα συσκευής.')
    if mode=='no_encryption' and plaintext_ack is not True:raise CommunicationSecurityError('Χρειάζεται ρητή επιβεβαίωση για περιεχόμενο χωρίς κρυπτογράφηση.')
    t=_now()
    with _connect() as con:
        con.execute("""INSERT INTO conversation_security_v249396(room_id,channel,mode,changed_by,plaintext_ack,updated_at)
        VALUES(?,?,?,?,?,?) ON CONFLICT(room_id,channel) DO UPDATE SET mode=excluded.mode,changed_by=excluded.changed_by,plaintext_ack=excluded.plaintext_ack,updated_at=excluded.updated_at""",(room_id,channel,mode,device_id,1 if plaintext_ack else 0,t));con.commit()
    return {"ok":True,"version":VERSION,"mode":mode,"changed_by":device_id,"plaintext_ack":bool(plaintext_ack),"updated_at":t}

def new_wake(room_id:str,channel:str,sender_id:str,recipient_id:str,mode:str,message_type:str) -> tuple[str,str]:
    event_id='cmsg_'+secrets.token_hex(12);nonce=secrets.token_urlsafe(32);h=hashlib.sha256(nonce.encode()).hexdigest();t=_now()
    with _connect() as con:
        con.execute("INSERT INTO communication_wake_v249396(event_id,room_id,channel,sender_id,recipient_id,mode,message_type,nonce_hash,created_at,expires_at) VALUES(?,?,?,?,?,?,?,?,?,?)",(event_id,_clean(room_id,120),_clean(channel,400),_clean(sender_id,180),_clean(recipient_id,180),normalize_mode(mode),_clean(message_type,40),h,t,t+7*24*3600));con.execute("DELETE FROM communication_wake_v249396 WHERE expires_at<?",(t,));con.commit()
    return event_id,nonce

def bind_wake_to_message(event_id:str,message_id:str) -> None:
    # Event id remains the FCM id. The message id is stored in meta separately.
    return None

def verify_wake(event_id:str,recipient_id:str,nonce:str) -> dict[str,Any]|None:
    h=hashlib.sha256(str(nonce or '').encode()).hexdigest();t=_now()
    with _connect() as con:
        row=con.execute("SELECT * FROM communication_wake_v249396 WHERE event_id=? AND recipient_id=? AND nonce_hash=? AND expires_at>?",(_clean(event_id,180),_clean(recipient_id,180),h,t)).fetchone()
    return dict(row) if row else None

def record_message(message_id:str,room_id:str,channel:str,sender_id:str,recipient_id:str,mode:str,message_type:str,ttl:int,storage_mode:str,wake_hash:str='') -> None:
    t=_now();ttl=normalize_ttl(ttl);storage_mode=normalize_storage(storage_mode)
    with _connect() as con:
        con.execute("""INSERT OR REPLACE INTO communication_message_meta_v249396(message_id,room_id,channel,sender_id,recipient_id,mode,message_type,auto_delete_seconds,storage_mode,created_at,read_at,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),normalize_mode(mode),_clean(message_type,40),ttl,storage_mode,t,0,0,_clean(wake_hash,128)));con.commit()

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

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

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

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

def queue_zero(item:dict[str,Any]) -> None:
    key=(_clean(item.get('room_id'),120),_clean(item.get('channel'),400),_clean(item.get('recipient_id'),180));x=dict(item);x['zero_expires_at']=_now()+ZERO_QUEUE_TTL_SECONDS
    with _ZERO_LOCK:_cleanup_zero(_now());_ZERO_QUEUE.setdefault(key,[]).append(x)
def _cleanup_zero(t:float) -> None:
    for k in list(_ZERO_QUEUE):
        alive=[x for x in _ZERO_QUEUE[k] if float(x.get('zero_expires_at') or 0)>t]
        if alive:_ZERO_QUEUE[k]=alive
        else:_ZERO_QUEUE.pop(k,None)
def pop_zero(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(_now());return list(_ZERO_QUEUE.pop(key,[]))

def diagnostics() -> dict[str,Any]:
    with _connect() as con:
        modes=int(con.execute('SELECT COUNT(*) FROM conversation_security_v249396').fetchone()[0]);meta=int(con.execute('SELECT COUNT(*) FROM communication_message_meta_v249396').fetchone()[0])
    with _ZERO_LOCK:_cleanup_zero(_now());zero=sum(len(v) for v in _ZERO_QUEUE.values())
    return {"ok":True,"version":VERSION,"conversation_modes":modes,"message_meta":meta,"zero_queue_memory_only":zero,"shared_aes_keys_on_server":False,"android_private_keys_on_server":False}
