# retoor <retoor@molodetz.nl>

from __future__ import annotations

import json
from datetime import datetime, timezone

from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError

from devplacepy import config
from devplacepy.database import (
    _in_clause,
    build_pagination,
    conditional_update_row,
    db,
    get_blocked_uids,
    get_table,
    get_users_by_uids,
    resolve_object_url,
    text_search_clause,
)
from devplacepy.services.audit import record as audit
from devplacepy.services.background import background
from devplacepy.services.game.store import (
    conditional_update_farm,
    ensure_farm,
    refund_farm,
)
from devplacepy.utils import create_notification, generate_uid, track_action
from devplacepy.utils.rewards import (
    XP_BATTLE_PART,
    XP_BATTLE_TOP,
    XP_BATTLE_WIN,
    award_rewards,
)

from . import rules

FILTERS = ("active", "ended", "mine")


class WarError(Exception):
    pass


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


def _iso(value: datetime | None = None) -> str:
    return (value or _now()).isoformat()


def _wars():
    return get_table("opinion_wars")


def _fighters():
    return get_table("opinion_war_fighters")


def _events():
    return get_table("opinion_war_events")


def born_live(fields: dict) -> dict:
    stamp = _iso()
    return {
        **fields,
        "created_at": fields.get("created_at") or stamp,
        "updated_at": stamp,
        "deleted_at": None,
        "deleted_by": None,
    }


def faction_label(war: dict, faction: str) -> str:
    return war.get("faction_a") if faction == "a" else war.get("faction_b")


def get_war(war_uid: str) -> dict | None:
    if not war_uid:
        return None
    return _wars().find_one(uid=war_uid, deleted_at=None)


def get_war_for_post(post_uid: str) -> dict | None:
    if not post_uid:
        return None
    return _wars().find_one(post_uid=post_uid, deleted_at=None)


def create_war(
    post_uid: str, user: dict, faction_a: str, faction_b: str, request=None
) -> str | None:
    faction_a = (faction_a or "").strip()
    faction_b = (faction_b or "").strip()
    if not faction_a or not faction_b:
        return None
    if len(faction_a) > rules.FACTION_NAME_MAX or len(faction_b) > rules.FACTION_NAME_MAX:
        return None
    if faction_a.casefold() == faction_b.casefold():
        return None
    if get_war_for_post(post_uid):
        return None
    uid = generate_uid()
    created_at = _iso()
    _wars().insert(
        born_live(
            {
                "uid": uid,
                "post_uid": post_uid,
                "user_uid": user["uid"],
                "faction_a": faction_a,
                "faction_b": faction_b,
                "hp_a": 0,
                "hp_b": 0,
                "leader": "",
                "status": "active",
                "winner": "",
                "created_at": created_at,
                "ends_at": rules.ends_at_for(created_at),
                "resolved_at": "",
            }
        )
    )
    label = f"{faction_a} vs {faction_b}"
    audit.record(
        request,
        "battle.create",
        user=user,
        target_type="battle",
        target_uid=uid,
        target_label=label,
        metadata={"faction_a": faction_a, "faction_b": faction_b},
        summary=f"{user['username']} started Opinion War {label}",
        links=[audit.target("battle", uid, label), audit.parent("post", post_uid)],
    )
    track_action(user["uid"], "battle_create")
    return uid


def join_war(war: dict, user: dict, faction: str, request=None) -> dict:
    war = resolve_if_due(war)
    if not war or war.get("status") != "active":
        raise WarError("This battle has ended.")
    if faction not in ("a", "b"):
        raise WarError("Pick faction a or b.")
    label = faction_label(war, faction)
    fighter = _fighters().find_one(war_uid=war["uid"], user_uid=user["uid"])
    if fighter and fighter.get("deleted_at"):
        _fighters().update(
            {
                "id": fighter["id"],
                "faction": faction,
                "deleted_at": None,
                "deleted_by": None,
                "updated_at": _iso(),
            },
            ["id"],
        )
        kind = "join"
    elif fighter:
        rows = conditional_update_row(
            "opinion_war_fighters",
            fighter["uid"],
            "faction = :faction",
            "faction != :faction AND deleted_at IS NULL",
            {"faction": faction},
        )
        if not rows:
            return war
        kind = "switch"
    else:
        try:
            _fighters().insert(
                born_live(
                    {
                        "uid": generate_uid(),
                        "war_uid": war["uid"],
                        "user_uid": user["uid"],
                        "faction": faction,
                        "hp_a": 0,
                        "hp_b": 0,
                        "fight_count": 0,
                        "last_fight_at": "",
                        "cooldown_notified_at": "",
                    }
                )
            )
        except IntegrityError:
            return join_war(get_war(war["uid"]) or war, user, faction, request)
        kind = "join"
    verb = "defected to" if kind == "switch" else "joined"
    allocate_event(
        war["uid"],
        kind,
        f"{user['username']} {verb} {label}",
        {"faction": faction},
        user["uid"],
    )
    audit.record(
        request,
        f"battle.{kind}",
        user=user,
        target_type="battle",
        target_uid=war["uid"],
        target_label=label,
        metadata={"faction": faction},
        summary=f"{user['username']} {verb} {label}",
        links=[audit.target("battle", war["uid"], label), audit.parent("post", war["post_uid"])],
    )
    return get_war(war["uid"]) or war


