"""
DB-bevisst bro mellom handicap_engine.py (ren, uten DB-avhengigheter) og
Postgres. Se ADR-005 (allowance er konfig), ADR-014 (fire uavhengige brytere),
ADR-007 (handicap_index_snapshot fryses i team_roster).

Kjent, bevisst begrensning (se plan): hvis en side aldri når forventet
deltakerantall, eller en deltakers tee mangler en matchende tee_rating-rad,
beregnes handicap for den siden ALDRI automatisk her -- ingen bakgrunnsjobb
prøver på nytt. Dette løses senere (sannsynligvis en organisator-overstyring).
"""

from __future__ import annotations

from dataclasses import dataclass

from asyncpg import Connection

from handicap_engine import (
    AllowanceStrategy,
    CombinedPercentage,
    DEFAULT_MATCHPLAY_ALLOWANCES,
    Format,
    PerPlayerPercentage,
    RankedSplit,
    WeightedLowHigh,
    course_handicap_raw,
    match_play_strokes,
    round_half_up,
)

# Formater der ENHETEN som deler én Playing Handicap er SIDEN (foursome-paret,
# greensome-paret, hele scramble-laget) -- ikke spilleren. Singles/fourball er
# ikke med her: der er enheten spilleren, og beregnes individuelt.
SIDE_IS_UNIT = {"foursome", "greensome", "scramble_2", "scramble_4"}

FORMAT_UNIT_SIZE: dict[str, int] = {
    "singles": 1,
    "fourball": 1,
    "foursome": 2,
    "greensome": 2,
    "scramble_2": 2,
    "scramble_4": 4,
}

# Nye formater denne runden (2026-07-30) som er strukturelt IDENTISKE med et
# eksisterende format -- ingen egen HCP-/allowance-logikk, bare et annet
# selvdeklarert navn. Løses opp HELT FØRST i parse_allowance_config/
# compute_and_store_side_handicaps/relative_strokes_for_match, slik at resten
# av disse funksjonene (og SIDE_IS_UNIT/FORMAT_UNIT_SIZE/Format-enumet)
# aldri ser aliasnavnet. Chapman/Pinehurst ("Try all", bekreftet av bruker):
# delt ball, én score per hull per side, CombinedPercentage 50% -- identisk
# med foursome, kun regler for HVEM som slår hvilket slag (ikke sporet i
# skjemaet, verken her eller for foursome).
ENGINE_FORMAT_ALIASES: dict[str, str] = {
    "chapman": "foursome",
    # Shamble (2026-07-30) -- variabel lagstørrelse (2-4), enheten er
    # SPILLEREN (som fourball/singles), ikke siden -- "singles" sin
    # PerPlayerPercentage(1.00) er telle-agnostisk (regner ett tall per
    # spiller uavhengig av hvor mange andre er på laget), i motsetning til
    # foursome/greensome/scramble sine faste antalls-strategier.
    # Kildens hullscore-regel (beste N av M) bruker rå brutto, ikke HCP-
    # justert netto -- course_handicap/playing_handicap beregnes og lagres
    # likevel her (for visning/et fremtidig steg), men brukes IKKE av
    # selve hullscore-utregningen (se scoring.py sin _side_net shamble-gren).
    "shamble": "singles",
    # Money Ball/Lone Ranger (2026-07-30) -- FAST 4-manns lag, enheten er
    # SPILLEREN (som Shamble/fourball), ikke siden. Rå brutto brukes for
    # selve hullscoren (se scoring.py sin _compute_money_ball_hole_results),
    # course_handicap/playing_handicap beregnes og lagres likevel for
    # visning (samme begrunnelse som Shamble).
    "money_ball": "singles",
    # High-low-high (2026-07-30) -- 2 lag à 2, individuell ball. course_
    # handicap/playing_handicap beregnes som fourball (samme "alle fire
    # samlet i én match_play_strokes-pool"-regel, R&A) -- selve poeng-
    # rangeringen (high/low per hull) er en HELT NY, egen beregning (se
    # app/routers/rounds.py sin _build_high_low_high_result), ikke del av
    # denne aliaseringen.
    "high_low_high": "fourball",
}


@dataclass(frozen=True)
class AllowanceConfig:
    use_handicap: bool
    use_course_handicap: bool
    use_matchplay_handicap: bool
    strategy: AllowanceStrategy


def _strategy_from_json(format_: str, spec: dict | None) -> AllowanceStrategy:
    if spec is None:
        return DEFAULT_MATCHPLAY_ALLOWANCES[Format(format_)]
    kind = spec["type"]
    if kind == "per_player":
        return PerPlayerPercentage(spec["percentage"])
    if kind == "combined":
        return CombinedPercentage(spec["percentage"])
    if kind == "weighted_low_high":
        return WeightedLowHigh(spec["low_weight"], spec["high_weight"])
    if kind == "ranked_split":
        return RankedSplit(tuple(spec["weights"]))
    raise ValueError(f"Ukjent allowance-strategitype: {kind!r}")


def parse_allowance_config(format_: str, allowance_override: dict | None) -> AllowanceConfig:
    """ADR-014: fire uavhengige brytere. Manglende nøkler = på/standard."""
    format_ = ENGINE_FORMAT_ALIASES.get(format_, format_)
    override = allowance_override or {}
    return AllowanceConfig(
        use_handicap=override.get("use_handicap", True),
        use_course_handicap=override.get("use_course_handicap", True),
        use_matchplay_handicap=override.get("use_matchplay_handicap", True),
        strategy=_strategy_from_json(format_, override.get("strategy")),
    )


