"""
Matcher, deltakere og blind draw-lås (ADR-013).

Autorisasjonsgrense (se plan): å opprette en match er organisator-arbeid (kun
org-medlemskap, som tournaments.py). Å legge til en DELTAKER eller LÅSE et lags
oppstilling krever i tillegg at brukeren er KAPTEIN for DET laget (eller
org-eier/admin) -- se app/team_authz.py sin user_is_team_captain for den fulle
begrunnelsen, inkl. fallback for lag uten utpekt kaptein ennå. Byttet fra
"hvem som helst rostret" 2026-07-19 (Brukerroller-runden, ADR-023).

Synlighet (ADR-013): motstanderens deltakere er skjult i app-laget (ikke RLS —
begge lag er i samme organisasjon) til BEGGE lag har låst for økten. Filteret
er lagt i SQL (WHERE ... AND ($revealed OR team_id = ANY($egne_lag))), ikke et
etterfølgende Python-filter, slik at lekkasje er strukturelt umulig.

Klokkeslett (se plan): match.tee_time er UTLEDET, ikke lagret -- session sin
scheduled_at + (sequence-1) * tee_interval_minutes, med en valgfri
tee_time_override som vinner hvis satt. Beregnes i Python i både create_match
og list_matches, som allerede henter økten for andre formål.
"""

import json
from datetime import datetime, timedelta

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

from ..auth import CurrentUser, get_authorized_org, get_current_user
from ..blind_draw import locked_team_ids, own_team_ids
from ..db import org_connection
from ..errors import app_error, translate_db_errors
from ..handicap import compute_and_store_side_handicaps, parse_allowance_config
from ..team_authz import is_org_member, user_is_team_captain, user_is_tournament_participant

router = APIRouter()


def _compute_tee_time(
    scheduled_at: datetime | None,
    tee_interval_minutes: int | None,
    sequence: int,
    override: datetime | None,
) -> datetime | None:
    if override is not None:
        return override
    if scheduled_at is None or tee_interval_minutes is None:
        return None
    return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes)


class MatchParticipantOut(BaseModel):
    id: str
    team_side: str
    team_roster_id: str
    player_name: str
    tee_id: str
    tee_name: str
    playing_handicap: int | None = None
    # Money Ball (2026-07-30) -- 0-3, null for alle andre formater.
    lineup_order: int | None = None


class MatchCreate(BaseModel):
    sequence: int
    team_a_id: str
    team_b_id: str


class MatchOut(BaseModel):
    id: str
    sequence: int
    team_a_id: str
    team_b_id: str
    status_text: str | None
    points_side_a: float | None
    points_side_b: float | None
    leading_side: str | None
    tee_time: datetime | None
    participants: list[MatchParticipantOut]


@router.post(
    "/orgs/{organization_id}/sessions/{session_id}/matches",
    response_model=MatchOut,
    status_code=201,
)
async def create_match(
    session_id: str,
    body: MatchCreate,
    organization_id: str = Depends(get_authorized_org),
) -> MatchOut:
    async with org_connection(organization_id) as conn, translate_db_errors():
        session = await conn.fetchrow(
            """
            SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
            FROM session WHERE id = $1
            """,
            session_id,
        )
        if session is None:
            raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")

        # Matchens FK garanterer bare at lagene er gyldige lag i ORG-en, ikke i
        # DENNE turneringen — sjekk det eksplisitt her. Stoler på DB CHECK
        # (team_a_id <> team_b_id) for distinkthet, dupliserer den ikke.
        team_count = await conn.fetchval(
            "SELECT count(*) FROM team WHERE tournament_id = $1 AND id = ANY($2::uuid[])",
            session["tournament_id"],
            [body.team_a_id, body.team_b_id],
        )
        if team_count != 2:
            raise app_error(400, "VALIDATION_FAILED", "Begge lagene må tilhøre øktens turnering.")

        row = await conn.fetchrow(
            """
            INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id)
            VALUES ($1, $2, $3, $4, $5)
            RETURNING id::text, sequence, team_a_id::text, team_b_id::text,
                      status_text, points_side_a::float AS points_side_a,
                      points_side_b::float AS points_side_b, leading_side, tee_time_override
            """,
            organization_id,
            session_id,
            body.sequence,
            body.team_a_id,
            body.team_b_id,
        )
    tee_time = _compute_tee_time(
        session["scheduled_at"], session["tee_interval_minutes"], row["sequence"], row["tee_time_override"]
    )
    return MatchOut(**dict(row), tee_time=tee_time, participants=[])