def fight(war: dict, user: dict, request=None) -> tuple[dict, int]:
    war = resolve_if_due(war)
    if not war or war.get("status") != "active":
        raise WarError("This battle has ended.")
    fighter = _fighters().find_one(
        war_uid=war["uid"], user_uid=user["uid"], deleted_at=None
    )
    if not fighter:
        raise WarError("Join a faction first.")
    now = _iso()
    previous = fighter.get("last_fight_at") or ""
    claimed = conditional_update_row(
        "opinion_war_fighters",
        fighter["uid"],
        "last_fight_at = :now",
        "(COALESCE(last_fight_at, '') = '' OR last_fight_at <= :cutoff) AND deleted_at IS NULL",
        {"now": now, "cutoff": rules.cooldown_cutoff()},
    )
    if not claimed:
        raise WarError("You can fight once every 24 hours.")
    farm = ensure_farm(user["uid"])
    spent = conditional_update_farm(
        farm["uid"],
        "coins = COALESCE(coins, 0) - :cost",
        "COALESCE(coins, 0) >= :cost",
        {"cost": rules.FIGHT_COST_COINS},
    )
    if not spent:
        _restore_cooldown(fighter["uid"], previous, now)
        raise WarError(
            f"A fight costs {rules.FIGHT_COST_COINS} Code Farm coins - earn some on your farm first."
        )
    faction = "a" if fighter.get("faction") != "b" else "b"
    side = "hp_a" if faction == "a" else "hp_b"
    damage = rules.damage_for(user.get("level"))
    landed = conditional_update_row(
        "opinion_wars",
        war["uid"],
        f"{side} = COALESCE({side}, 0) + :damage",
        "status = 'active' AND ends_at > :now AND deleted_at IS NULL",
        {"damage": damage, "now": now},
    )
    if not landed:
        refund_farm(farm["uid"], rules.FIGHT_COST_COINS)
        _restore_cooldown(fighter["uid"], previous, now)
        resolve_if_due(get_war(war["uid"]))
        raise WarError("The battle ended before your attack landed.")
    conditional_update_row(
        "opinion_war_fighters",
        fighter["uid"],
        f"{side} = COALESCE({side}, 0) + :damage, fight_count = COALESCE(fight_count, 0) + 1",
        "1 = 1",
        {"damage": damage},
    )
    label = faction_label(war, faction)
    fresh = get_war(war["uid"]) or war
    allocate_event(
        war["uid"],
        "fight",
        f"{user['username']} dealt {damage} HP for {label}",
        {
            "faction": faction,
            "damage": damage,
            "hp_a": int(fresh.get("hp_a") or 0),
            "hp_b": int(fresh.get("hp_b") or 0),
        },
        user["uid"],
    )
    _claim_lead(fresh, user)
    track_action(user["uid"], "battle_fight")
    audit.record(
        request,
        "battle.fight",
        user=user,
        target_type="battle",
        target_uid=war["uid"],
        target_label=label,
        metadata={"faction": faction, "damage": damage, "cost": rules.FIGHT_COST_COINS},
        summary=f"{user['username']} dealt {damage} HP for {label}",
        links=[audit.target("battle", war["uid"], label), audit.parent("post", war["post_uid"])],
    )
    return get_war(war["uid"]) or fresh, damage


