"""
Kommunikasjon (ADR-025): lag-intern chat («det hemmelige rommet») og
offentlig runde-feed («Banter Board»). Delt `message`-tabell (migrasjon 013)
med en scope-diskriminator, men to helt ulike autorisasjonsmodeller:

- Lag-chat (`router`, /orgs/...): org-medlemskap (get_authorized_org) +
  `user_is_rostered_on_team` -- BEVISST INGEN org-admin-unntak, ulikt resten
  av appen. Ekte privat.
- Offentlig feed (`public_router`, /public/tournaments/...): LESING
  gjenbruker registration.py sitt trenivå-visibility-mønster uendret
  (inkl. anonym tilgang for `public`-synlige turneringer). POSTING/SLETTING
  er strengere -- krever ekte innlogging OG org-medlemskap ELLER faktisk
  deltakelse i akkurat denne turneringen (ikke bare "kan se den").

Sanntid via WebSockets (ADR-025 Beslutning B): et rent in-memory
tilkoblingsregister PER PROSESS -- trygt med dagens ene `teecup_api`-
container, men deles IKKE på tvers av flere prosesser/containere (samme
klasse begrensning som den allerede aksepterte in-memory-cachen, se
ARCHITECTURE_DECISIONS.md sine "Åpne spørsmål"). WS-rutene kan ikke bruke
get_current_user/get_authorized_org direkte (de er Request-typet, ikke
WebSocket-kompatible via Depends()) -- egne, minimale kopier av samme
sjekker brukes i stedet.
"""

from collections import defaultdict

from fastapi import APIRouter, Depends, File, Form, UploadFile, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from ..auth import CurrentUser, get_current_user, get_current_user_optional
from ..auth import get_current_user_from_websocket
from ..db import org_connection, plain_connection
from ..errors import app_error, translate_db_errors
from ..realtime import live_sockets_for
from ..team_authz import is_org_admin, user_is_rostered_on_team
from .. import storage
from .registration import check_visibility, code_matches, is_participant, resolve_org
from .round_messages import ALLOWED_REACTION_EMOJIS, CommentIn, ReactionIn, ReactionSummary

router = APIRouter(tags=["messaging"])
public_router = APIRouter(prefix="/public/tournaments", tags=["public-messaging"])

_MESSAGE_COLUMNS = """
    id::text, author_user_id::text, author_display_name, body, image_key, created_at
"""


class MessageOut(BaseModel):
    id: str
    author_user_id: str
    author_display_name: str
    body: str | None
    image_url: str | None
    created_at: str
    reactions: list[ReactionSummary] = []
    comment_count: int = 0


def _row_to_message(
    row, reactions: list[ReactionSummary] | None = None, comment_count: int = 0
) -> MessageOut:
    return MessageOut(
        id=row["id"],
        author_user_id=row["author_user_id"],
        author_display_name=row["author_display_name"],
        body=row["body"],
        image_url=storage.public_url(row["image_key"]) if row["image_key"] else None,
        created_at=row["created_at"].isoformat(),
        reactions=reactions or [],
        comment_count=comment_count,
    )


# --- Reaksjoner/kommentarer (ADR-046) -- delt mellom lag-chat og ---------
# --- oppslagstavle, siden begge lagrer i `message` med scope-diskriminator.

_MESSAGE_COMMENT_COLUMNS = """
    id::text, message_id::text, parent_comment_id::text, author_user_id::text,
    author_display_name, body, created_at
"""


class CommentOut(BaseModel):
    id: str
    message_id: str
    parent_comment_id: str | None
    author_user_id: str
    author_display_name: str
    body: str
    created_at: str


def _row_to_message_comment(row) -> CommentOut:
    return CommentOut(
        id=row["id"],
        message_id=row["message_id"],
        parent_comment_id=row["parent_comment_id"],
        author_user_id=row["author_user_id"],
        author_display_name=row["author_display_name"],
        body=row["body"],
        created_at=row["created_at"].isoformat(),
    )