async def fetch_matches(
    conn, organization_id: str, session_id: str, user_id: str | None
) -> list[MatchOut]:
    """Delt mellom den innloggede list_matches under og den offentlige
    /public/tournaments/{id}/sessions/{id}/matches (registration.py,
    tilskuer-rolle/ADR-026). `user_id=None` for en anonym/ikke-tilknyttet
    offentlig leser -- own_team_ids() håndterer det (tom mengde, ser kun
    avslørte matcher, se blind_draw.py)."""
    session = await conn.fetchrow(
        """
        SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
        FROM session WHERE id = $1
        """,
        session_id,
    )
    if session is None:
        raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")

    matches = await conn.fetch(
        """
        SELECT id::text, sequence, team_a_id::text, team_b_id::text,
               status_text, points_side_a::float AS points_side_a,
               points_side_b::float AS points_side_b, leading_side, tee_time_override
        FROM match
        WHERE session_id = $1
        ORDER BY sequence
        """,
        session_id,
    )
    match_ids = [m["id"] for m in matches]

    locked = await locked_team_ids(conn, session_id)
    revealed = len(locked) >= 2
    # Tomt sett hvis revealed (ubrukt da $2=true gjør ANY-leddet irrelevant).
    own = (
        set()
        if revealed
        else await own_team_ids(conn, organization_id, session["tournament_id"], user_id)
    )

    participant_rows = await conn.fetch(
        """
        SELECT mp.match_id::text AS match_id, mp.id::text, mp.team_side::text,
               mp.team_roster_id::text, p.display_name AS player_name,
               mp.tee_id::text, tee.name AS tee_name, mp.playing_handicap, mp.lineup_order
        FROM match_participant mp
        JOIN team_roster tr ON tr.id = mp.team_roster_id
        JOIN player p ON p.id = tr.player_id
        JOIN tee ON tee.id = mp.tee_id
        WHERE mp.match_id = ANY($1::uuid[])
          AND ($2 OR tr.team_id::text = ANY($3::text[]))
        """,
        match_ids,
        revealed,
        list(own),
    )

    by_match: dict[str, list[MatchParticipantOut]] = {mid: [] for mid in match_ids}
    for r in participant_rows:
        by_match[r["match_id"]].append(
            MatchParticipantOut(
                id=r["id"],
                team_side=r["team_side"],
                team_roster_id=r["team_roster_id"],
                player_name=r["player_name"],
                tee_id=r["tee_id"],
                tee_name=r["tee_name"],
                playing_handicap=r["playing_handicap"],
                lineup_order=r["lineup_order"],
            )
        )

    return [
        MatchOut(
            id=m["id"],
            sequence=m["sequence"],
            team_a_id=m["team_a_id"],
            team_b_id=m["team_b_id"],
            status_text=m["status_text"],
            points_side_a=m["points_side_a"],
            points_side_b=m["points_side_b"],
            leading_side=m["leading_side"],
            tee_time=_compute_tee_time(
                session["scheduled_at"], session["tee_interval_minutes"], m["sequence"], m["tee_time_override"]
            ),
            participants=by_match[m["id"]],
        )
        for m in matches
    ]