def _restore_cooldown(fighter_uid: str, previous: str, claimed: str) -> None:
    conditional_update_row(
        "opinion_war_fighters",
        fighter_uid,
        "last_fight_at = :previous",
        "last_fight_at = :claimed",
        {"previous": previous, "claimed": claimed},
    )


def _claim_lead(war: dict, user: dict) -> None:
    new_leader = rules.leader_of(int(war.get("hp_a") or 0), int(war.get("hp_b") or 0))
    if not new_leader:
        return
    rows = conditional_update_row(
        "opinion_wars",
        war["uid"],
        "leader = :leader",
        "COALESCE(leader, '') != :leader AND status = 'active'",
        {"leader": new_leader},
    )
    if not rows:
        return
    label = faction_label(war, new_leader)
    allocate_event(
        war["uid"],
        "lead_change",
        f"{label} took the lead!",
        {
            "faction": new_leader,
            "hp_a": int(war.get("hp_a") or 0),
            "hp_b": int(war.get("hp_b") or 0),
        },
        user["uid"],
    )
    background.submit(
        _notify_fighters,
        war["uid"],
        f"{label} took the lead in an Opinion War you fight in",
        user["uid"],
        user["uid"],
    )


def _notify_fighters(
    war_uid: str, message: str, related_uid: str | None, exclude_uid: str | None
) -> None:
    war = get_war(war_uid)
    if not war:
        return
    url = resolve_object_url("post", war["post_uid"])
    for row in _fighters().find(war_uid=war_uid, deleted_at=None):
        recipient = row.get("user_uid") or ""
        if not recipient or recipient == exclude_uid:
            continue
        create_notification(
            recipient, "battle", message, related_uid or recipient, target_url=url
        )


def allocate_event(
    war_uid: str, kind: str, message: str, payload: dict | None = None, actor_uid: str = ""
) -> dict:
    uid = generate_uid()
    created_at = _iso()
    with db:
        db.query(
            "INSERT INTO opinion_war_events "
            "(uid, war_uid, seq, kind, message, payload, actor_uid, created_at, deleted_at, deleted_by) "
            "SELECT :uid, :war_uid, COALESCE(MAX(seq), 0) + 1, :kind, :message, :payload, "
            ":actor_uid, :created_at, NULL, NULL "
            "FROM opinion_war_events WHERE war_uid = :war_uid",
            uid=uid,
            war_uid=war_uid,
            kind=kind,
            message=message,
            payload=json.dumps(payload or {}, ensure_ascii=False),
            actor_uid=actor_uid or "",
            created_at=created_at,
        )
    row = _events().find_one(uid=uid)
    return _event_dict(row) if row else {
        "seq": 0,
        "kind": kind,
        "message": message,
        "faction": (payload or {}).get("faction", ""),
        "created_at": created_at,
    }


def _event_dict(row: dict) -> dict:
    try:
        payload = json.loads(row.get("payload") or "{}")
    except (TypeError, ValueError):
        payload = {}
    if not isinstance(payload, dict):
        payload = {}
    event = {
        "seq": int(row.get("seq") or 0),
        "kind": row.get("kind") or "",
        "message": row.get("message") or "",
        "faction": payload.get("faction", ""),
        "created_at": row.get("created_at") or "",
    }
    for key in ("hp_a", "hp_b", "damage", "winner"):
        if key in payload:
            event[key] = payload[key]
    return event


def events_for(war_uid: str, after_seq: int = 0, limit: int = rules.EVENT_LIMIT_DEFAULT) -> list[dict]:
    capped = max(1, min(int(limit or 1), rules.EVENT_LIMIT_DEFAULT))
    rows = _events().find(
        war_uid=war_uid,
        seq={">": max(0, int(after_seq or 0))},
        deleted_at=None,
        order_by=["seq"],
        _limit=capped,
    )
    return [_event_dict(dict(row)) for row in rows]


def resolve_if_due(war: dict | None) -> dict | None:
    if not war or war.get("status") != "active":
        return war
    if not rules.is_ended(war.get("ends_at") or ""):
        return war
    return resolve_war(war)


def resolve_war(war: dict) -> dict:
    rows = conditional_update_row(
        "opinion_wars",
        war["uid"],
        "status = 'resolved', resolved_at = :now, "
        "winner = CASE WHEN COALESCE(hp_a, 0) > COALESCE(hp_b, 0) THEN 'a' "
        "WHEN COALESCE(hp_b, 0) > COALESCE(hp_a, 0) THEN 'b' ELSE 'draw' END",
        "status = 'active' AND deleted_at IS NULL",
        {"now": _iso()},
    )
    resolved = get_war(war["uid"]) or war
    if rows:
        _award_and_announce(resolved)
    return resolved


