# retoor <retoor@molodetz.nl>

from __future__ import annotations

import hashlib
import secrets
from datetime import datetime, timedelta, timezone
from typing import Any

from devplacepy.database import get_int_setting, get_table
from devplacepy.utils import generate_uid

CODE_TTL_SETTING = "telegram_code_ttl_minutes"
DEFAULT_CODE_TTL_MINUTES = 60
PAIRINGS = "telegram_pairings"
LINKS = "telegram_links"
OUTBOX = "telegram_outbox"
MAX_OUTBOX_ATTEMPTS = 3
OUTBOX_RETENTION_HOURS = 24


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _iso(value: datetime) -> str:
    return value.isoformat()


def _hash(code: str) -> str:
    return hashlib.sha256(code.encode("utf-8")).hexdigest()


def _parse(value: str) -> datetime:
    parsed = datetime.fromisoformat(value)
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed


def _ttl_minutes() -> int:
    return max(1, get_int_setting(CODE_TTL_SETTING, DEFAULT_CODE_TTL_MINUTES))


def _active_code_hashes() -> set[str]:
    table = get_table(PAIRINGS)
    now = _now()
    active: set[str] = set()
    for row in table.find(used=0):
        try:
            if _parse(row["expires_at"]) > now:
                active.add(row["code_hash"])
        except (ValueError, KeyError, TypeError):
            continue
    return active


def _evict_oldest_active() -> None:
    table = get_table(PAIRINGS)
    oldest = None
    for row in table.find(used=0):
        created = row.get("created_at") or ""
        if oldest is None or created < (oldest.get("created_at") or ""):
            oldest = row
    if oldest is not None:
        table.update({"id": oldest["id"], "used": 1}, ["id"])


def _unique_code() -> str:
    for _ in range(2):
        taken = _active_code_hashes()
        for _ in range(256):
            candidate = f"{secrets.randbelow(10000):04d}"
            if _hash(candidate) not in taken:
                return candidate
        _evict_oldest_active()
    raise RuntimeError("Telegram pairing code pool is exhausted")


def issue_code(user_uid: str) -> dict[str, Any]:
    table = get_table(PAIRINGS)
    for row in table.find(user_uid=user_uid, used=0):
        table.update({"id": row["id"], "used": 1}, ["id"])
    code = _unique_code()
    ttl = _ttl_minutes()
    expires_at = _now() + timedelta(minutes=ttl)
    table.insert(
        {
            "uid": generate_uid(),
            "user_uid": user_uid,
            "code_hash": _hash(code),
            "expires_at": _iso(expires_at),
            "used": 0,
            "created_at": _iso(_now()),
        }
    )
    return {"code": code, "expires_at": _iso(expires_at), "ttl_minutes": ttl}


def verify_code(code: str, chat_id: int, from_id: int) -> dict[str, Any] | None:
    cleaned = code.strip()
    if not (cleaned.isdigit() and len(cleaned) == 4):
        return None
    table = get_table(PAIRINGS)
    match = table.find_one(code_hash=_hash(cleaned), used=0)
    if not match:
        return None
    try:
        if _parse(match["expires_at"]) < _now():
            return None
    except (ValueError, KeyError, TypeError):
        return None
    table.update({"id": match["id"], "used": 1}, ["id"])
    bind(match["user_uid"], chat_id, from_id)
    users = get_table("users")
    return users.find_one(uid=match["user_uid"])


def bind(user_uid: str, chat_id: int, from_id: int) -> None:
    table = get_table(LINKS)
    for row in table.find(user_uid=user_uid):
        table.delete(id=row["id"])
    for row in table.find(chat_id=chat_id):
        table.delete(id=row["id"])
    table.insert(
        {
            "uid": generate_uid(),
            "user_uid": user_uid,
            "chat_id": int(chat_id),
            "from_id": int(from_id),
            "created_at": _iso(_now()),
        }
    )


def link_for_user(user_uid: str) -> dict[str, Any] | None:
    return get_table(LINKS).find_one(user_uid=user_uid)


def user_for_chat(chat_id: int) -> dict[str, Any] | None:
    return get_table(LINKS).find_one(chat_id=int(chat_id))


def unpair(user_uid: str) -> int:
    table = get_table(LINKS)
    removed = 0
    for row in table.find(user_uid=user_uid):
        table.delete(id=row["id"])
        removed += 1
    return removed


def is_paired(user_uid: str) -> bool:
    return link_for_user(user_uid) is not None


def enqueue_outbox(user_uid: str, text: str) -> bool:
    link = link_for_user(user_uid)
    if not link:
        return False
    get_table(OUTBOX).insert(
        {
            "uid": generate_uid(),
            "user_uid": user_uid,
            "chat_id": int(link["chat_id"]),
            "text": text,
            "status": "pending",
            "attempts": 0,
            "created_at": _iso(_now()),
            "sent_at": None,
        }
    )
    return True


def pending_outbox(limit: int) -> list[dict[str, Any]]:
    table = get_table(OUTBOX)
    return list(table.find(status="pending", _limit=limit, order_by=["id"]))


def mark_outbox_sent(row_id: int) -> None:
    get_table(OUTBOX).update(
        {"id": row_id, "status": "sent", "sent_at": _iso(_now())}, ["id"]
    )


def mark_outbox_failed(row_id: int, attempts: int) -> None:
    table = get_table(OUTBOX)
    attempts += 1
    status = "failed" if attempts >= MAX_OUTBOX_ATTEMPTS else "pending"
    table.update({"id": row_id, "status": status, "attempts": attempts}, ["id"])


def prune_outbox() -> int:
    table = get_table(OUTBOX)
    cutoff = _iso(_now() - timedelta(hours=OUTBOX_RETENTION_HOURS))
    removed = 0
    for row in table.find(status="sent"):
        if (row.get("created_at") or "") < cutoff:
            table.delete(id=row["id"])
            removed += 1
    for row in table.find(status="failed"):
        if (row.get("created_at") or "") < cutoff:
            table.delete(id=row["id"])
            removed += 1
    return removed