async def compute_and_store_side_handicaps(
    conn: Connection,
    match_id: str,
    team_side: str,
    format_: str,
    config: AllowanceConfig,
) -> None:
    """Beregn og lagre course_handicap/playing_handicap for én side i en match.

    Kalles fra matches.py sin add_participant RETT ETTER hver innsetting.
    Avgjør selv om den er klar til å beregne:
      - singles/fourball (enhet = spiller): beregner uansett, for hver
        deltaker som finnes (idempotent -- trygt å kalle flere ganger).
      - foursome/greensome/scramble (enhet = side): beregner KUN når siden har
        nøyaktig FORMAT_UNIT_SIZE[format] deltakere OG alle har en matchende
        tee_rating -- ellers returnerer den uten å skrive noe (se
        modul-docstring).

    Bruker ALLTID tee-ens full_18-rating (ADR-008), uavhengig av øktens
    hole_config -- front/back-9-fordelingen skjer senere, ved selve
    slagtildelingen (`allocate_over_played_holes` i scoring.py), ikke her.
    FIKSET 2026-07-19: joinet tidligere mot øktens hole_config som
    rating-scope direkte, som gjorde at en front_9/back_9-økt ALDRI fikk
    beregnet handicap i praksis -- ingen front_9/back_9-scopet tee_rating
    har noensinne blitt opprettet (kun full_18, se ADR-019 Beslutning D og
    tee-endepunktet), så joinen fant alltid null rader. Bekreftet mot ekte
    produksjonsdata (en front_9 foursome-økt med NULL course_handicap/
    playing_handicap på alle fire deltakere).

    ADR-029 (2026-07-19): ratingen er også kjønnsspesifikk nå (`tee_rating.
    gender`) -- ett fysisk utslag (`tee`) kan ha én rating per kjønn, aldri
    én felles. Joines derfor også mot spillerens `player.gender`. Krever at
    `matches.py` sin `add_participant` har validert FØR innsetting at
    spilleren har et registrert kjønn OG at valgt utslag faktisk har en
    matchende rating -- denne funksjonen forventer at det alltid stemmer,
    den validerer ikke selv (samme mønster som for manglende
    handicap_index_snapshot).
    """
    format_ = ENGINE_FORMAT_ALIASES.get(format_, format_)
    participants = await conn.fetch(
        """
        SELECT mp.id::text AS id,
               tr.handicap_index_snapshot::float AS handicap_index_snapshot,
               tee_rating.course_rating::float AS course_rating,
               tee_rating.slope_rating::float AS slope_rating,
               tee_rating.par AS par
        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_rating ON tee_rating.tee_id = mp.tee_id
                        AND tee_rating.scope = 'full_18'
                        AND tee_rating.gender = p.gender
        WHERE mp.match_id = $1 AND mp.team_side = $2::team_side
        """,
        match_id,
        team_side,
    )

    if format_ in SIDE_IS_UNIT:
        expected = FORMAT_UNIT_SIZE[format_]
        total_on_side = await conn.fetchval(
            "SELECT count(*) FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side",
            match_id,
            team_side,
        )
        if total_on_side != expected or len(participants) != expected:
            return  # ikke komplett ennå, eller mangler tee_rating for noen

    if not participants:
        return

    if not config.use_handicap:
        for p in participants:
            await conn.execute(
                "UPDATE match_participant SET course_handicap = 0, playing_handicap = 0 WHERE id = $1",
                p["id"],
            )
        return

    course_handicaps = [
        course_handicap_raw(p["handicap_index_snapshot"], p["slope_rating"], p["course_rating"], p["par"])
        if config.use_course_handicap
        else p["handicap_index_snapshot"]
        for p in participants
    ]

    if format_ in SIDE_IS_UNIT:
        playing = config.strategy.playing_handicap(course_handicaps)
        for p, ch in zip(participants, course_handicaps):
            await conn.execute(
                "UPDATE match_participant SET course_handicap = $1, playing_handicap = $2 WHERE id = $3",
                round_half_up(ch),
                playing,
                p["id"],
            )
    else:
        for p, ch in zip(participants, course_handicaps):
            playing = config.strategy.playing_handicap([ch])
            await conn.execute(
                "UPDATE match_participant SET course_handicap = $1, playing_handicap = $2 WHERE id = $3",
                round_half_up(ch),
                playing,
                p["id"],
            )


async def relative_strokes_for_match(
    conn: Connection, match_id: str, format_: str, config: AllowanceConfig
) -> dict[str, int]:
    """Relative slag per scoringsenhet i matchen.

    Nøkkel = match_participant_id for singles/fourball (fourball: ALLE fire
    spillerne i matchen samlet i én liste før match_play_strokes -- jf. R&A-
    regelen om at laveste av alle fire spiller av scratch, ikke side-vis).
    Nøkkel = team_side ('a'/'b') for foursome/greensome/scramble.

    Returnerer {} hvis handicap ikke er beregnet for én eller flere enheter
    ennå (se compute_and_store_side_handicaps) -- kalleren tolker det som
    "kan ikke avgjøre noen hull ennå".
    """
    format_ = ENGINE_FORMAT_ALIASES.get(format_, format_)
    if format_ in SIDE_IS_UNIT:
        rows = await conn.fetch(
            """
            SELECT DISTINCT ON (team_side) team_side::text AS unit, playing_handicap
            FROM match_participant
            WHERE match_id = $1
            ORDER BY team_side
            """,
            match_id,
        )
    else:
        rows = await conn.fetch(
            "SELECT id::text AS unit, playing_handicap FROM match_participant WHERE match_id = $1",
            match_id,
        )

    if not rows or any(r["playing_handicap"] is None for r in rows):
        return {}

    absolutes = [r["playing_handicap"] for r in rows]
    relative = match_play_strokes(absolutes) if config.use_matchplay_handicap else absolutes
    return {r["unit"]: v for r, v in zip(rows, relative)}
