from __future__ import annotations

from typing import Any, Callable

VERSION = "V249400-friendship-consistency"
_GENERIC_NAMES = {"", "μέλος", "μέλοσ", "member", "user", "χρήστης", "χρήστησ"}


def install_friendship_consistency_v249400(*, one: Callable, rows: Callable, execute: Callable,
                                            now: Callable, person_lookup: Callable):
    """Return alias-aware friendship helpers used by lists, messages and calls.

    A legacy device id is treated as the same member only when the people table has
    the same non-generic nickname and the same non-empty avatar. This deliberately
    avoids joining two unrelated users who merely chose the same nickname.
    """
    def clean(value: Any, limit: int = 160) -> str:
        return str(value or "").strip()[:limit]

    def norm(value: Any) -> str:
        return " ".join(clean(value, 120).casefold().split())

    def profile(room_id: str, device_id: str, fallback: str = "Μέλος") -> dict:
        try:
            raw = dict(person_lookup(room_id, device_id, fallback) or {})
        except Exception:
            raw = {}
        did = clean(raw.get("device_id") or raw.get("id") or device_id, 160)
        return {
            "device_id": did,
            "id": did,
            "nickname": clean(raw.get("nickname") or raw.get("name") or fallback or "Μέλος", 120),
            "avatar": clean(raw.get("avatar") or raw.get("photo") or "", 120000),
            "last_seen": float(raw.get("last_seen") or 0),
        }

    def aliases(room_id: str, device_id: str) -> list[str]:
        device_id = clean(device_id)
        if not device_id:
            return []
        current = profile(room_id, device_id, "")
        name = norm(current.get("nickname"))
        avatar = clean(current.get("avatar"), 120000)
        result = [device_id]
        if name in _GENERIC_NAMES or not avatar:
            return result
        try:
            candidates = rows(
                "SELECT device_id,nickname,avatar,last_seen FROM people WHERE room_id=? AND avatar=? ORDER BY last_seen DESC LIMIT 20",
                (room_id, avatar),
            )
        except Exception:
            candidates = []
        for row in candidates:
            d = dict(row)
            candidate = clean(d.get("device_id"))
            if candidate and candidate not in result and norm(d.get("nickname")) == name:
                result.append(candidate)
        return result[:8]

    def canonical_id(room_id: str, device_id: str) -> str:
        ids = aliases(room_id, device_id)
        if not ids:
            return clean(device_id)
        placeholders = ",".join("?" for _ in ids)
        try:
            row = one(
                f"SELECT device_id FROM people WHERE room_id=? AND device_id IN ({placeholders}) ORDER BY last_seen DESC LIMIT 1",
                (room_id, *ids),
            )
            if row:
                return clean(dict(row).get("device_id")) or ids[0]
        except Exception:
            pass
        return ids[0]

    def accepted_row(room_id: str, a: str, b: str):
        aa, bb = aliases(room_id, a), aliases(room_id, b)
        if not aa or not bb or set(aa) & set(bb):
            return None, aa, bb
        pa, pb = ",".join("?" for _ in aa), ",".join("?" for _ in bb)
        sql = f"""
            SELECT * FROM social_events
            WHERE room_id=? AND type='contact' AND status='accepted'
              AND ((from_id IN ({pa}) AND to_id IN ({pb}))
                OR (from_id IN ({pb}) AND to_id IN ({pa})))
            ORDER BY updated_at DESC LIMIT 1
        """
        return one(sql, (room_id, *aa, *bb, *bb, *aa)), aa, bb

    def migrate_row(room_id: str, row: Any, a: str, b: str, aa: list[str], bb: list[str]) -> bool:
        if not row:
            return False
        d = dict(row)
        old_from, old_to = clean(d.get("from_id")), clean(d.get("to_id"))
        if old_from in aa and old_to in bb:
            new_from, new_to = clean(a), clean(b)
        elif old_from in bb and old_to in aa:
            new_from, new_to = clean(b), clean(a)
        else:
            return False
        if not new_from or not new_to or new_from == new_to:
            return False
        if old_from == new_from and old_to == new_to:
            return False
        fp, tp = profile(room_id, new_from), profile(room_id, new_to)
        execute(
            "UPDATE social_events SET from_id=?,from_name=?,to_id=?,to_name=?,updated_at=? WHERE room_id=? AND id=? AND type='contact' AND status='accepted'",
            (new_from, fp["nickname"], new_to, tp["nickname"], float(now()), room_id, clean(d.get("id"))),
        )
        # Keep existing private-message history visible after a verified device-id repair.
        try:
            old_channel = "dm:" + ":".join(sorted([old_from, old_to]))
            new_channel = "dm:" + ":".join(sorted([new_from, new_to]))
            if old_channel != new_channel:
                execute("UPDATE messages SET channel=? WHERE room_id=? AND channel=?", (new_channel, room_id, old_channel))
                execute("UPDATE message_reads SET channel=? WHERE room_id=? AND channel=?", (new_channel, room_id, old_channel))
                execute("UPDATE message_typing SET channel=? WHERE room_id=? AND channel=?", (new_channel, room_id, old_channel))
        except Exception:
            pass
        return True

    def are_friends(room_id: str, a: str, b: str) -> bool:
        try:
            a, b = clean(a, 120), clean(b, 120)
            if not a or not b or a == b:
                return False
            row, aa, bb = accepted_row(room_id, a, b)
            if not row:
                return False
            migrate_row(room_id, row, a, b, aa, bb)
            return True
        except Exception:
            return False

    def friend_rows(room_id: str, device_id: str) -> list[dict]:
        device_id = clean(device_id, 120)
        own = aliases(room_id, device_id)
        if not own:
            return []
        placeholders = ",".join("?" for _ in own)
        try:
            accepted = rows(
                f"SELECT * FROM social_events WHERE room_id=? AND type='contact' AND status='accepted' AND (from_id IN ({placeholders}) OR to_id IN ({placeholders})) ORDER BY updated_at DESC LIMIT 500",
                (room_id, *own, *own),
            )
        except Exception:
            return []
        result, seen = [], set()
        for raw in accepted:
            row = dict(raw)
            from_id, to_id = clean(row.get("from_id")), clean(row.get("to_id"))
            if from_id in own and to_id not in own:
                peer_old, peer_name = to_id, clean(row.get("to_name"), 120)
            elif to_id in own and from_id not in own:
                peer_old, peer_name = from_id, clean(row.get("from_name"), 120)
            else:
                continue
            peer = canonical_id(room_id, peer_old) or peer_old
            if not peer or peer == device_id or peer in seen:
                continue
            if are_friends(room_id, device_id, peer):
                seen.add(peer)
                result.append(profile(room_id, peer, peer_name or "Μέλος"))
        return result

    def diagnostics(room_id: str, device_id: str, peer_id: str = "") -> dict:
        own = aliases(room_id, device_id)
        peer = aliases(room_id, peer_id) if peer_id else []
        return {
            "ok": True,
            "version": VERSION,
            "device_id": clean(device_id),
            "peer_id": clean(peer_id),
            "device_aliases": own,
            "peer_aliases": peer,
            "accepted": are_friends(room_id, device_id, peer_id) if peer_id else False,
            "friend_count": len(friend_rows(room_id, device_id)) if device_id else 0,
            "alias_policy": "same non-generic nickname plus same non-empty avatar",
        }

    return are_friends, friend_rows, diagnostics