async def _message_reactions(
    conn, message_ids: list[str], viewer_user_id: str | None
) -> dict[str, list[ReactionSummary]]:
    if not message_ids:
        return {}
    rows = await conn.fetch(
        """
        SELECT mr.message_id::text AS message_id, mr.emoji, count(*) AS count,
               COALESCE(bool_or(mr.user_id = $2), false) AS reacted_by_me,
               array_agg(
                   COALESCE(p.display_name, split_part(au.email, '@', 1))
                   ORDER BY mr.created_at
               ) AS reactors
        FROM message_reaction mr
        JOIN app_user au ON au.id = mr.user_id
        LEFT JOIN player p ON p.organization_id = mr.organization_id AND p.user_id = mr.user_id
        WHERE mr.message_id = ANY($1::uuid[])
        GROUP BY mr.message_id, mr.emoji
        """,
        message_ids,
        viewer_user_id,
    )
    result: dict[str, list[ReactionSummary]] = {}
    for r in rows:
        result.setdefault(r["message_id"], []).append(
            ReactionSummary(
                emoji=r["emoji"],
                count=r["count"],
                reacted_by_me=r["reacted_by_me"],
                reactors=list(r["reactors"]),
            )
        )
    return result


async def _message_comment_counts(conn, message_ids: list[str]) -> dict[str, int]:
    if not message_ids:
        return {}
    rows = await conn.fetch(
        """
        SELECT message_id::text AS message_id, count(*) AS count
        FROM message_comment
        WHERE message_id = ANY($1::uuid[])
        GROUP BY message_id
        """,
        message_ids,
    )
    return {r["message_id"]: r["count"] for r in rows}


async def _resolve_author_display_name(conn, organization_id: str, user_id: str) -> str:
    """Fryses ved skrivetidspunkt (samme prinsipp som handicap_index_snapshot,
    ADR-007) -- avsenderens player.display_name i DENNE org-en hvis den
    finnes, ellers e-postens lokaldel (dekker org-ansatte uten egen
    spillerprofil som poster i den offentlige feeden)."""
    name = await conn.fetchval(
        "SELECT display_name FROM player WHERE organization_id = $1 AND user_id = $2 LIMIT 1",
        organization_id,
        user_id,
    )
    if name:
        return name
    async with plain_connection() as plain:
        email = await plain.fetchval("SELECT email FROM app_user WHERE id = $1", user_id)
    return email.split("@")[0] if email else "Ukjent"


# --- Sanntid: in-memory tilkoblingsregister (se moduldocstring) ------------

_team_sockets: dict[str, set[WebSocket]] = defaultdict(set)
_feed_sockets: dict[str, set[WebSocket]] = defaultdict(set)


async def _broadcast(sockets: set[WebSocket], payload: dict) -> None:
    dead = []
    for ws in list(sockets):
        try:
            await ws.send_json(payload)
        except Exception:
            dead.append(ws)
    for ws in dead:
        sockets.discard(ws)


async def _is_org_member(user_id: str, organization_id: str) -> bool:
    """WS-variant av get_authorized_org sin medlemskapssjekk -- samme grunn
    som get_current_user_from_websocket: ingen HTTP Request å binde
    Depends() til i en WS-scope."""
    async with plain_connection() as conn:
        return bool(
            await conn.fetchval(
                "SELECT EXISTS (SELECT 1 FROM organization_membership WHERE user_id = $1 AND organization_id = $2)",
                user_id,
                organization_id,
            )
        )


# ---------------------------------------------------------------------------
# Lag-chat (privat) -- /orgs/...
# ---------------------------------------------------------------------------


