"""
Offentlig, uautentisert registrerings- og landingsside-API (ADR-017, ADR-018).

Bevisst egen fil med prefiks /public/..., atskilt fra /orgs/... -- gjør
sikkerhetsgrensen eksplisitt i koden: disse endepunktene krever VERKEN
innlogging (get_current_user) ELLER org-medlemskap (get_authorized_org) for
å svare i det hele tatt.

To SECURITY DEFINER-broer (public_tournament_org, public_org_by_slug) er den
ENESTE måten disse endepunktene får vite riktig RLS-kontekst FØR den er
kjent -- se ADR-017 Beslutning E / ADR-018 Beslutning F.

VIKTIG (ADR-018 Beslutning B): RLS (org_isolation) beskytter kun TENANT-
grenser (org A ser aldri org B), ikke INNHOLDS-synlighet innenfor riktig
org-kontekst. `tournament.visibility` håndheves derfor eksplisitt her i
applikasjonslaget, på hvert lese- OG skrive-endepunkt (registrering følger
samme grense som lesing, ADR-018 Beslutning D) -- RLS alene løser det ikke.
"""

from datetime import date, datetime, timezone

from fastapi import APIRouter, Depends
from pydantic import BaseModel, EmailStr, Field

from ..auth import CurrentUser, get_current_user_optional
from ..blind_draw import locked_team_ids
from ..db import org_connection, plain_connection
from ..errors import app_error, translate_db_errors
from .. import storage
from ..team_authz import user_is_tournament_participant
from .matches import MatchOut, fetch_matches
from .scoring import Scorecard, fetch_scorecard
from .tournaments import Leaderboard, SessionOut, _fetch_sessions, fetch_leaderboard

router = APIRouter(prefix="/public/tournaments", tags=["public-registration"])
org_router = APIRouter(prefix="/public/orgs", tags=["public-registration"])


async def resolve_org(tournament_id: str) -> str:
    async with plain_connection() as conn:
        org_id = await conn.fetchval("SELECT public_tournament_org($1)", tournament_id)
    if org_id is None:
        raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
    return str(org_id)


async def is_participant(conn, user_id: str, organization_id: str, tournament_id: str) -> bool:
    """Tynn wrapper -- selve sjekken flyttet til team_authz.py 2026-07-21
    (`user_is_tournament_participant`) slik at org-scopede endepunkter
    (matches.py/scoring.py/tournaments.py) kan gjenbruke den uten sirkulær
    import. Beholder denne funksjonens navn/argumentrekkefølge uendret her
    for å unngå å røre alle kallsteder i denne filen."""
    return await user_is_tournament_participant(conn, organization_id, tournament_id, user_id)


def code_matches(actual_join_code: str, provided_code: str | None) -> bool:
    return provided_code is not None and provided_code.strip().lower() == actual_join_code.lower()


async def check_visibility(
    conn,
    visibility: str,
    organization_id: str,
    tournament_id: str,
    user: CurrentUser | None,
) -> None:
    if visibility == "public":
        return
    if user is not None:
        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
        # ADR-031: en faktisk DELTAKER skal aldri stenges ute av øktens
        # egen turnering, uansett hvilken av de to ikke-offentlige tierene
        # som er valgt -- visibility styrer eksponering mot UTENFORSTÅENDE,
        # ikke mot spillere som faktisk er med. Utvidet fra kun
        # visibility == "participants" (opprinnelig ADR-018 Beslutning C)
        # da "Mine runder"-dashbordet gjorde det tydelig at 'org' (som er
        # DEFAULT for enhver ny turnering) ellers ville stengt ute enhver
        # spiller uten organisasjonsmedlemskap fra sin egen turnering.
        if await is_participant(conn, user.user_id, organization_id, tournament_id):
            return
    raise app_error(403, "NOT_VISIBLE", "Du har ikke tilgang til denne turneringen.")


class JoinCodeResult(BaseModel):
    tournament_id: str
    organization_id: str
    tournament_name: str