@router.get(
    "/orgs/{organization_id}/sessions/{session_id}/matches",
    response_model=list[MatchOut],
)
async def list_matches(
    session_id: str,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[MatchOut]:
    """2026-07-21 (deltaker-tilgang-runden): fjernet get_authorized_org sin
    blanke org-medlemskapssperre -- erstattet med et eksplisitt OR
    (org-medlem ELLER deltaker i turneringen økten hører til), samme
    begrunnelse som scoring.py sin get_scorecard."""
    async with org_connection(organization_id) as conn:
        tournament_id = await conn.fetchval("SELECT tournament_id::text FROM session WHERE id = $1", session_id)
        if tournament_id is None:
            raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
        if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_tournament_participant(
            conn, organization_id, tournament_id, user.user_id
        ):
            raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne turneringen.")
        return await fetch_matches(conn, organization_id, session_id, user.user_id)


class ParticipantCreate(BaseModel):
    team_side: str = Field(pattern="^[ab]$")
    team_roster_id: str
    tee_id: str
    # Money Ball (2026-07-30) -- KUN meningsfullt for session.format=
    # 'money_ball'. 0-3, unik per (match, side).
    lineup_order: int | None = Field(default=None, ge=0, le=3)


@router.post(
    "/orgs/{organization_id}/matches/{match_id}/participants",
    response_model=MatchParticipantOut,
    status_code=201,
)
async def add_participant(
    match_id: str,
    body: ParticipantCreate,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> MatchParticipantOut:
    async with org_connection(organization_id) as conn, translate_db_errors():
        match = await conn.fetchrow(
            """
            SELECT session_id::text AS session_id, team_a_id::text AS team_a_id,
                   team_b_id::text AS team_b_id
            FROM match WHERE id = $1
            """,
            match_id,
        )
        if match is None:
            raise app_error(404, "NOT_FOUND", "Matchen finnes ikke.")

        expected_team_id = match["team_a_id"] if body.team_side == "a" else match["team_b_id"]

        roster_team_id = await conn.fetchval(
            "SELECT team_id::text FROM team_roster WHERE id = $1", body.team_roster_id
        )
        if roster_team_id is None:
            raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
        if roster_team_id != expected_team_id:
            raise app_error(
                400, "MISMATCHED_SIDE", "team_roster_id tilhører ikke laget på angitt side i denne matchen."
            )

        session = await conn.fetchrow(
            """
            SELECT course_id::text AS course_id, format,
                   allowance_override::text AS allowance_override
            FROM session WHERE id = $1
            """,
            match["session_id"],
        )
        tee_course_id = await conn.fetchval(
            "SELECT course_id::text FROM tee WHERE id = $1", body.tee_id
        )
        if tee_course_id is None or tee_course_id != session["course_id"]:
            raise app_error(400, "OUT_OF_SCOPE", "tee_id tilhører ikke øktens bane.")

        if not await user_is_team_captain(conn, organization_id, expected_team_id, user.user_id):
            raise app_error(
                403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
            )

        # Shamble (2026-07-30): variabel lagstørrelse (2-4), IKKE en fast
        # FORMAT_UNIT_SIZE -- compute_and_store_side_handicaps sin vanlige
        # SIDE_IS_UNIT-kapasitetssjekk gjelder ikke her (Shamble aliaser til
        # "singles", individuell enhet, telle-agnostisk). Håndhever derfor
        # kun et TAK her (min-2-sjekk finnes bevisst ikke ennå for org-
        # lagturneringer -- ingen tilsvarende "er oppsettet komplett"-gate
        # finnes for individuelle formater der i dag, samme som fourball).
        if session["format"] == "shamble":
            side_count = await conn.fetchval(
                "SELECT count(*) FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side",
                match_id,
                body.team_side,
            )
            if side_count >= 4:
                raise app_error(409, "LIMIT_REACHED", "Et Shamble-lag kan maks ha 4 spillere.")

        # Money Ball (2026-07-30): FAST 4 -- ikke fleksibel som Shamble.
        # lineup_order (0-3) er PÅKREVD her (avgjør rotasjonen), unik per
        # (match, side) -- håndhevet eksplisitt FØR innsetting, ikke kun
        # via DB-constrainten, for en tydelig feilmelding.
        if session["format"] == "money_ball":
            side_count = await conn.fetchval(
                "SELECT count(*) FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side",
                match_id,
                body.team_side,
            )
            if side_count >= 4:
                raise app_error(409, "LIMIT_REACHED", "Et Money Ball-lag kan maks ha 4 spillere.")
            if body.lineup_order is None:
                raise app_error(400, "VALIDATION_FAILED", "lineup_order er påkrevd for Money Ball.")
            taken = await conn.fetchval(
                "SELECT 1 FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side AND lineup_order = $3",
                match_id,
                body.team_side,
                body.lineup_order,
            )
            if taken:
                raise app_error(409, "DUPLICATE", "En annen spiller på laget har allerede denne plassen i rekkefølgen.")

        locked = await locked_team_ids(conn, match["session_id"])
        if expected_team_id in locked:
            raise app_error(
                409, "ALREADY_LOCKED", "Laget har allerede låst oppstillingen for denne økten."
            )

        # Beregnet HELT FØR innsetting -- se app/handicap.py sin
        # compute_and_store_side_handicaps, som ellers krasjer rått
        # (TypeError i handicap_engine.py sin course_handicap_raw) på en
        # spiller uten handicap_index når use_handicap er på (standard).
        # Fant under scratch-testing av blind draw-skjermen -- reell,
        # preeksisterende feil, ikke noe denne runden introduserte.
        allowance_override = (
            json.loads(session["allowance_override"]) if session["allowance_override"] else None
        )
        config = parse_allowance_config(session["format"], allowance_override)
        if config.use_handicap:
            roster_row = await conn.fetchrow(
                """
                SELECT tr.handicap_index_snapshot, p.gender
                FROM team_roster tr
                JOIN player p ON p.id = tr.player_id
                WHERE tr.id = $1
                """,
                body.team_roster_id,
            )
            if roster_row["handicap_index_snapshot"] is None:
                raise app_error(
                    400,
                    "VALIDATION_FAILED",
                    "Denne spilleren mangler handicap-indeks -- sett en verdi på spilleren "
                    "før hen kan legges til i en handicap-basert økt.",
                )
            # ADR-029: kjønn hører til RATINGEN (tee_rating), ikke selve
            # utslaget -- et utslag kan mangle rating for ett kjønn (klubben
            # har bevisst ikke slopet det for det kjønnet). Sjekket her, FØR
            # innsetting, av samme "fail loudly"-grunn som sjekken over.
            if roster_row["gender"] is None:
                raise app_error(
                    400,
                    "VALIDATION_FAILED",
                    "Denne spilleren mangler registrert kjønn -- sett kjønn på spilleren "
                    "før hen kan legges til i en handicap-basert økt (trengs for å finne "
                    "riktig tee-rating).",
                )
            has_rating = await conn.fetchval(
                """
                SELECT EXISTS(
                    SELECT 1 FROM tee_rating
                    WHERE tee_id = $1 AND scope = 'full_18' AND gender = $2
                )
                """,
                body.tee_id,
                roster_row["gender"],
            )
            if not has_rating:
                kjonn_tekst = "dame" if roster_row["gender"] == "f" else "herre"
                raise app_error(
                    400,
                    "VALIDATION_FAILED",
                    f"Dette utslaget har ingen {kjonn_tekst}-rating -- velg et annet utslag.",
                )

        row = await conn.fetchrow(
            """
            WITH inserted AS (
                INSERT INTO match_participant
                    (organization_id, match_id, team_side, team_roster_id, tee_id, lineup_order)
                VALUES ($1, $2, $3, $4, $5, $6)
                RETURNING id, team_side, team_roster_id, tee_id, lineup_order
            )
            SELECT inserted.id::text, inserted.team_side::text,
                   inserted.team_roster_id::text, p.display_name AS player_name,
                   inserted.tee_id::text, tee.name AS tee_name, inserted.lineup_order
            FROM inserted
            JOIN team_roster tr ON tr.id = inserted.team_roster_id
            JOIN player p ON p.id = tr.player_id
            JOIN tee ON tee.id = inserted.tee_id
            """,
            organization_id,
            match_id,
            body.team_side,
            body.team_roster_id,
            body.tee_id,
            body.lineup_order,
        )

        # Beregn/lagre course_handicap/playing_handicap nå som deltakeren er
        # satt inn. Funksjonen avgjør selv om siden er "klar" (singles/
        # fourball: alltid; foursome/greensome/scramble: kun når siden er
        # komplett) -- se app/handicap.py. `config` allerede parset over.
        await compute_and_store_side_handicaps(
            conn, match_id, body.team_side, session["format"], config
        )
        # row ble hentet FØR beregningen over -- les playing_handicap på nytt
        # slik at responsen ikke lyver om en verdi som faktisk finnes.
        playing_handicap = await conn.fetchval(
            "SELECT playing_handicap FROM match_participant WHERE id = $1::uuid", row["id"]
        )
    return MatchParticipantOut(**dict(row), playing_handicap=playing_handicap)


@router.delete(
    "/orgs/{organization_id}/matches/{match_id}/participants/{participant_id}",
    status_code=204,
)
async def remove_participant(
    match_id: str,
    participant_id: str,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> None:
    """Fjern et deltaker-valg FØR laget har låst -- lar en kaptein endre seg
    (velge en annen spiller/tee for samme plass) uten å etterlate en
    foreldreløs rad, siden det ikke finnes noen PATCH på match_participant.
    Reist av brukeren rett før blind draw-skjermen (som lar spiller/tee
    endres fritt i UI-et helt frem til låsing)."""
    async with org_connection(organization_id) as conn, translate_db_errors():
        row = await conn.fetchrow(
            """
            SELECT mp.team_side::text AS team_side,
                   m.session_id::text AS session_id,
                   m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id
            FROM match_participant mp
            JOIN match m ON m.id = mp.match_id
            WHERE mp.id = $1 AND mp.match_id = $2
            """,
            participant_id,
            match_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke.")

        team_id = row["team_a_id"] if row["team_side"] == "a" else row["team_b_id"]

        if not await user_is_team_captain(conn, organization_id, team_id, user.user_id):
            raise app_error(
                403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
            )

        locked = await locked_team_ids(conn, row["session_id"])
        if team_id in locked:
            raise app_error(
                409, "ALREADY_LOCKED", "Laget har allerede låst oppstillingen for denne økten."
            )

        await conn.execute("DELETE FROM match_participant WHERE id = $1", participant_id)


class LockCreate(BaseModel):
    team_id: str


class LockOut(BaseModel):
    team_id: str
    locked_at: str


@router.post(
    "/orgs/{organization_id}/sessions/{session_id}/lock",
    response_model=LockOut,
    status_code=201,
)
async def lock_lineup(
    session_id: str,
    body: LockCreate,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> LockOut:
    async with org_connection(organization_id) as conn, translate_db_errors():
        session = await conn.fetchrow("SELECT id FROM session WHERE id = $1", session_id)
        if session is None:
            raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")

        if not await user_is_team_captain(conn, organization_id, body.team_id, user.user_id):
            raise app_error(
                403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
            )

        # UNIQUE(session_id, team_id) gir 409 via translate_db_errors ved
        # dobbel-lås — ingen manuell sjekk nødvendig.
        row = await conn.fetchrow(
            """
            INSERT INTO lineup_lock (organization_id, session_id, team_id, locked_by)
            VALUES ($1, $2, $3, $4)
            RETURNING team_id::text, locked_at::text
            """,
            organization_id,
            session_id,
            body.team_id,
            user.user_id,
        )
    return LockOut(**dict(row))
