"""
Egendefinerte baner på organisasjonsnivå (ADR-001: gjenbrukbar på tvers av
turneringer, samme mønster som players.py).

`course.source = 'custom'` er den eneste veien inn her -- 'official' (ekte
teeoff-baner via ADR-004 sitt planlagte, lesende API) er fortsatt kun vedtatt,
ikke bygget (se FEATURE_BACKLOG.md), og krever uansett `external_course_ref`
satt (skjemaets CHECK-constraint), som ingen klient kan oppgi meningsfullt
ennå. Denne runden dekker KUN det organisatoren trenger for å opprette en
økt: en bane ved navn, ingen hull-/tee-/rating-detaljer ennå (session-motoren
bruker foreløpig kun course_id som fremmednøkkel, ikke banens innhold).
"""

from fastapi import APIRouter, Depends, Query

from .. import golfapi_cache, golfapi_client, teeoff_client
from ..auth import CurrentUser, get_authorized_org, get_current_user
from ..config import settings
from ..db import org_connection
from ..errors import app_error, translate_db_errors
from ..team_authz import is_org_member, user_is_org_player
from pydantic import BaseModel, Field

router = APIRouter()


class Course(BaseModel):
    id: str
    name: str
    source: str


_COURSE_COLUMNS = "id::text, name, source::text AS source"


@router.get("/orgs/{organization_id}/courses", response_model=list[Course])
async def list_courses(
    organization_id: str = Depends(get_authorized_org),
) -> list[Course]:
    async with org_connection(organization_id) as conn:
        rows = await conn.fetch(f"SELECT {_COURSE_COLUMNS} FROM course ORDER BY name")
    return [Course(**dict(r)) for r in rows]


# --- Tee-er (ADR-013-relatert: match_participant.tee_id krever en ekte tee) --
#
# Manglet helt frem til nå -- verken en liste-vei (selv offisielt importerte
# baner har tee-rader, men ingenting eksponerte dem) eller en opprett-vei for
# egendefinerte baner (de har ALDRI hatt noen vei til tee-data, uavhengig av
# ADR-019). Reist av brukeren rett før blind draw-skjermen: uten dette kan en
# match aldri få en deltaker (tee_id er påkrevd, ingen gyldig verdi finnes).
# Kun full_18-rating dekket her, samme begrunnelse som ADR-019 Beslutning D.
# Hull-/stroke-index-data for egendefinerte baner (trengs for korrekt
# slagfordeling i SCORING-fasen, ikke i blind draw) er bevisst UTENFOR
# omfang denne runden -- egen, senere sak når scorekort-skjermen bygges.


class TeeRatingInput(BaseModel):
    gender: str = Field(pattern="^[mf]$")
    course_rating: float = Field(gt=0)
    slope_rating: int = Field(ge=55, le=155)
    par: int = Field(ge=27, le=90)