@router.get("/by-code/{code}", response_model=JoinCodeResult)
async def resolve_join_code(code: str) -> JoinCodeResult:
    """Løser en invitasjonskode til riktig turnering (ADR-020 Beslutning A) --
    MÅ registreres FØR /{tournament_id} under, ellers ville "by-code" blitt
    tolket som en tournament_id (ugyldig UUID -> 500, ikke 404).

    Bevisst UAVHENGIG av tournament.visibility -- ingen check_visibility()-
    kall her, i motsetning til alle andre endepunkter i denne filen. Koden
    ER selve invitasjonen; å likevel kreve org-medlemskap/deltakerstatus
    ville gjort den verdiløs nettopp for personer som mangler det."""
    async with plain_connection() as conn:
        tournament_id = await conn.fetchval("SELECT public_tournament_by_code($1)", code)
    if tournament_id is None:
        raise app_error(404, "NOT_FOUND", "Fant ingen turnering med denne koden.")

    organization_id = await resolve_org(str(tournament_id))
    async with org_connection(organization_id) as conn:
        name = await conn.fetchval("SELECT name FROM tournament WHERE id = $1", tournament_id)
    return JoinCodeResult(
        tournament_id=str(tournament_id), organization_id=organization_id, tournament_name=name
    )


class SponsorOut(BaseModel):
    id: str
    name: str
    url: str | None
    logo_url: str | None


class PublicTournamentInfo(BaseModel):
    id: str
    name: str
    organization_name: str
    status: str
    visibility: str
    description: str | None
    start_date: date | None
    end_date: date | None
    registration_open: bool
    registration_deadline: datetime | None
    registration_capacity: int | None
    confirmed_count: int
    hero_image_url: str | None
    sponsors: list[SponsorOut]