def _award_and_announce(war: dict) -> None:
    winner = war.get("winner") or "draw"
    hp_a = int(war.get("hp_a") or 0)
    hp_b = int(war.get("hp_b") or 0)
    fighters = list(_fighters().find(war_uid=war["uid"], deleted_at=None))
    active = [row for row in fighters if int(row.get("fight_count") or 0) > 0]
    for row in active:
        award_rewards(row["user_uid"], XP_BATTLE_PART)
        if winner in ("a", "b") and row.get("faction") == winner:
            award_rewards(row["user_uid"], XP_BATTLE_WIN)
            track_action(row["user_uid"], "battle_win")
    if winner in ("a", "b") and active:
        top = min(
            active,
            key=lambda row: (
                -(int(row.get("hp_a") or 0) + int(row.get("hp_b") or 0)),
                int(row.get("id") or 0),
            ),
        )
        award_rewards(top["user_uid"], XP_BATTLE_TOP)
    if winner == "a":
        message = f"Battle over: {war['faction_a']} wins {hp_a} to {hp_b}"
    elif winner == "b":
        message = f"Battle over: {war['faction_b']} wins {hp_b} to {hp_a}"
    else:
        message = f"Battle over: a draw at {hp_a} HP each"
    allocate_event(
        war["uid"], "result", message, {"winner": winner, "hp_a": hp_a, "hp_b": hp_b}, ""
    )
    background.submit(_notify_fighters, war["uid"], message, None, None)
    audit.record_system(
        "battle.resolve",
        target_type="battle",
        target_uid=war["uid"],
        target_label=f"{war['faction_a']} vs {war['faction_b']}",
        metadata={"winner": winner, "hp_a": hp_a, "hp_b": hp_b},
        summary=message,
        links=[
            audit.target("battle", war["uid"], f"{war['faction_a']} vs {war['faction_b']}"),
            audit.parent("post", war["post_uid"]),
        ],
    )


def resolve_due_wars(limit: int = 25) -> int:
    if "opinion_wars" not in db.tables:
        return 0
    due = list(
        _wars().find(
            status="active",
            ends_at={"<=": _iso()},
            deleted_at=None,
            order_by=["ends_at"],
            _limit=max(1, limit),
        )
    )
    resolved = 0
    for war in due:
        if resolve_war(dict(war)).get("status") == "resolved":
            resolved += 1
    return resolved


def cooldown_ready_fighters(limit: int = 50) -> list[dict]:
    if "opinion_war_fighters" not in db.tables:
        return []
    rows = db.query(
        "SELECT f.* FROM opinion_war_fighters f "
        "JOIN opinion_wars w ON w.uid = f.war_uid "
        "WHERE f.deleted_at IS NULL AND w.deleted_at IS NULL AND w.status = 'active' "
        "AND COALESCE(f.last_fight_at, '') != '' AND f.last_fight_at <= :cutoff "
        "AND COALESCE(f.cooldown_notified_at, '') != f.last_fight_at "
        "LIMIT :limit",
        cutoff=rules.cooldown_cutoff(),
        limit=max(1, limit),
    )
    return [dict(row) for row in rows]


def notify_cooldown_ready(fighter: dict) -> bool:
    last = fighter.get("last_fight_at") or ""
    rows = conditional_update_row(
        "opinion_war_fighters",
        fighter["uid"],
        "cooldown_notified_at = :last",
        "COALESCE(cooldown_notified_at, '') != :last AND last_fight_at = :last",
        {"last": last},
    )
    if not rows:
        return False
    war = get_war(fighter.get("war_uid") or "")
    if not war:
        return True
    create_notification(
        fighter["user_uid"],
        "battle",
        "Your Fight is ready - return to the battle",
        fighter["user_uid"],
        target_url=resolve_object_url("post", war["post_uid"]),
    )
    return True