class TeeCreate(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    # ADR-029: kjønn hører til RATINGEN, ikke selve utslaget -- ett fysisk
    # utslag kan ha én rating per kjønn (aldri to av samme), organisator
    # oppgir hvilke som faktisk finnes for banen (ofte begge, noen ganger
    # bare én -- klubben har bevisst ikke slopet den andre).
    ratings: list[TeeRatingInput] = Field(min_length=1, max_length=2)


class TeeRatingOut(BaseModel):
    gender: str
    course_rating: float
    slope_rating: int
    par: int


class Tee(BaseModel):
    id: str
    name: str
    ratings: list[TeeRatingOut]


@router.get("/orgs/{organization_id}/courses/{course_id}/tees", response_model=list[Tee])
async def list_tees(
    course_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> list[Tee]:
    async with org_connection(organization_id) as conn:
        rows = await conn.fetch(
            """
            SELECT t.id::text AS id, t.name,
                   tr.gender, tr.course_rating::float AS course_rating,
                   tr.slope_rating, tr.par
            FROM tee t
            JOIN tee_rating tr ON tr.tee_id = t.id AND tr.scope = 'full_18'
            WHERE t.course_id = $1
            ORDER BY t.name, tr.gender
            """,
            course_id,
        )
    tees: dict[str, Tee] = {}
    for r in rows:
        tee = tees.setdefault(r["id"], Tee(id=r["id"], name=r["name"], ratings=[]))
        tee.ratings.append(
            TeeRatingOut(
                gender=r["gender"],
                course_rating=r["course_rating"],
                slope_rating=r["slope_rating"],
                par=r["par"],
            )
        )
    return list(tees.values())


@router.post("/orgs/{organization_id}/courses/{course_id}/tees", response_model=Tee, status_code=201)
async def create_tee(
    course_id: str,
    body: TeeCreate,
    organization_id: str = Depends(get_authorized_org),
) -> Tee:
    genders = [r.gender for r in body.ratings]
    if len(genders) != len(set(genders)):
        raise app_error(
            400, "VALIDATION_FAILED", "Kan ikke oppgi to ratinger for samme kjønn på ett utslag."
        )

    async with org_connection(organization_id) as conn, translate_db_errors():
        source = await conn.fetchval("SELECT source FROM course WHERE id = $1", course_id)
        if source is None:
            raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
        if source == "official":
            raise app_error(
                400,
                "VALIDATION_FAILED",
                "Offisielle baner får tee-ene sine fra teeoff -- kan ikke legges til manuelt.",
            )

        tee_row = await conn.fetchrow(
            "INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id::text",
            organization_id,
            course_id,
            body.name,
        )
        ratings_out: list[TeeRatingOut] = []
        for r in body.ratings:
            await conn.execute(
                """
                INSERT INTO tee_rating
                    (organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
                VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
                """,
                organization_id,
                tee_row["id"],
                r.gender,
                r.course_rating,
                r.slope_rating,
                r.par,
            )
            ratings_out.append(
                TeeRatingOut(
                    gender=r.gender, course_rating=r.course_rating, slope_rating=r.slope_rating, par=r.par
                )
            )
    return Tee(id=tee_row["id"], name=body.name, ratings=ratings_out)


# --- Hull (ADR-008-relatert: stroke_index kreves for slagfordeling i         --
#     scoring-fasen -- se app/routers/scoring.py sin bruk av `hole`-tabellen) -
#
# Egendefinerte baner har ALDRI hatt noen vei til hull-data, uavhengig av
# ADR-019 (samme klasse hull som tee-mangelen -- flagget FØR scorekort-
# skjermen ble bygget, ikke oppdaget for sent). Kun `stroke`-scoringsmodus
# trenger dette; `hole_result`-modus fungerer allerede uten. Engangs
# oppsett: enten alle 18 hull settes i ÉN innsending, eller ingen -- unngår
# kompleksiteten med delvise/partielle hull-sett.


class HoleCreate(BaseModel):
    hole_number: int = Field(ge=1, le=18)
    par: int = Field(ge=3, le=6)
    stroke_index: int = Field(ge=1, le=18)


class Hole(BaseModel):
    hole_number: int
    par: int
    stroke_index: int


@router.get("/orgs/{organization_id}/courses/{course_id}/holes", response_model=list[Hole])
async def list_holes(
    course_id: str,
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> list[Hole]:
    """2026-07-21 (deltaker-tilgang-runden): org-medlem ELLER en bruker
    koblet til NOEN spillerprofil i organisasjonen (`user_is_org_player`,
    bevisst løsere enn en full turnering-deltaker-sjekk -- par/stroke-index
    er lavsensitiv banedata, ikke spillerdata, og en bane er ikke bundet til
    én bestemt turnering)."""
    async with org_connection(organization_id) as conn:
        if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_org_player(
            conn, organization_id, user.user_id
        ):
            raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne organisasjonen.")
        rows = await conn.fetch(
            "SELECT hole_number, par, stroke_index FROM hole WHERE course_id = $1 ORDER BY hole_number",
            course_id,
        )
    return [Hole(**dict(r)) for r in rows]


@router.post("/orgs/{organization_id}/courses/{course_id}/holes", response_model=list[Hole], status_code=201)
async def create_holes(
    course_id: str,
    body: list[HoleCreate],
    organization_id: str = Depends(get_authorized_org),
) -> list[Hole]:
    if len(body) != 18:
        raise app_error(400, "VALIDATION_FAILED", "Må oppgi nøyaktig 18 hull i én innsending.")
    numbers = sorted(h.hole_number for h in body)
    if numbers != list(range(1, 19)):
        raise app_error(400, "VALIDATION_FAILED", "Hullnumrene må dekke nøyaktig 1-18, hver én gang.")

    async with org_connection(organization_id) as conn, translate_db_errors():
        source = await conn.fetchval("SELECT source FROM course WHERE id = $1", course_id)
        if source is None:
            raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
        if source == "official":
            raise app_error(
                400,
                "VALIDATION_FAILED",
                "Offisielle baner får hullene sine fra teeoff -- kan ikke legges til manuelt.",
            )
        existing = await conn.fetchval("SELECT count(*) FROM hole WHERE course_id = $1", course_id)
        if existing > 0:
            raise app_error(
                409, "DUPLICATE", "Banen har allerede registrert hull -- kan ikke settes opp på nytt."
            )

        rows = []
        for h in body:
            rows.append(
                await conn.fetchrow(
                    """
                    INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
                    VALUES ($1, $2, $3, $4, $5)
                    RETURNING hole_number, par, stroke_index
                    """,
                    organization_id,
                    course_id,
                    h.hole_number,
                    h.par,
                    h.stroke_index,
                )
            )
    return sorted((Hole(**dict(r)) for r in rows), key=lambda h: h.hole_number)


# --- Manuell baneoppretting med fullt hull-/utslagoppsett (2026-08-04) ------
#
# `create_course` over tok frem til nå KUN imot et navn -- ingen vei til
# hull/utslag fantes for en manuelt opprettet org-bane (kun de to ubrukte
# sub-ressurs-endepunktene over, `create_tee`/`create_holes`, som ALDRI fikk
# noe frontend-kallsted). Brukeren oppdaget dette hullet: "opprett manuell
# bane" i turneringsmodulen var i praksis ubrukelig uten et sted å taste inn
# hull/par/hcp-indeks/utslag etterpå.
#
# Denne utvidede varianten dekker begge deler i ÉN innsending (speiler
# `PersonalCourseCreate` i rounds.py sin form), OG publiserer samtidig en
# kopi til det delte, plattform-omfattende bane-biblioteket
# (`personal_course`, 020_personal_rounds.sql) -- brukeren bekreftet
# eksplisitt at custom-baner opprettet via den nye mal-baserte flyten skal
# "lagres og være offentlige", slik at samme bane kan brukes som mal av
# andre organisasjoner OG i single-runde-modulen. `personal_course` har
# ingen RLS/organization_id (bevisst globalt siden migrasjon 020) -- å skrive
# til den fra en org-scopet tilkobling er trygt, ingen policy er i veien.


class CourseCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    # Enten BEGGE er med, eller INGEN -- ufullstendig oppsett (kun holes,
    # ikke tees) gir en tydelig 400 heller enn en course-rad som stille
    # mangler halve dataene.
    holes: list[HoleCreate] | None = None
    tees: list[TeeCreate] | None = None
    # Satt når banen opprettes med en offentlig custom-bane (personal_course)
    # som mal -- ren attribusjon på den NYE personal_course-publiseringen
    # under, ingen vedvarende kobling på selve org-bane-raden (samme
    # "engangs-kopi"-filosofi som offisiell TeeOff-import, ADR-019).
    template_personal_course_id: str | None = None


@router.post("/orgs/{organization_id}/courses", response_model=Course, status_code=201)
async def create_course(
    body: CourseCreate,
    organization_id: str = Depends(get_authorized_org),
    user: CurrentUser = Depends(get_current_user),
) -> Course:
    if (body.holes is None) != (body.tees is None):
        raise app_error(400, "VALIDATION_FAILED", "Hull og utslag må enten begge være med, eller ingen av dem.")
    if body.holes is not None and body.tees is not None:
        numbers = sorted(h.hole_number for h in body.holes)
        if len(body.holes) != 18 or numbers != list(range(1, 19)):
            raise app_error(400, "VALIDATION_FAILED", "Må oppgi nøyaktig 18 hull, nummerert 1-18.")
        for t in body.tees:
            genders = [r.gender for r in t.ratings]
            if len(genders) != len(set(genders)):
                raise app_error(
                    400, "VALIDATION_FAILED", "Kan ikke oppgi to ratinger for samme kjønn på ett utslag."
                )

    async with org_connection(organization_id) as conn, translate_db_errors():
        async with conn.transaction():
            row = await conn.fetchrow(
                f"""
                INSERT INTO course (organization_id, name, source)
                VALUES ($1, $2, 'custom')
                RETURNING {_COURSE_COLUMNS}
                """,
                organization_id,
                body.name,
            )
            if body.holes is not None and body.tees is not None:
                for h in body.holes:
                    await conn.execute(
                        """
                        INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
                        VALUES ($1, $2, $3, $4, $5)
                        """,
                        organization_id,
                        row["id"],
                        h.hole_number,
                        h.par,
                        h.stroke_index,
                    )
                for t in body.tees:
                    tee_row = await conn.fetchrow(
                        "INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
                        organization_id,
                        row["id"],
                        t.name,
                    )
                    for r in t.ratings:
                        await conn.execute(
                            """
                            INSERT INTO tee_rating
                                (organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
                            VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
                            """,
                            organization_id,
                            tee_row["id"],
                            r.gender,
                            r.course_rating,
                            r.slope_rating,
                            r.par,
                        )

                pc_row = await conn.fetchrow(
                    """
                    INSERT INTO personal_course (name, created_by_user_id, forked_from_id)
                    VALUES ($1, $2, $3) RETURNING id
                    """,
                    body.name,
                    user.user_id,
                    body.template_personal_course_id,
                )
                for h in body.holes:
                    await conn.execute(
                        """
                        INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index)
                        VALUES ($1, $2, $3, $4)
                        """,
                        pc_row["id"],
                        h.hole_number,
                        h.par,
                        h.stroke_index,
                    )
                for t in body.tees:
                    pc_tee_row = await conn.fetchrow(
                        "INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
                        pc_row["id"],
                        t.name,
                    )
                    for r in t.ratings:
                        await conn.execute(
                            """
                            INSERT INTO personal_course_tee_rating
                                (personal_course_tee_id, gender, course_rating, slope_rating, par)
                            VALUES ($1, $2, $3, $4, $5)
                            """,
                            pc_tee_row["id"],
                            r.gender,
                            r.course_rating,
                            r.slope_rating,
                            r.par,
                        )
    return Course(**dict(row))


# --- Offisiell banedata fra teeoff (ADR-019) ---------------------------------


class OfficialFacility(BaseModel):
    slug: str
    name: str
    city: str | None = None
    county: str | None = None


class OfficialCourseOption(BaseModel):
    teeoff_course_id: int
    name: str
    is_main_course: bool


class OfficialFacilityDetail(BaseModel):
    slug: str
    name: str
    courses: list[OfficialCourseOption]


class OfficialCourseImport(BaseModel):
    facility_slug: str
    teeoff_course_id: int


@router.get("/orgs/{organization_id}/courses/official-search", response_model=list[OfficialFacility])
async def search_official_courses(
    q: str = Query(default=""),
    organization_id: str = Depends(get_authorized_org),
) -> list[OfficialFacility]:
    try:
        facilities = await teeoff_client.search_facilities(q)
    except teeoff_client.TeeoffUnavailableError:
        raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra teeoff akkurat nå.")
    return [
        OfficialFacility(slug=f["slug"], name=f["name"], city=f.get("city"), county=f.get("county"))
        for f in facilities
    ]


@router.get(
    "/orgs/{organization_id}/courses/official-search/{slug}",
    response_model=OfficialFacilityDetail,
)
async def get_official_facility(
    slug: str,
    organization_id: str = Depends(get_authorized_org),
) -> OfficialFacilityDetail:
    try:
        facility = await teeoff_client.get_facility(slug)
    except teeoff_client.TeeoffNotFoundError:
        raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
    except teeoff_client.TeeoffUnavailableError:
        raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")

    courses = [
        OfficialCourseOption(
            teeoff_course_id=c["id"],
            name=c["name"],
            is_main_course=bool(c.get("is_main_course")),
        )
        # Kun 18-hulls baner kan importeres foreløpig (ADR-019) -- filtrert
        # her slik at organisator aldri ser et valg som senere feiler.
        for c in facility.get("courses", [])
        if len(c.get("holes") or []) == 18
    ]
    return OfficialFacilityDetail(slug=facility["slug"], name=facility["name"], courses=courses)


@router.post("/orgs/{organization_id}/courses/official-import", response_model=Course, status_code=201)
async def import_official_course(
    body: OfficialCourseImport,
    organization_id: str = Depends(get_authorized_org),
) -> Course:
    external_ref = f"{body.facility_slug}:{body.teeoff_course_id}"

    # Idempotent: importerer organisatoren samme bane på nytt (vanlig --
    # flere økter spilles ofte på samme bane), gi tilbake den EKSISTERENDE
    # raden i stedet for en 409-feil. Sjekket FØR noe teeoff-kall gjøres,
    # både raskere og mer robust mot at teeoff er nede akkurat da. Fant
    # og fikset 2026-07-18 etter en reell brukerrapport: duplikat-sperren
    # (migrasjon 010) var riktig i prinsippet, men ga ingen vei videre for
    # den helt vanlige "bruk samme bane på runde 2" siden av saken.
    async with org_connection(organization_id) as conn:
        existing = await conn.fetchrow(
            f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1",
            external_ref,
        )
        if existing is not None:
            return Course(**dict(existing))

    try:
        facility = await teeoff_client.get_facility(body.facility_slug)
    except teeoff_client.TeeoffNotFoundError:
        raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
    except teeoff_client.TeeoffUnavailableError:
        raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")

    course_data = next(
        (c for c in facility.get("courses", []) if c.get("id") == body.teeoff_course_id), None
    )
    if course_data is None:
        raise app_error(404, "NOT_FOUND", "Banen finnes ikke på dette anlegget i teeoff.")

    holes = course_data.get("holes") or []
    tees = course_data.get("tees") or []

    # ADR-019 Beslutning C: hele importen feiler tydelig FØR noe skrives,
    # aldri en delvis importert bane som senere feiler i handicap-beregning.
    if len(holes) != 18:
        raise app_error(
            400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull i teeoff ennå."
        )
    for h in holes:
        if h.get("par") is None or h.get("hcp_index") is None:
            raise app_error(
                400,
                "EXTERNAL_DATA_INCOMPLETE",
                "Banen mangler par eller HCP-index på ett eller flere hull i teeoff.",
            )

    # ADR-019 Beslutning D: kun full_18-rating importeres, teeoff har ingen
    # egen front9/back9-rating. ADR-029: kjønn hører til RATINGEN, ikke selve
    # utslaget -- ett teeoff-utslag blir HER importert som ÉN fysisk
    # `tee`-rad med inntil to kjønnsspesifikke ratinger (tidligere ble dette
    # feilaktig importert som to separate tee-rader, én per kjønn).
    tee_inputs: list[tuple[str, list[tuple[str, float, int]]]] = []
    for t in tees:
        name = t.get("name") or "Tee"
        ratings: list[tuple[str, float, int]] = []
        if t.get("cr_men") is not None and t.get("slope_men") is not None:
            ratings.append(("m", float(t["cr_men"]), int(t["slope_men"])))
        if t.get("cr_women") is not None and t.get("slope_women") is not None:
            ratings.append(("f", float(t["cr_women"]), int(t["slope_women"])))
        if ratings:
            tee_inputs.append((name, ratings))
    if not tee_inputs:
        raise app_error(
            400,
            "EXTERNAL_DATA_INCOMPLETE",
            "Banen har ingen tee med registrert rating/slope i teeoff ennå.",
        )

    par_total = sum(h["par"] for h in holes)

    # Navn kombinerer anlegg + bane -- "Hovedbanen" alene er tvetydig (mange
    # klubber navngir hovedbanen sin nøyaktig likt). Fant og fikset samme
    # runde som idempotent-fiksen over, samme bakenforliggende brukerrapport.
    # UNNTAK (2026-07-25, brukerrapport om Tjøme Golfklubb): har anlegget
    # bare ÉN bane totalt, er banenavnet ("Hovedbanen") overflødig
    # informasjon -- kun anleggsnavnet brukes da.
    if len(facility.get("courses", [])) == 1:
        course_name = facility.get("name") or "Bane"
    else:
        course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"

    async with org_connection(organization_id) as conn, translate_db_errors():
        course_row = await conn.fetchrow(
            f"""
            INSERT INTO course (organization_id, name, source, external_course_ref)
            VALUES ($1, $2, 'official', $3)
            RETURNING {_COURSE_COLUMNS}
            """,
            organization_id,
            course_name,
            external_ref,
        )
        course_id = course_row["id"]

        for h in holes:
            await conn.execute(
                """
                INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
                VALUES ($1, $2, $3, $4, $5)
                """,
                organization_id,
                course_id,
                h["hole_number"],
                h["par"],
                h["hcp_index"],
            )

        for name, ratings in tee_inputs:
            tee_row = await conn.fetchrow(
                "INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
                organization_id,
                course_id,
                name,
            )
            for gender, course_rating, slope_rating in ratings:
                await conn.execute(
                    """
                    INSERT INTO tee_rating
                        (organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
                    VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
                    """,
                    organization_id,
                    tee_row["id"],
                    gender,
                    course_rating,
                    slope_rating,
                    par_total,
                )

    return Course(**dict(course_row))


# --- Rangefinder-koordinater for offisielle baner (migrasjon 079) -----------
#
# Frem til nå har rangefinder-koordinater (grønn/hindringer, GPS-basert) KUN
# eksistert for GolfAPI-importerte baner. Offisielle (teeoff-koblede) baner
# har ALDRI hatt noen koordinatkilde -- teeoff selv har ingen slik data, og
# det finnes ingen automatisk importvei (i motsetning til GolfAPI). Dette er
# derfor en RENT MANUELL skrive-vei: selve tolkningsarbeidet (fritekst-navn
# på feltbefarte punkter -> poi_type/location/side_fairway) krever
# dømmekraft og skjer FØR kallet hit, ikke automatisert her (bekreftet
# konkret av Tjøme-runden -- "Voll"/"Bjella" kunne ikke vært gjettet av et
# script). Full erstatning (ikke delta) -- speiler hvordan koordinatsett
# faktisk oppstår i praksis (en ny feltbefaring erstatter den forrige, ikke
# et lite tillegg til den). Lagres GLOBALT (teeoff_course_coordinate, nøkkel
# course.external_course_ref) -- IKKE per organisasjon, samme begrunnelse
# som golfapi_course_coordinate: flere organisasjoner kan importere samme
# fysiske bane fra teeoff hver sin course-rad, koordinatene skal deles.


class CoursePointIn(BaseModel):
    hole_number: int = Field(ge=1, le=18)
    poi_type: str
    location: str | None = None
    side_fairway: str | None = None
    latitude: float = Field(ge=-90, le=90)
    longitude: float = Field(ge=-180, le=180)
    # Eksplisitt, menneske-satt paring av forkant/bakkant til SAMME
    # fysiske hindring (migrasjon 080/ADR-084) -- se hazard_group-
    # kommentaren i migrasjonsfilen. Frivillig, ingen validering utover
    # tekst -- kun meningsfullt unikt innenfor ett hull.
    hazard_group: str | None = None


class CoursePointOut(BaseModel):
    hole_number: int
    poi_type: str
    location: str | None
    side_fairway: str | None
    latitude: float
    longitude: float
    hazard_group: str | None


_COORDINATE_COLUMNS = "hole_number, poi_type, location, side_fairway, latitude, longitude, hazard_group"


@router.get("/orgs/{organization_id}/courses/{course_id}/coordinates", response_model=list[CoursePointOut])
async def list_course_coordinates(
    course_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> list[CoursePointOut]:
    async with org_connection(organization_id) as conn:
        course_row = await conn.fetchrow("SELECT source, external_course_ref FROM course WHERE id = $1", course_id)
        if course_row is None:
            raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
        if course_row["source"] != "official" or course_row["external_course_ref"] is None:
            return []
        rows = await conn.fetch(
            f"""
            SELECT {_COORDINATE_COLUMNS} FROM teeoff_course_coordinate
            WHERE external_course_ref = $1 ORDER BY hole_number
            """,
            course_row["external_course_ref"],
        )
    return [CoursePointOut(**dict(r)) for r in rows]


@router.put("/orgs/{organization_id}/courses/{course_id}/coordinates", response_model=list[CoursePointOut])
async def replace_course_coordinates(
    course_id: str,
    body: list[CoursePointIn],
    organization_id: str = Depends(get_authorized_org),
) -> list[CoursePointOut]:
    async with org_connection(organization_id) as conn, translate_db_errors():
        course_row = await conn.fetchrow("SELECT source, external_course_ref FROM course WHERE id = $1", course_id)
        if course_row is None:
            raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
        if course_row["source"] != "official":
            raise app_error(
                400,
                "VALIDATION_FAILED",
                "Kun offisielle (TeeOff-koblede) baner kan få rangefinder-koordinater satt manuelt.",
            )
        external_ref = course_row["external_course_ref"]

        await conn.execute("DELETE FROM teeoff_course_coordinate WHERE external_course_ref = $1", external_ref)
        for p in body:
            await conn.execute(
                """
                INSERT INTO teeoff_course_coordinate
                    (external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude, hazard_group)
                VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
                """,
                external_ref, p.hole_number, p.poi_type, p.location, p.side_fairway, p.latitude, p.longitude, p.hazard_group,
            )
        rows = await conn.fetch(
            f"""
            SELECT {_COORDINATE_COLUMNS} FROM teeoff_course_coordinate
            WHERE external_course_ref = $1 ORDER BY hole_number
            """,
            external_ref,
        )
    return [CoursePointOut(**dict(r)) for r in rows]


# --- Internasjonal banedata fra GolfAPI.io (ADR-064) -------------------------
#
# Tredje banekilde, for baner utenfor TeeOffs dekning -- Tjøme Golfklubb var
# den konkrete anledningen (bekreftet IKKE i TeeOff 2026-08-12). ULIKT
# TeeOff-importen over: GolfAPI-kall koster ekte, begrenset budsjett, derfor
# går ALL henting via golfapi_cache.get_or_fetch_golfapi_course() -- aldri
# et direkte golfapi_client-kall herfra. Samme "kopier inn, aldri live
# oppslag"-filosofi og samme "fail loudly on ufullstendige data"-prinsipp
# som ADR-019, men fra en annen kilde.


class InternationalCourseOption(BaseModel):
    course_id: str
    course_name: str
    num_holes: int
    has_gps: bool


class InternationalClub(BaseModel):
    club_id: str
    club_name: str
    city: str | None = None
    country: str | None = None
    courses: list[InternationalCourseOption]


class InternationalCourseImport(BaseModel):
    golfapi_course_id: str


@router.get("/orgs/{organization_id}/courses/international-search", response_model=list[InternationalClub])
async def search_international_courses(
    q: str = Query(default=""),
    organization_id: str = Depends(get_authorized_org),
) -> list[InternationalClub]:
    if not q.strip():
        return []
    try:
        clubs = await golfapi_client.search_clubs(name=q)
    except golfapi_client.GolfApiNotConfiguredError:
        raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")
    except golfapi_client.GolfApiUnavailableError:
        raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra GolfAPI akkurat nå.")
    return [
        InternationalClub(
            club_id=c["clubID"],
            club_name=c["clubName"],
            city=c.get("city"),
            country=c.get("country"),
            courses=[
                InternationalCourseOption(
                    course_id=course["courseID"],
                    course_name=course.get("courseName") or "Bane",
                    num_holes=int(course["numHoles"]),
                    has_gps=bool(int(course.get("hasGPS") or 0)),
                )
                for course in c.get("courses", [])
            ],
        )
        for c in clubs
    ]


@router.post("/orgs/{organization_id}/courses/international-import", response_model=Course, status_code=201)
async def import_international_course(
    body: InternationalCourseImport,
    organization_id: str = Depends(get_authorized_org),
) -> Course:
    if not settings.GOLFAPI_TOKEN:
        raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")

    # Idempotent, samme begrunnelse som import_official_course over: samme
    # organisasjon importerer ofte samme bane til flere økter.
    async with org_connection(organization_id) as conn:
        existing = await conn.fetchrow(
            f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1 AND source = 'international'",
            body.golfapi_course_id,
        )
        if existing is not None:
            return Course(**dict(existing))

    async with org_connection(organization_id) as conn, translate_db_errors():
        try:
            await golfapi_cache.get_or_fetch_golfapi_course(conn, body.golfapi_course_id)
        except golfapi_client.GolfApiNotFoundError:
            raise app_error(404, "NOT_FOUND", "Banen finnes ikke hos GolfAPI.")
        except golfapi_client.GolfApiUnavailableError:
            raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra GolfAPI akkurat nå.")

        gc = await conn.fetchrow("SELECT club_name, course_name, city FROM golfapi_course WHERE course_id = $1", body.golfapi_course_id)
        holes = await conn.fetch(
            "SELECT hole_number, par_men, index_men FROM golfapi_course_hole WHERE golfapi_course_id = $1 ORDER BY hole_number",
            body.golfapi_course_id,
        )
        tees = await conn.fetch(
            "SELECT name, course_rating_men, slope_men, course_rating_women, slope_women FROM golfapi_course_tee WHERE golfapi_course_id = $1",
            body.golfapi_course_id,
        )

        # ADR-064 (speiler ADR-019 Beslutning C): ufullstendige data feiler
        # importen tydelig FØR noe skrives til course/hole/tee.
        if len(holes) != 18:
            raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull hos GolfAPI.")
        if not tees:
            raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ingen registrerte utslag hos GolfAPI.")
        if not any(
            (t["course_rating_men"] is not None and t["slope_men"] is not None)
            or (t["course_rating_women"] is not None and t["slope_women"] is not None)
            for t in tees
        ):
            raise app_error(
                400, "EXTERNAL_DATA_INCOMPLETE", "Ingen av utslagene har en brukbar rating/slope hos GolfAPI."
            )

        par_total = sum(h["par_men"] for h in holes)
        course_name = f"{gc['club_name']} – {gc['course_name']}" if gc["course_name"] else gc["club_name"]

        course_row = await conn.fetchrow(
            f"""
            INSERT INTO course (organization_id, name, source, external_course_ref)
            VALUES ($1, $2, 'international', $3)
            RETURNING {_COURSE_COLUMNS}
            """,
            organization_id,
            course_name,
            body.golfapi_course_id,
        )
        course_id = course_row["id"]

        for h in holes:
            await conn.execute(
                "INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4, $5)",
                organization_id, course_id, h["hole_number"], h["par_men"], h["index_men"],
            )

        for t in tees:
            # Reelt funn (Nesbyen, 2026-08-13): GolfAPI kan mangle RATING
            # FOR BEGGE kjønn på et utslag (opprinnelig antatt at menns alltid
            # finnes, speilet fra ADR-019s teeoff-import) -- begge sjekkes nå
            # individuelt, samme mønster som teeoff-importen allerede bruker.
            # Utslag uten NOEN brukbar rating hoppes over (usable_tees under).
            ratings: list[tuple[str, float, int]] = []
            if t["course_rating_men"] is not None and t["slope_men"] is not None:
                ratings.append(("m", float(t["course_rating_men"]), int(t["slope_men"])))
            if t["course_rating_women"] is not None and t["slope_women"] is not None:
                ratings.append(("f", float(t["course_rating_women"]), int(t["slope_women"])))
            if not ratings:
                continue
            tee_row = await conn.fetchrow(
                "INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
                organization_id, course_id, t["name"],
            )
            for gender, course_rating, slope_rating in ratings:
                await conn.execute(
                    """
                    INSERT INTO tee_rating (organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
                    VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
                    """,
                    organization_id, tee_row["id"], gender, course_rating, slope_rating, par_total,
                )

    return Course(**dict(course_row))