@router.get("/orgs/{organization_id}/teams/{team_id}/messages", response_model=list[MessageOut])
async def list_team_messages(
    team_id: str,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[MessageOut]:
    async with org_connection(organization_id) as conn:
        exists = await conn.fetchval("SELECT id FROM team WHERE id = $1", team_id)
        if exists is None:
            raise app_error(404, "NOT_FOUND", "Laget finnes ikke.")
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
        # Nyeste øverst (2026-08-06, brukerens eksplisitte ønske -- "over
        # alt", samme rekkefølge som round_message og tournament_feed nå).
        rows = await conn.fetch(
            f"""
            SELECT {_MESSAGE_COLUMNS} FROM message
            WHERE team_id = $1 AND scope = 'team'
            ORDER BY created_at DESC
            """,
            team_id,
        )
        message_ids = [r["id"] for r in rows]
        reactions_by_id = await _message_reactions(conn, message_ids, user.user_id)
        comment_counts = await _message_comment_counts(conn, message_ids)
    return [
        _row_to_message(r, reactions_by_id.get(r["id"]), comment_counts.get(r["id"], 0))
        for r in rows
    ]


@router.post(
    "/orgs/{organization_id}/teams/{team_id}/messages", response_model=MessageOut, status_code=201
)
async def send_team_message(
    team_id: str,
    organization_id: str,
    body: str | None = Form(default=None, max_length=2000),
    image: UploadFile | None = File(default=None),
    user: CurrentUser = Depends(get_current_user),
) -> MessageOut:
    if not body and image is None:
        raise app_error(400, "VALIDATION_FAILED", "Meldingen må ha tekst eller bilde.")
    raw = await storage.read_optional_image(image)

    async with org_connection(organization_id) as conn, translate_db_errors():
        team = await conn.fetchrow(
            "SELECT tournament_id::text AS tournament_id FROM team WHERE id = $1", team_id
        )
        if team is None:
            raise app_error(404, "NOT_FOUND", "Laget finnes ikke.")
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")

        image_key = None
        if raw is not None:
            try:
                image_key = await storage.upload_image("messages", team_id, raw)
            except storage.InvalidImageError:
                raise app_error(400, "VALIDATION_FAILED", "Filen er ikke et gyldig bilde.")

        display_name = await _resolve_author_display_name(conn, organization_id, user.user_id)

        row = await conn.fetchrow(
            f"""
            INSERT INTO message
                (organization_id, scope, tournament_id, team_id, author_user_id, author_display_name, body, image_key)
            VALUES ($1, 'team', $2, $3, $4, $5, $6, $7)
            RETURNING {_MESSAGE_COLUMNS}
            """,
            organization_id,
            team["tournament_id"],
            team_id,
            user.user_id,
            display_name,
            body,
            image_key,
        )
    message = _row_to_message(row)
    await _broadcast(_team_sockets[team_id], message.model_dump())
    return message


@router.delete(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}", status_code=204
)
async def delete_team_message(
    team_id: str,
    message_id: str,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> None:
    """Kun forfatteren selv -- INGEN org-admin-unntak (samme begrunnelse som
    lesetilgangen: et ekte privat rom har ingen ekstern moderator)."""
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT author_user_id::text AS author_user_id FROM message
            WHERE id = $1 AND team_id = $2 AND scope = 'team'
            """,
            message_id,
            team_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Meldingen finnes ikke.")
        if row["author_user_id"] != user.user_id:
            raise app_error(403, "NOT_MESSAGE_AUTHOR", "Du kan kun slette dine egne meldinger.")
        await conn.execute("DELETE FROM message WHERE id = $1", message_id)


# --- Reaksjoner/kommentarer på lag-chat-meldinger (ADR-046) ---------------
# Samme "hvem kan poste = hvem kan reagere/kommentere"-modell som selve
# meldingen (rostret på laget); sletting av kommentar er kun forfatteren,
# samme "ekte privat rom" begrunnelse som `delete_team_message`.


async def _get_team_message_or_404(conn, team_id: str, message_id: str) -> None:
    exists = await conn.fetchval(
        "SELECT id FROM message WHERE id = $1 AND team_id = $2 AND scope = 'team'",
        message_id,
        team_id,
    )
    if exists is None:
        raise app_error(404, "NOT_FOUND", "Meldingen finnes ikke.")


@router.put(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}/reaction",
    response_model=list[ReactionSummary],
)
async def set_team_message_reaction(
    team_id: str,
    organization_id: str,
    message_id: str,
    payload: ReactionIn,
    user: CurrentUser = Depends(get_current_user),
) -> list[ReactionSummary]:
    if payload.emoji not in ALLOWED_REACTION_EMOJIS:
        raise app_error(400, "VALIDATION_FAILED", "Ugyldig emoji.")
    async with org_connection(organization_id) as conn:
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
        await _get_team_message_or_404(conn, team_id, message_id)
        await conn.execute(
            """
            INSERT INTO message_reaction (organization_id, message_id, user_id, emoji)
            VALUES ($1, $2, $3, $4)
            ON CONFLICT (message_id, user_id) DO UPDATE SET emoji = EXCLUDED.emoji, created_at = now()
            """,
            organization_id,
            message_id,
            user.user_id,
            payload.emoji,
        )
        summaries = await _message_reactions(conn, [message_id], user.user_id)
    return summaries.get(message_id, [])


@router.delete(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}/reaction",
    response_model=list[ReactionSummary],
)
async def remove_team_message_reaction(
    team_id: str,
    organization_id: str,
    message_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[ReactionSummary]:
    async with org_connection(organization_id) as conn:
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
        await _get_team_message_or_404(conn, team_id, message_id)
        await conn.execute(
            "DELETE FROM message_reaction WHERE message_id = $1 AND user_id = $2",
            message_id,
            user.user_id,
        )
        summaries = await _message_reactions(conn, [message_id], user.user_id)
    return summaries.get(message_id, [])


@router.get(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}/comments",
    response_model=list[CommentOut],
)
async def list_team_message_comments(
    team_id: str,
    organization_id: str,
    message_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[CommentOut]:
    async with org_connection(organization_id) as conn:
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
        await _get_team_message_or_404(conn, team_id, message_id)
        rows = await conn.fetch(
            f"""
            SELECT {_MESSAGE_COMMENT_COLUMNS} FROM message_comment
            WHERE message_id = $1 ORDER BY created_at ASC
            """,
            message_id,
        )
    return [_row_to_message_comment(r) for r in rows]


@router.post(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}/comments",
    response_model=CommentOut,
    status_code=201,
)
async def post_team_message_comment(
    team_id: str,
    organization_id: str,
    message_id: str,
    payload: CommentIn,
    user: CurrentUser = Depends(get_current_user),
) -> CommentOut:
    body = payload.body.strip()
    if not body:
        raise app_error(400, "VALIDATION_FAILED", "Kommentaren kan ikke være tom.")
    async with org_connection(organization_id) as conn, translate_db_errors():
        if not await user_is_rostered_on_team(conn, team_id, user.user_id):
            raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
        await _get_team_message_or_404(conn, team_id, message_id)
        if payload.parent_comment_id is not None:
            parent_exists = await conn.fetchval(
                "SELECT id FROM message_comment WHERE id = $1 AND message_id = $2",
                payload.parent_comment_id,
                message_id,
            )
            if parent_exists is None:
                raise app_error(404, "NOT_FOUND", "Kommentaren du svarer på finnes ikke.")

        display_name = await _resolve_author_display_name(conn, organization_id, user.user_id)
        row = await conn.fetchrow(
            f"""
            INSERT INTO message_comment
                (organization_id, message_id, parent_comment_id, author_user_id, author_display_name, body)
            VALUES ($1, $2, $3, $4, $5, $6)
            RETURNING {_MESSAGE_COMMENT_COLUMNS}
            """,
            organization_id,
            message_id,
            payload.parent_comment_id,
            user.user_id,
            display_name,
            body,
        )
    return _row_to_message_comment(row)


@router.delete(
    "/orgs/{organization_id}/teams/{team_id}/messages/{message_id}/comments/{comment_id}",
    status_code=204,
)
async def delete_team_message_comment(
    team_id: str,
    organization_id: str,
    message_id: str,
    comment_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> None:
    """Kun forfatteren selv, INGEN unntak -- samme "ekte privat rom"-
    begrunnelse som `delete_team_message`."""
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT author_user_id::text AS author_user_id FROM message_comment
            WHERE id = $1 AND message_id = $2
            """,
            comment_id,
            message_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Kommentaren finnes ikke.")
        if row["author_user_id"] != user.user_id:
            raise app_error(403, "NOT_COMMENT_AUTHOR", "Du kan kun slette dine egne kommentarer.")
        await conn.execute("DELETE FROM message_comment WHERE id = $1", comment_id)


@router.websocket("/ws/orgs/{organization_id}/teams/{team_id}/messages")
async def team_chat_ws(websocket: WebSocket, organization_id: str, team_id: str) -> None:
    user = await get_current_user_from_websocket(websocket)
    if user is None:
        await websocket.close(code=4401)
        return
    if not await _is_org_member(user.user_id, organization_id):
        await websocket.close(code=4403)
        return
    async with org_connection(organization_id) as conn:
        allowed = await user_is_rostered_on_team(conn, team_id, user.user_id)
    if not allowed:
        await websocket.close(code=4403)
        return

    await websocket.accept()
    _team_sockets[team_id].add(websocket)
    try:
        while True:
            # Klienten sender ingenting -- denne løkken finnes kun for å
            # oppdage disconnect (receive() kaster WebSocketDisconnect da).
            await websocket.receive_text()
    except WebSocketDisconnect:
        pass
    finally:
        _team_sockets[team_id].discard(websocket)


# ---------------------------------------------------------------------------
# Offentlig runde-feed -- /public/tournaments/...
# ---------------------------------------------------------------------------


async def _may_post_to_feed(conn, user: CurrentUser, organization_id: str, tournament_id: str) -> bool:
    """Strengere enn LESING (check_visibility): en 'public'-synlig turnering
    kan leses helt anonymt, men posting krever alltid ekte innlogging OG
    tilknytning -- org-medlem ELLER faktisk deltaker/registrert i NØYAKTIG
    denne turneringen. Hindrer at en helt urelatert innlogget bruker (konto
    et helt annet sted i systemet) kan poste på en fremmed offentlig side."""
    is_member = await conn.fetchval(
        "SELECT EXISTS (SELECT 1 FROM organization_membership WHERE user_id = $1 AND organization_id = $2)",
        user.user_id,
        organization_id,
    )
    if is_member:
        return True
    return await is_participant(conn, user.user_id, organization_id, tournament_id)


@public_router.get("/{tournament_id}/feed", response_model=list[MessageOut])
async def get_feed(
    tournament_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> list[MessageOut]:
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
        if not code_matches(row["join_code"], code):
            await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)

        # Nyeste øverst (2026-08-06, brukerens eksplisitte ønske -- "over
        # alt", samme rekkefølge som round_message og lag-chatten nå).
        rows = await conn.fetch(
            f"""
            SELECT {_MESSAGE_COLUMNS} FROM message
            WHERE tournament_id = $1 AND scope = 'tournament_feed'
            ORDER BY created_at DESC
            """,
            tournament_id,
        )
        message_ids = [r["id"] for r in rows]
        reactions_by_id = await _message_reactions(conn, message_ids, user.user_id if user else None)
        comment_counts = await _message_comment_counts(conn, message_ids)
    return [
        _row_to_message(r, reactions_by_id.get(r["id"]), comment_counts.get(r["id"], 0))
        for r in rows
    ]


@public_router.post("/{tournament_id}/feed", response_model=MessageOut, status_code=201)
async def post_to_feed(
    tournament_id: str,
    body: str | None = Form(default=None, max_length=2000),
    image: UploadFile | None = File(default=None),
    code: str | None = Form(default=None, max_length=20),
    user: CurrentUser = Depends(get_current_user),
) -> MessageOut:
    if not body and image is None:
        raise app_error(400, "VALIDATION_FAILED", "Meldingen må ha tekst eller bilde.")
    raw = await storage.read_optional_image(image)

    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn, translate_db_errors():
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
        if not code_matches(row["join_code"], code):
            await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
        if not await _may_post_to_feed(conn, user, organization_id, tournament_id):
            raise app_error(
                403,
                "NOT_A_PARTICIPANT",
                "Du må være medlem av organisasjonen eller delta i turneringen for å poste.",
            )

        image_key = None
        if raw is not None:
            try:
                image_key = await storage.upload_image("messages", tournament_id, raw)
            except storage.InvalidImageError:
                raise app_error(400, "VALIDATION_FAILED", "Filen er ikke et gyldig bilde.")

        display_name = await _resolve_author_display_name(conn, organization_id, user.user_id)

        inserted = await conn.fetchrow(
            f"""
            INSERT INTO message
                (organization_id, scope, tournament_id, team_id, author_user_id, author_display_name, body, image_key)
            VALUES ($1, 'tournament_feed', $2, NULL, $3, $4, $5, $6)
            RETURNING {_MESSAGE_COLUMNS}
            """,
            organization_id,
            tournament_id,
            user.user_id,
            display_name,
            body,
            image_key,
        )
    message = _row_to_message(inserted)
    await _broadcast(_feed_sockets[tournament_id], message.model_dump())
    return message


@public_router.delete("/{tournament_id}/feed/{message_id}", status_code=204)
async def delete_feed_message(
    tournament_id: str,
    message_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> None:
    """Forfatteren selv, ELLER org-eier/admin (moderering) -- ulikt lag-
    chatten, siden feeden er offentlig og derfor trenger en reell
    moderasjonsvei."""
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT author_user_id::text AS author_user_id FROM message
            WHERE id = $1 AND tournament_id = $2 AND scope = 'tournament_feed'
            """,
            message_id,
            tournament_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Meldingen finnes ikke.")
        if row["author_user_id"] != user.user_id and not await is_org_admin(
            conn, organization_id, user.user_id
        ):
            raise app_error(
                403, "NOT_MESSAGE_AUTHOR", "Du kan kun slette dine egne meldinger (eller som organisasjonsadministrator)."
            )
        await conn.execute("DELETE FROM message WHERE id = $1", message_id)


# --- Reaksjoner/kommentarer på oppslagstavle-innlegg (ADR-046) -----------
# Samme "hvem kan poste = hvem kan reagere/kommentere"-modell som selve
# innlegget (`_may_post_to_feed`); sletting av kommentar er forfatter
# ELLER org-admin, samme moderasjonsvei som `delete_feed_message`.


async def _get_feed_message_or_404(conn, tournament_id: str, message_id: str) -> None:
    exists = await conn.fetchval(
        "SELECT id FROM message WHERE id = $1 AND tournament_id = $2 AND scope = 'tournament_feed'",
        message_id,
        tournament_id,
    )
    if exists is None:
        raise app_error(404, "NOT_FOUND", "Meldingen finnes ikke.")


@public_router.put("/{tournament_id}/feed/{message_id}/reaction", response_model=list[ReactionSummary])
async def set_feed_message_reaction(
    tournament_id: str,
    message_id: str,
    payload: ReactionIn,
    user: CurrentUser = Depends(get_current_user),
) -> list[ReactionSummary]:
    if payload.emoji not in ALLOWED_REACTION_EMOJIS:
        raise app_error(400, "VALIDATION_FAILED", "Ugyldig emoji.")
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        if not await _may_post_to_feed(conn, user, organization_id, tournament_id):
            raise app_error(
                403,
                "NOT_A_PARTICIPANT",
                "Du må være medlem av organisasjonen eller delta i turneringen for å reagere.",
            )
        await _get_feed_message_or_404(conn, tournament_id, message_id)
        await conn.execute(
            """
            INSERT INTO message_reaction (organization_id, message_id, user_id, emoji)
            VALUES ($1, $2, $3, $4)
            ON CONFLICT (message_id, user_id) DO UPDATE SET emoji = EXCLUDED.emoji, created_at = now()
            """,
            organization_id,
            message_id,
            user.user_id,
            payload.emoji,
        )
        summaries = await _message_reactions(conn, [message_id], user.user_id)
    return summaries.get(message_id, [])


@public_router.delete("/{tournament_id}/feed/{message_id}/reaction", response_model=list[ReactionSummary])
async def remove_feed_message_reaction(
    tournament_id: str,
    message_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[ReactionSummary]:
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        if not await _may_post_to_feed(conn, user, organization_id, tournament_id):
            raise app_error(
                403,
                "NOT_A_PARTICIPANT",
                "Du må være medlem av organisasjonen eller delta i turneringen for å reagere.",
            )
        await _get_feed_message_or_404(conn, tournament_id, message_id)
        await conn.execute(
            "DELETE FROM message_reaction WHERE message_id = $1 AND user_id = $2",
            message_id,
            user.user_id,
        )
        summaries = await _message_reactions(conn, [message_id], user.user_id)
    return summaries.get(message_id, [])


@public_router.get("/{tournament_id}/feed/{message_id}/comments", response_model=list[CommentOut])
async def list_feed_message_comments(
    tournament_id: str,
    message_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> list[CommentOut]:
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
        if not code_matches(row["join_code"], code):
            await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
        await _get_feed_message_or_404(conn, tournament_id, message_id)
        rows = await conn.fetch(
            f"""
            SELECT {_MESSAGE_COMMENT_COLUMNS} FROM message_comment
            WHERE message_id = $1 ORDER BY created_at ASC
            """,
            message_id,
        )
    return [_row_to_message_comment(r) for r in rows]


@public_router.post("/{tournament_id}/feed/{message_id}/comments", response_model=CommentOut, status_code=201)
async def post_feed_message_comment(
    tournament_id: str,
    message_id: str,
    payload: CommentIn,
    code: str | None = None,
    user: CurrentUser = Depends(get_current_user),
) -> CommentOut:
    body = payload.body.strip()
    if not body:
        raise app_error(400, "VALIDATION_FAILED", "Kommentaren kan ikke være tom.")
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn, translate_db_errors():
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
        if not code_matches(row["join_code"], code):
            await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
        if not await _may_post_to_feed(conn, user, organization_id, tournament_id):
            raise app_error(
                403,
                "NOT_A_PARTICIPANT",
                "Du må være medlem av organisasjonen eller delta i turneringen for å kommentere.",
            )
        await _get_feed_message_or_404(conn, tournament_id, message_id)
        if payload.parent_comment_id is not None:
            parent_exists = await conn.fetchval(
                "SELECT id FROM message_comment WHERE id = $1 AND message_id = $2",
                payload.parent_comment_id,
                message_id,
            )
            if parent_exists is None:
                raise app_error(404, "NOT_FOUND", "Kommentaren du svarer på finnes ikke.")

        display_name = await _resolve_author_display_name(conn, organization_id, user.user_id)
        inserted = await conn.fetchrow(
            f"""
            INSERT INTO message_comment
                (organization_id, message_id, parent_comment_id, author_user_id, author_display_name, body)
            VALUES ($1, $2, $3, $4, $5, $6)
            RETURNING {_MESSAGE_COMMENT_COLUMNS}
            """,
            organization_id,
            message_id,
            payload.parent_comment_id,
            user.user_id,
            display_name,
            body,
        )
    return _row_to_message_comment(inserted)


@public_router.delete("/{tournament_id}/feed/{message_id}/comments/{comment_id}", status_code=204)
async def delete_feed_message_comment(
    tournament_id: str,
    message_id: str,
    comment_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> None:
    """Forfatteren selv, ELLER org-eier/admin -- samme moderasjonsvei som
    `delete_feed_message`."""
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT author_user_id::text AS author_user_id FROM message_comment
            WHERE id = $1 AND message_id = $2
            """,
            comment_id,
            message_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Kommentaren finnes ikke.")
        if row["author_user_id"] != user.user_id and not await is_org_admin(
            conn, organization_id, user.user_id
        ):
            raise app_error(
                403,
                "NOT_COMMENT_AUTHOR",
                "Du kan kun slette dine egne kommentarer (eller som organisasjonsadministrator).",
            )
        await conn.execute("DELETE FROM message_comment WHERE id = $1", comment_id)


@router.websocket("/ws/public/tournaments/{tournament_id}/feed")
async def feed_ws(websocket: WebSocket, tournament_id: str, code: str | None = None) -> None:
    user = await get_current_user_from_websocket(websocket)
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            await websocket.close(code=4404)
            return
        if not code_matches(row["join_code"], code):
            try:
                await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
            except Exception:
                await websocket.close(code=4403)
                return

    await websocket.accept()
    _feed_sockets[tournament_id].add(websocket)
    try:
        while True:
            await websocket.receive_text()
    except WebSocketDisconnect:
        pass
    finally:
        _feed_sockets[tournament_id].discard(websocket)


@router.websocket("/ws/public/tournaments/{tournament_id}/live")
async def tournament_live_ws(websocket: WebSocket, tournament_id: str, code: str | None = None) -> None:
    """ADR-027: "Følg live"-siden (leaderboard/matcher/scorekort,
    registration.py) sin sanntid. Sender kun et "noe endret seg"-signal --
    se app/realtime.py sin moduldocstring for hvorfor -- klienten reagerer
    ved å hente de vanlige REST-endepunktene på nytt. Samme visibility-sjekk
    som selve de offentlige lese-endepunktene den speiler."""
    user = await get_current_user_from_websocket(websocket)
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            "SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
        )
        if row is None:
            await websocket.close(code=4404)
            return
        if not code_matches(row["join_code"], code):
            try:
                await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
            except Exception:
                await websocket.close(code=4403)
                return

    await websocket.accept()
    sockets = live_sockets_for(tournament_id)
    sockets.add(websocket)
    try:
        while True:
            await websocket.receive_text()
    except WebSocketDisconnect:
        pass
    finally:
        sockets.discard(websocket)