@router.get("/{tournament_id}", response_model=PublicTournamentInfo)
async def get_public_tournament(
    tournament_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> PublicTournamentInfo:
    organization_id = await resolve_org(tournament_id)
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT t.id::text, t.name, o.name AS organization_name, t.status::text,
                   t.visibility, t.description, t.hero_image_key, t.join_code,
                   t.start_date, t.end_date, t.registration_deadline,
                   t.registration_capacity,
                   (SELECT count(*)::int FROM tournament_registration tr
                    WHERE tr.tournament_id = t.id AND tr.status IN ('confirmed', 'pending')
                   ) AS confirmed_count
            FROM tournament t
            JOIN organization o ON o.id = t.organization_id
            WHERE t.id = $1
            """,
            tournament_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")

        # ADR-020 Beslutning A: en matchende kode overstyrer visibility helt.
        if not code_matches(row["join_code"], code):
            await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)

        sponsor_rows = await conn.fetch(
            "SELECT id::text, name, url, logo_key FROM tournament_sponsor WHERE tournament_id = $1 ORDER BY created_at",
            tournament_id,
        )

    deadline = row["registration_deadline"]
    deadline_passed = deadline is not None and deadline < datetime.now(timezone.utc)
    return PublicTournamentInfo(
        id=row["id"],
        name=row["name"],
        organization_name=row["organization_name"],
        status=row["status"],
        visibility=row["visibility"],
        description=row["description"],
        start_date=row["start_date"],
        end_date=row["end_date"],
        registration_open=not deadline_passed,
        registration_deadline=deadline,
        registration_capacity=row["registration_capacity"],
        confirmed_count=row["confirmed_count"],
        hero_image_url=storage.public_url(row["hero_image_key"]) if row["hero_image_key"] else None,
        sponsors=[
            SponsorOut(
                id=r["id"],
                name=r["name"],
                url=r["url"],
                logo_url=storage.public_url(r["logo_key"]) if r["logo_key"] else None,
            )
            for r in sponsor_rows
        ],
    )


@router.get("/{tournament_id}/sessions", response_model=list[SessionOut])
async def get_public_sessions(
    tournament_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> list[SessionOut]:
    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)
        return await _fetch_sessions(conn, tournament_id)


# --- Tilskuer-rolle (ADR-026): leaderboard, matcher, scorekort -------------
#
# Samme synlighets-/kode-mønster som info-/sesjon-kallene over -- ingen ny
# mekanisme. Leaderboard har ingen spillernivå-data i det hele tatt (kun
# lag-id/-navn/-farge/poeng), så INGEN ekstra reveal-sjekk trengs der.
# Match-listen arver blind draw-skjulingen (ADR-013) automatisk via
# fetch_matches() sin egen own_team_ids()-håndtering (tom mengde for en
# anonym/uinnloggede leser -- ser bare avslørte matcher, akkurat som en
# tilfeldig org-medlem uten roster ville gjort). Scorekortet trenger en
# EKSPLISITT reveal-sjekk her (matchen kan i prinsippet ha registrerte hull
# før begge lag har låst -- ikke normal flyt, men ikke teknisk umulig).


@router.get("/{tournament_id}/leaderboard", response_model=Leaderboard)
async def get_public_leaderboard(
    tournament_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> Leaderboard:
    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)
        return await fetch_leaderboard(conn, tournament_id)


@router.get(
    "/{tournament_id}/sessions/{session_id}/matches", response_model=list[MatchOut]
)
async def get_public_matches(
    tournament_id: str,
    session_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> list[MatchOut]:
    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)

        # Sikkerhetskritisk: session_id må faktisk høre til DENNE turneringen
        # -- org_connection() setter kun tenant-grensen (RLS), ikke at stiens
        # to id-er faktisk henger sammen. Uten denne sjekken kunne noen med
        # kode/tilgang til én offentlig turnering lese en HVILKEN SOM HELST
        # økt i samme organisasjon (inkl. en privat en) ved å gjette id-en.
        session_tournament = await conn.fetchval(
            "SELECT tournament_id::text FROM session WHERE id = $1", session_id
        )
        if session_tournament != tournament_id:
            raise app_error(404, "NOT_FOUND", "Økten finnes ikke i denne turneringen.")

        user_id = user.user_id if user is not None else None
        return await fetch_matches(conn, organization_id, session_id, user_id)


@router.get("/{tournament_id}/matches/{match_id}/scorecard", response_model=Scorecard)
async def get_public_scorecard(
    tournament_id: str,
    match_id: str,
    code: str | None = None,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> Scorecard:
    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)

        # Samme tenant-vs-sti-konsistenssjekk som matchlisten over, PLUSS en
        # eksplisitt reveal-sjekk (blind draw, ADR-013) -- scorekortet skal
        # aldri kunne leses offentlig før laget selv har avslørt oppstillingen.
        match_row = await conn.fetchrow(
            """
            SELECT s.tournament_id::text AS tournament_id, m.session_id::text AS session_id
            FROM match m JOIN session s ON s.id = m.session_id
            WHERE m.id = $1
            """,
            match_id,
        )
        if match_row is None or match_row["tournament_id"] != tournament_id:
            raise app_error(404, "NOT_FOUND", "Matchen finnes ikke i denne turneringen.")

        locked = await locked_team_ids(conn, match_row["session_id"])
        if len(locked) < 2:
            raise app_error(403, "NOT_VISIBLE", "Denne matchen er ikke avslørt ennå.")

        return await fetch_scorecard(conn, match_id)


class RegisterRequest(BaseModel):
    display_name: str = Field(min_length=1, max_length=200)
    email: EmailStr | None = None
    mobile: str | None = None
    birth_date: date | None = None
    nickname: str | None = None
    country: str | None = None
    club: str | None = None
    club_member_number: str | None = None
    gender: str | None = Field(default=None, pattern="^[mfx]$")
    handicap_index: float | None = None
    # API-et krever eksplisitt True -- ingen implisitt samtykke (ADR-017).
    consent: bool
    # ADR-020 Beslutning A: en matchende invitasjonskode overstyrer visibility.
    code: str | None = None


class RegistrationResult(BaseModel):
    id: str
    status: str


@router.post("/{tournament_id}/register", response_model=RegistrationResult, status_code=201)
async def register_for_tournament(
    tournament_id: str,
    body: RegisterRequest,
    user: CurrentUser | None = Depends(get_current_user_optional),
) -> RegistrationResult:
    if not body.consent:
        raise app_error(400, "VALIDATION_FAILED", "Samtykke må gis for å melde seg på.")

    organization_id = await resolve_org(tournament_id)

    async with org_connection(organization_id) as conn, translate_db_errors():
        # Lås turneringsraden: forhindrer at to samtidige påmeldinger begge
        # leser "under kapasitet" og begge kommer inn (TOCTOU) -- samme
        # mønster som to-lags-grensen i tournaments.py sin create_team.
        tournament = await conn.fetchrow(
            """
            SELECT visibility, join_code, registration_deadline, registration_capacity,
                   registration_overflow_policy, registration_requires_approval
            FROM tournament WHERE id = $1 FOR UPDATE
            """,
            tournament_id,
        )
        if tournament is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")

        # ADR-018 Beslutning D: registrering følger samme synlighetsgrense
        # som selve landingssiden -- ingen særbehandling. ADR-020 Beslutning A:
        # unntaket er en matchende invitasjonskode, som overstyrer dette helt.
        if not code_matches(tournament["join_code"], body.code):
            await check_visibility(conn, tournament["visibility"], organization_id, tournament_id, user)

        deadline = tournament["registration_deadline"]
        if deadline is not None and deadline < datetime.now(timezone.utc):
            raise app_error(409, "REGISTRATION_CLOSED", "Påmeldingen er stengt.")

        # E-post-matching (ADR-017 Beslutning B): finn en allerede
        # organisator-opprettet spillerrad med samme e-post og fyll inn
        # manglende felt der, i stedet for å opprette en duplikat.
        player_id = None
        if body.email:
            existing = await conn.fetchrow(
                "SELECT id FROM player WHERE organization_id = $1 AND lower(email) = lower($2)",
                organization_id,
                body.email,
            )
            if existing is not None:
                player_id = existing["id"]
                await conn.execute(
                    """
                    UPDATE player SET
                        mobile = COALESCE(mobile, $2),
                        birth_date = COALESCE(birth_date, $3),
                        nickname = COALESCE(nickname, $4),
                        country = COALESCE(country, $5),
                        club = COALESCE(club, $6),
                        club_member_number = COALESCE(club_member_number, $7),
                        gender = COALESCE(gender, $8),
                        handicap_index = COALESCE(handicap_index, $9)
                    WHERE id = $1
                    """,
                    player_id,
                    body.mobile,
                    body.birth_date,
                    body.nickname,
                    body.country,
                    body.club,
                    body.club_member_number,
                    body.gender,
                    body.handicap_index,
                )

        if player_id is None:
            player_row = await conn.fetchrow(
                """
                INSERT INTO player
                    (organization_id, display_name, handicap_index, gender,
                     mobile, email, birth_date, nickname, country, club, club_member_number)
                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
                RETURNING id
                """,
                organization_id,
                body.display_name,
                body.handicap_index,
                body.gender,
                body.mobile,
                body.email.lower() if body.email else None,
                body.birth_date,
                body.nickname,
                body.country,
                body.club,
                body.club_member_number,
            )
            player_id = player_row["id"]

        already = await conn.fetchval(
            "SELECT id FROM tournament_registration WHERE tournament_id = $1 AND player_id = $2",
            tournament_id,
            player_id,
        )
        if already is not None:
            raise app_error(409, "DUPLICATE", "Denne spilleren er allerede påmeldt.")

        # Rekkefølge fra ADR-017 Beslutning C: frist -> kapasitet -> godkjenning.
        status = "confirmed"
        if tournament["registration_capacity"] is not None:
            active_count = await conn.fetchval(
                """
                SELECT count(*) FROM tournament_registration
                WHERE tournament_id = $1 AND status IN ('confirmed', 'pending')
                """,
                tournament_id,
            )
            if active_count >= tournament["registration_capacity"]:
                if tournament["registration_overflow_policy"] == "waitlist":
                    status = "waitlisted"
                else:
                    raise app_error(409, "LIMIT_REACHED", "Turneringen er full.")

        if status != "waitlisted" and tournament["registration_requires_approval"]:
            status = "pending"

        row = await conn.fetchrow(
            """
            INSERT INTO tournament_registration
                (organization_id, tournament_id, player_id, status, consent_given_at)
            VALUES ($1, $2, $3, $4, now())
            RETURNING id::text, status
            """,
            organization_id,
            tournament_id,
            player_id,
            status,
        )
    return RegistrationResult(id=row["id"], status=row["status"])


class PublicOrgTournament(BaseModel):
    id: str
    name: str
    status: str
    start_date: date | None
    end_date: date | None


class PublicOrgInfo(BaseModel):
    name: str
    tournaments: list[PublicOrgTournament]


@org_router.get("/{slug}", response_model=PublicOrgInfo)
async def get_public_org(slug: str) -> PublicOrgInfo:
    async with plain_connection() as conn:
        organization_id = await conn.fetchval("SELECT public_org_by_slug($1)", slug)
    # NULL for BÅDE "finnes ikke" og "finnes, men er privat" -- samme
    # anti-enumerering som magic-link (ADR-009), se ADR-018 Beslutning F.
    if organization_id is None:
        raise app_error(404, "NOT_FOUND", "Organisasjonen finnes ikke.")

    async with org_connection(str(organization_id)) as conn:
        org_row = await conn.fetchrow("SELECT name FROM organization WHERE id = $1", organization_id)
        tournament_rows = await conn.fetch(
            """
            SELECT id::text, name, status::text, start_date, end_date
            FROM tournament
            WHERE visibility = 'public'
            ORDER BY start_date DESC NULLS LAST, created_at DESC
            """,
        )
    return PublicOrgInfo(
        name=org_row["name"],
        tournaments=[PublicOrgTournament(**dict(r)) for r in tournament_rows],
    )