def get_wars_by_post_uids(post_uids: list, user: dict | None = None) -> dict:
    uids = [uid for uid in (post_uids or []) if uid]
    if not uids or "opinion_wars" not in db.tables:
        return {}
    placeholders, params = _in_clause(uids)
    wars = [
        dict(row)
        for row in db.query(
            f"SELECT * FROM opinion_wars WHERE post_uid IN ({placeholders}) "
            "AND deleted_at IS NULL",
            **params,
        )
    ]
    if not wars:
        return {}
    serialized = serialize_wars(wars, user)
    return {item["post_uid"]: item for item in serialized}


def get_war_serialized(war: dict | None, user: dict | None = None) -> dict | None:
    if not war:
        return None
    items = serialize_wars([dict(war)], user)
    return items[0] if items else None


def get_war_serialized_for_post(post_uid: str, user: dict | None = None) -> dict | None:
    return get_war_serialized(get_war_for_post(post_uid), user)


def serialize_wars(wars: list[dict], user: dict | None = None) -> list[dict]:
    wars = [resolve_if_due(war) for war in wars]
    war_uids = [war["uid"] for war in wars]
    placeholders, params = _in_clause(war_uids)
    fighters_by_war: dict[str, list[dict]] = {uid: [] for uid in war_uids}
    if "opinion_war_fighters" in db.tables:
        for row in db.query(
            f"SELECT * FROM opinion_war_fighters WHERE war_uid IN ({placeholders}) "
            "AND deleted_at IS NULL",
            **params,
        ):
            fighters_by_war.setdefault(row["war_uid"], []).append(dict(row))
    events_by_war: dict[str, list[dict]] = {uid: [] for uid in war_uids}
    if "opinion_war_events" in db.tables:
        for row in db.query(
            "SELECT * FROM ("
            "SELECT e.*, ROW_NUMBER() OVER (PARTITION BY war_uid ORDER BY seq DESC) AS rn "
            f"FROM opinion_war_events e WHERE war_uid IN ({placeholders}) "
            "AND deleted_at IS NULL) WHERE rn <= :ticker",
            **params,
            ticker=rules.EVENT_TICKER_LIMIT,
        ):
            events_by_war.setdefault(row["war_uid"], []).append(dict(row))
    posts_by_uid: dict[str, dict] = {}
    post_placeholders, post_params = _in_clause([war["post_uid"] for war in wars])
    if "posts" in db.tables:
        for row in db.query(
            f"SELECT uid, slug, title FROM posts WHERE uid IN ({post_placeholders}) "
            "AND deleted_at IS NULL",
            **post_params,
        ):
            posts_by_uid[row["uid"]] = dict(row)
    user_uids = {war["user_uid"] for war in wars}
    for rows in fighters_by_war.values():
        ranked = _ranked(rows)
        for fighter in ranked[: rules.TOP_CONTRIBUTORS]:
            user_uids.add(fighter["user_uid"])
    users = get_users_by_uids(list(user_uids))
    return [
        _serialize_war(
            war,
            _ranked(fighters_by_war.get(war["uid"], [])),
            events_by_war.get(war["uid"], []),
            posts_by_uid.get(war["post_uid"]),
            users,
            user,
        )
        for war in wars
    ]


def _ranked(fighters: list[dict]) -> list[dict]:
    return sorted(
        fighters,
        key=lambda row: (
            -(int(row.get("hp_a") or 0) + int(row.get("hp_b") or 0)),
            int(row.get("id") or 0),
        ),
    )


def _person(users: dict, uid: str) -> dict:
    row = users.get(uid) or {}
    return {
        "uid": uid,
        "username": row.get("username") or "unknown",
        "avatar_seed": row.get("avatar_seed"),
        "level": int(row.get("level") or 1),
    }


