from __future__ import annotations

from typing import Any, Callable

from fastapi import HTTPException, Query

VERSION = "V249400-friend-identity-response-consistency"


def install_friend_identity_v249390(
    app,
    *,
    one: Callable,
    rows: Callable,
    execute: Callable,
    now: Callable,
    push_notify_devices: Callable,
    person_lookup: Callable,
    friend_request_handler: Callable,
    ws_notify: Callable | None = None,
):
    """Install strict friend-request identity routes.

    Rules:
    - IDs, never names/avatars, are authoritative.
    - Names and avatars are resolved from the server people table.
    - Only the recipient may accept/reject a pending request.
    - Outgoing requests never expose accept/reject controls.
    - An old local device ID is accepted only when nickname and non-empty avatar both match the current server profile.
    """

    def clean(value: Any, limit: int = 160) -> str:
        return str(value or "").strip()[:limit]

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

    def parse_bool(value: Any) -> bool:
        if isinstance(value, bool):
            return value
        if isinstance(value, (int, float)) and value in (0, 1):
            return bool(value)
        text = clean(value, 24).casefold()
        if text in {"1", "true", "yes", "accept", "accepted", "approve", "ok"}:
            return True
        if text in {"0", "false", "no", "reject", "rejected", "deny", "decline"}:
            return False
        raise HTTPException(400, "accepted must be a real boolean")

    def person(room_id: str, device_id: str, fallback: str = "Μέλος") -> dict[str, Any]:
        p = dict(person_lookup(room_id, device_id, fallback) or {})
        did = clean(p.get("device_id") or p.get("id") or device_id, 160)
        return {
            "device_id": did,
            "id": did,
            "nickname": clean(p.get("nickname") or p.get("name") or fallback or "Μέλος", 120),
            "avatar": clean(p.get("avatar") or p.get("photo") or "", 120000),
            "last_seen": float(p.get("last_seen") or 0),
        }

    def same_profile_alias(room_id: str, current_id: str, alias_id: str) -> bool:
        current_id = clean(current_id)
        alias_id = clean(alias_id)
        if not current_id or not alias_id:
            return False
        if current_id == alias_id:
            return True
        cur = person(room_id, current_id, "")
        old = person(room_id, alias_id, "")
        cur_name = norm_name(cur.get("nickname"))
        old_name = norm_name(old.get("nickname"))
        if not cur_name or not old_name or cur_name in {"μέλος", "member"} or old_name in {"μέλος", "member"}:
            return False
        if cur_name != old_name:
            return False
        cur_avatar = clean(cur.get("avatar"), 120000)
        old_avatar = clean(old.get("avatar"), 120000)
        # V249390: do not bind two members only because they share a nickname.
        # Old-device repair is permitted only when both server profiles have the
        # same non-empty avatar as well as the same non-generic nickname.
        if not cur_avatar or not old_avatar or cur_avatar != old_avatar:
            return False
        return True

    def allowed_ids(room_id: str, current_id: str, aliases_raw: Any) -> list[str]:
        current_id = clean(current_id)
        values = [current_id]
        if isinstance(aliases_raw, (list, tuple, set)):
            values.extend(clean(x) for x in aliases_raw)
        else:
            values.extend(clean(x) for x in clean(aliases_raw, 1000).split(","))
        out: list[str] = []
        for value in values:
            if not value or value in out:
                continue
            if value == current_id or same_profile_alias(room_id, current_id, value):
                out.append(value)
            if len(out) >= 4:
                break
        return out or ([current_id] if current_id else [])

    def notify(room_id: str) -> None:
        if ws_notify is None:
            return
        try:
            ws_notify(room_id)
        except Exception:
            pass

    @app.get("/api/social/friend/notifications_v249390")
    def friend_notifications_v249390(
        room_id: str = Query("ithaki-phone-test"),
        device_id: str = Query(""),
        aliases: str = Query(""),
        limit: int = Query(100, ge=1, le=300),
    ):
        room_id = clean(room_id, 120) or "ithaki-phone-test"
        device_id = clean(device_id)
        if not device_id:
            return {"ok": True, "version": VERSION, "items": [], "counts": {"incoming": 0, "outgoing": 0}}
        ids = allowed_ids(room_id, device_id, aliases)
        placeholders = ",".join("?" for _ in ids)
        sql = f"""
            SELECT id,type,status,from_id,from_name,to_id,to_name,created_at,updated_at
            FROM social_events
            WHERE room_id=? AND type='contact' AND status='pending'
              AND (from_id IN ({placeholders}) OR to_id IN ({placeholders}))
            ORDER BY updated_at DESC, created_at DESC
            LIMIT ?
        """
        params = [room_id, *ids, *ids, int(limit)]
        result = []
        seen = set()
        for row in rows(sql, tuple(params)):
            d = dict(row)
            from_id = clean(d.get("from_id"))
            to_id = clean(d.get("to_id"))
            if not from_id or not to_id or from_id == to_id:
                continue
            if to_id in ids and from_id not in ids:
                relation = "incoming" if to_id == device_id else "incoming_alias"
                peer_id = from_id
                fallback = clean(d.get("from_name"), 120) or "Μέλος"
                can_respond = True
            elif from_id in ids and to_id not in ids:
                relation = "outgoing" if from_id == device_id else "outgoing_alias"
                peer_id = to_id
                fallback = clean(d.get("to_name"), 120) or "Μέλος"
                can_respond = False
            else:
                continue
            key = (clean(d.get("id")), relation, peer_id)
            if key in seen:
                continue
            seen.add(key)
            peer = person(room_id, peer_id, fallback)
            result.append({
                "id": clean(d.get("id")),
                "event_id": clean(d.get("id")),
                "type": "contact",
                "status": "pending",
                "relation": relation,
                "can_accept": can_respond,
                "can_reject": can_respond,
                "peer_id": peer_id,
                "peer_name": peer["nickname"],
                "peer_avatar": peer["avatar"],
                "peer": peer,
                "from_id": from_id,
                "to_id": to_id,
                "created_at": float(d.get("created_at") or 0),
                "updated_at": float(d.get("updated_at") or 0),
                "old_device_repair": relation == "incoming_alias",
            })
        incoming = sum(1 for item in result if item["can_accept"])
        outgoing = len(result) - incoming
        return {
            "ok": True,
            "version": VERSION,
            "device_id": device_id,
            "accepted_device_candidates": ids,
            "items": result,
            "counts": {"incoming": incoming, "outgoing": outgoing, "total": len(result)},
            "policy": "Only incoming requests show accept/reject. Avatar and name are resolved by peer_id on the server.",
        }

    @app.post("/api/social/friend/request_v249390")
    def friend_request_v249390(payload: dict):
        payload = dict(payload or {})
        room_id = clean(payload.get("room_id"), 120) or "ithaki-phone-test"
        from_id = clean(payload.get("from_id"))
        to_id = clean(payload.get("to_id"))
        if not from_id or not to_id:
            raise HTTPException(400, "from_id and to_id required")
        if from_id == to_id:
            raise HTTPException(400, "cannot send request to yourself")
        source = person(room_id, from_id, clean(payload.get("from_name"), 120) or "Μέλος")
        target = person(room_id, to_id, clean(payload.get("to_name"), 120) or "Μέλος")
        payload.update({
            "room_id": room_id,
            "from_id": from_id,
            "from_name": source["nickname"],
            "to_id": to_id,
            "to_name": target["nickname"],
        })
        result = dict(friend_request_handler(payload) or {})
        result.update({
            "version": VERSION,
            "from_profile": source,
            "to_profile": target,
            "identity_binding": "device_id_authoritative_server_profile",
        })
        return result

    @app.post("/api/social/friend/respond_v249390")
    def friend_respond_v249390(payload: dict):
        payload = dict(payload or {})
        room_id = clean(payload.get("room_id"), 120) or "ithaki-phone-test"
        event_id = clean(payload.get("id") or payload.get("event_id"))
        device_id = clean(payload.get("device_id"))
        accepted = parse_bool(payload.get("accepted"))
        if not event_id or not device_id:
            raise HTTPException(400, "id and device_id required")
        ids = allowed_ids(room_id, device_id, payload.get("device_aliases") or payload.get("aliases") or [])
        event = one(
            "SELECT * FROM social_events WHERE room_id=? AND id=? AND type='contact' LIMIT 1",
            (room_id, event_id),
        )
        if not event:
            raise HTTPException(404, "friend request not found")
        event = dict(event)
        status = clean(event.get("status"), 24) or "pending"
        from_id = clean(event.get("from_id"))
        to_id = clean(event.get("to_id"))
        if status != "pending":
            peer_id = from_id if to_id in ids else to_id if from_id in ids else ""
            peer = person(room_id, peer_id, "Μέλος") if peer_id else None
            return {
                "ok": True, "version": VERSION, "id": event_id, "status": status,
                "accepted": status == "accepted", "already_done": True,
                "friend_id": peer_id if status == "accepted" else "",
                "friend": peer if status == "accepted" else None,
                "communication_allowed": status == "accepted",
            }
        if from_id in ids:
            raise HTTPException(403, "the sender cannot accept or reject their own outgoing request")
        if to_id not in ids:
            raise HTTPException(403, "only the intended recipient can respond")
        if from_id == device_id:
            raise HTTPException(400, "cannot create friendship with yourself")

        current = person(room_id, device_id, clean(event.get("to_name"), 120) or "Μέλος")
        peer = person(room_id, from_id, clean(event.get("from_name"), 120) or "Μέλος")
        ts = float(now())
        new_status = "accepted" if accepted else "rejected"
        changed = execute(
            """
            UPDATE social_events
            SET to_id=?, to_name=?, status=?, updated_at=?
            WHERE room_id=? AND id=? AND type='contact' AND status='pending'
            """,
            (device_id, current["nickname"], new_status, ts, room_id, event_id),
        )
        # Close duplicate pending cards for the same verified pair, in either direction.
        try:
            placeholders = ",".join("?" for _ in ids)
            execute(
                f"""UPDATE social_events SET status='rejected',updated_at=?
                    WHERE room_id=? AND type='contact' AND status='pending' AND id<>?
                      AND ((from_id=? AND to_id IN ({placeholders}))
                        OR (to_id=? AND from_id IN ({placeholders})))""",
                (ts, room_id, event_id, from_id, *ids, from_id, *ids),
            )
        except Exception:
            pass
        try:
            push_notify_devices(
                room_id,
                [from_id],
                "OK ITHAKI",
                f"{current['nickname']} {'αποδέχτηκε' if accepted else 'απέρριψε'} το αίτημα φιλίας",
                {"kind": "contact", "event_id": event_id, "status": new_status, "room_id": room_id, "peer_id": device_id},
            )
        except Exception:
            pass
        notify(room_id)
        return {
            "ok": True,
            "version": VERSION,
            "id": event_id,
            "status": new_status,
            "accepted": accepted,
            "friend_id": from_id if accepted else "",
            "friend": peer if accepted else None,
            "responder": current,
            "device_repair": to_id != device_id,
            "old_to_id": to_id if to_id != device_id else "",
            "new_to_id": device_id,
            "only_recipient_can_respond": True,
            "rows_changed": int(changed or 0) if isinstance(changed, int) else 1,
        }

    @app.get("/api/social/friend/state_v249390")
    def friend_state_v249390(
        room_id: str = Query("ithaki-phone-test"),
        device_id: str = Query(""),
        peer_id: str = Query(""),
        aliases: str = Query(""),
    ):
        room_id = clean(room_id, 120) or "ithaki-phone-test"
        device_id = clean(device_id)
        peer_id = clean(peer_id)
        peer = person(room_id, peer_id, "Μέλος") if peer_id else person(room_id, "", "Μέλος")
        if not device_id or not peer_id:
            return {"ok": True, "version": VERSION, "state": "profile_required", "accepted": False, "communication_allowed": False, "peer": peer}
        if device_id == peer_id:
            return {"ok": True, "version": VERSION, "state": "self", "accepted": False, "communication_allowed": False, "peer": peer}
        ids = allowed_ids(room_id, device_id, aliases)
        placeholders = ",".join("?" for _ in ids)
        accepted = one(
            f"""
            SELECT * FROM social_events
            WHERE room_id=? AND type='contact' AND status='accepted'
              AND ((from_id IN ({placeholders}) AND to_id=?) OR (to_id IN ({placeholders}) AND from_id=?))
            ORDER BY updated_at DESC LIMIT 1
            """,
            (room_id, *ids, peer_id, *ids, peer_id),
        )
        migrated = False
        if accepted:
            accepted = dict(accepted)
            from_id = clean(accepted.get("from_id"))
            to_id = clean(accepted.get("to_id"))
            if from_id in ids and from_id != device_id:
                execute("UPDATE social_events SET from_id=?, updated_at=? WHERE id=? AND status='accepted'", (device_id, float(now()), clean(accepted.get("id"))))
                migrated = True
            elif to_id in ids and to_id != device_id:
                execute("UPDATE social_events SET to_id=?, updated_at=? WHERE id=? AND status='accepted'", (device_id, float(now()), clean(accepted.get("id"))))
                migrated = True
        outgoing = one(
            f"SELECT * FROM social_events WHERE room_id=? AND type='contact' AND status='pending' AND from_id IN ({placeholders}) AND to_id=? ORDER BY updated_at DESC LIMIT 1",
            (room_id, *ids, peer_id),
        )
        incoming = one(
            f"SELECT * FROM social_events WHERE room_id=? AND type='contact' AND status='pending' AND to_id IN ({placeholders}) AND from_id=? ORDER BY updated_at DESC LIMIT 1",
            (room_id, *ids, peer_id),
        )
        state = "accepted" if accepted else "pending_incoming" if incoming else "pending_outgoing" if outgoing else "none"
        return {
            "ok": True,
            "version": VERSION,
            "state": state,
            "accepted": bool(accepted),
            "blocked": False,
            "pending_outgoing": bool(outgoing),
            "pending_incoming": bool(incoming),
            "communication_allowed": bool(accepted),
            "event_id": clean((accepted or incoming or outgoing or {}).get("id")),
            "peer_id": peer_id,
            "peer": peer,
            "accepted_device_candidates": ids,
            "accepted_identity_migrated_to_current_device": migrated,
            "policy": "Friendship is keyed by member IDs; names and avatars are display-only.",
        }

    @app.get("/api/diagnostics/friends_v249390")
    def diagnostics_friends_v249390(
        room_id: str = Query("ithaki-phone-test"),
        device_id: str = Query(""),
    ):
        room_id = clean(room_id, 120) or "ithaki-phone-test"
        device_id = clean(device_id)
        def count(sql: str, params=()):
            try:
                row = one(sql, params)
                return int((row or {}).get("n") or 0)
            except Exception:
                return -1
        return {
            "ok": True,
            "version": VERSION,
            "device_id": device_id,
            "counts": {
                "pending_incoming": count("SELECT COUNT(*) n FROM social_events WHERE room_id=? AND type='contact' AND status='pending' AND to_id=?", (room_id, device_id)) if device_id else 0,
                "pending_outgoing": count("SELECT COUNT(*) n FROM social_events WHERE room_id=? AND type='contact' AND status='pending' AND from_id=?", (room_id, device_id)) if device_id else 0,
                "accepted_for_device": count("SELECT COUNT(*) n FROM social_events WHERE room_id=? AND type='contact' AND status='accepted' AND (from_id=? OR to_id=?)", (room_id, device_id, device_id)) if device_id else 0,
                "self_rows": count("SELECT COUNT(*) n FROM social_events WHERE room_id=? AND type='contact' AND from_id=to_id", (room_id,)),
            },
            "rules": {
                "only_recipient_can_accept_or_reject": True,
                "outgoing_request_has_no_accept_button": True,
                "avatar_resolved_by_peer_id": True,
                "reject_string_is_not_truthy_accept": True,
                "legacy_local_only_acceptance_disabled_in_v249390_ui": True,
                "old_device_alias_requires_same_nonempty_avatar": True,
            },
        }