def _serialize_war(
    war: dict,
    ranked: list[dict],
    event_rows: list[dict],
    post: dict | None,
    users: dict,
    viewer: dict | None,
) -> dict:
    hp_a = int(war.get("hp_a") or 0)
    hp_b = int(war.get("hp_b") or 0)
    pct_a, pct_b = rules.pct_split(hp_a, hp_b)
    events = sorted((_event_dict(row) for row in event_rows), key=lambda e: -e["seq"])
    last_seq = events[0]["seq"] if events else 0
    status = war.get("status") or "active"
    winner = war.get("winner") or ""
    viewer_row = None
    viewer_uid = (viewer or {}).get("uid") or ""
    if viewer_uid:
        for index, row in enumerate(ranked):
            if row.get("user_uid") == viewer_uid:
                viewer_row = {
                    "faction": row.get("faction") or "",
                    "hp": int(row.get("hp_a") or 0) + int(row.get("hp_b") or 0),
                    "rank": index + 1,
                    "fight_count": int(row.get("fight_count") or 0),
                    "last_fight_at": row.get("last_fight_at") or "",
                    "next_fight_at": rules.cooldown_ready_at(row.get("last_fight_at") or ""),
                    "can_fight": status == "active"
                    and rules.can_fight_at(row.get("last_fight_at") or ""),
                }
                break
    top_contributors = [
        {
            **_person(users, row["user_uid"]),
            "faction": row.get("faction") or "",
            "hp": int(row.get("hp_a") or 0) + int(row.get("hp_b") or 0),
        }
        for row in ranked[: rules.TOP_CONTRIBUTORS]
        if int(row.get("hp_a") or 0) + int(row.get("hp_b") or 0) > 0
    ]
    slug = (post or {}).get("slug") or war["post_uid"]
    return {
        "uid": war["uid"],
        "post_uid": war["post_uid"],
        "post_url": f"/posts/{slug}",
        "post_title": (post or {}).get("title") or "",
        "author": _person(users, war["user_uid"]),
        "faction_a": war.get("faction_a") or "",
        "faction_b": war.get("faction_b") or "",
        "hp_a": hp_a,
        "hp_b": hp_b,
        "pct_a": pct_a,
        "pct_b": pct_b,
        "leader": war.get("leader") or "",
        "fighter_count": len(ranked),
        "status": status,
        "winner": winner,
        "winner_label": faction_label(war, winner) or "" if winner in ("a", "b") else "",
        "created_at": war.get("created_at") or "",
        "ends_at": war.get("ends_at") or "",
        "ends_in": rules.ends_in_label(war.get("ends_at") or ""),
        "resolved_at": war.get("resolved_at") or "",
        "last_seq": last_seq,
        "fight_cost": rules.FIGHT_COST_COINS,
        "top_contributors": top_contributors,
        "recent_events": events,
        "viewer": viewer_row,
    }


def list_wars(
    *,
    viewer: dict | None = None,
    war_filter: str = "active",
    search: str = "",
    page: int = 1,
    per_page: int = config.BATTLES_LIST_PER_PAGE,
) -> tuple[list[dict], dict]:
    table = _wars()
    resolve_due_wars()
    clauses, filters = _list_clauses(table, viewer, war_filter, search)
    if clauses is None:
        return [], build_pagination(1, 0, per_page)
    total = table.count(*clauses, deleted_at=None, **filters)
    pagination = build_pagination(page, total, per_page)
    offset = (pagination["page"] - 1) * pagination["per_page"]
    rows = list(
        table.find(
            *clauses,
            deleted_at=None,
            order_by=["-created_at"],
            _limit=pagination["per_page"],
            _offset=offset,
            **filters,
        )
    )
    return serialize_wars([dict(row) for row in rows], viewer), pagination


def filter_counts(viewer: dict | None, search: str = "") -> dict[str, int]:
    table = _wars()
    counts: dict[str, int] = {}
    for name in FILTERS:
        clauses, filters = _list_clauses(table, viewer, name, search)
        counts[name] = (
            0 if clauses is None else table.count(*clauses, deleted_at=None, **filters)
        )
    return counts


def _joined_war_uids(viewer_uid: str) -> list[str]:
    return [
        row["war_uid"]
        for row in _fighters().find(user_uid=viewer_uid, deleted_at=None)
    ]


def _list_clauses(table, viewer, war_filter, search):
    if not table.exists:
        return None, {}
    viewer_uid = (viewer or {}).get("uid", "")
    columns = table.table.columns
    clauses = []
    filters: dict = {}
    if war_filter == "ended":
        filters["status"] = "resolved"
    elif war_filter == "mine":
        if not viewer_uid:
            return None, {}
        joined = _joined_war_uids(viewer_uid)
        own = columns.user_uid == viewer_uid
        clauses.append(or_(own, columns.uid.in_(joined)) if joined else own)
    else:
        filters["status"] = "active"
    if viewer_uid and war_filter != "mine":
        blocked = get_blocked_uids(viewer_uid)
        if blocked:
            clauses.append(columns.user_uid.notin_(blocked))
    match = text_search_clause(
        table, search, ("faction_a", "faction_b"), author_field="user_uid"
    )
    if match is not None:
        clauses.append(match)
    return clauses, filters
