"""
Order of Merit (ADR-043 [OOM]): sesong-sammenlagt rangering på tvers av
FLERE individuelle turneringer (`tournament.format_type = 'individual'`,
ADR-037) -- IKKE org-lagturneringer (ADR-011, poeng er der knyttet til
hele kamper, ikke enkeltspillere, samme begrensning som
konkurranseklassenes leaderboard-splitting, ADR-041).

To uavhengige akser (GolfBox-inspirert, se plan-loggen 2026-08-04):
HVA telles per lenket turnering (`result_type`: poeng-etter-plassering/
Stableford-sum/brutto/netto/pengeliste) og HVORDAN det summeres over
flere turneringer (`aggregation_mode`: sum/snitt/eclectic -- eclectic
ikke bygget i denne runden, se FEATURE_BACKLOG.md).

Gjenbruker `_compute_individual_standings` fra individual_tournaments.py
for HVER lenket turnering sin egen leaderboard/plassering -- ingen
duplisering av posisjon-/til-par-beregningen. "Regn ut ved lesing" for
selve OOM-leaderboardet også (samme filosofi som Nassau/High-low-high/
individual_leaderboard selv) -- ingen cache-tabell.

Bygget i stadier (se plan-loggen): denne runden dekker spiller-OOM med
poeng/stableford/brutto/netto/pengeliste-resultattyper, sum/snitt-
aggregering, og behold-N-beste/minimum-resultater/aldersgrense-grenser.
Lag-OOM og Eclectic-aggregering er bevisst IKKE bygget ennå (egne,
senere stadier) -- se FEATURE_BACKLOG.md.
"""

from __future__ import annotations

import json
from typing import Literal

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

from ..auth import get_authorized_org
from ..db import org_connection
from ..errors import app_error, translate_db_errors
from handicap_engine import order_of_merit_aggregate, order_of_merit_points_for_position

from .individual_tournaments import _compute_individual_standings

router = APIRouter()

_RESULT_TYPES = Literal["points", "stableford", "gross", "net", "money"]
_AGGREGATION_MODES = Literal["sum", "average", "eclectic"]


# ---------------------------------------------------------------------------
# CRUD: selve OOM-en
# ---------------------------------------------------------------------------

class OrderOfMeritCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    kind: Literal["player", "team"]
    result_type: _RESULT_TYPES
    aggregation_mode: _AGGREGATION_MODES = "sum"
    count_best_n: int | None = Field(default=None, ge=1)
    min_results_required: int | None = Field(default=None, ge=1)
    birth_year_from: int | None = None
    birth_year_to: int | None = None
    public_visible: bool = False


class OrderOfMeritUpdate(BaseModel):
    """Alle felt valgfrie -- exclude_unset, samme PATCH-semantikk som
    TournamentUpdate/SponsorUpdate/TournamentClassUpdate.

    `kind` og `result_type` er IKKE med -- begge låses etter opprettelse.
    `kind` per plan (matcher GolfBox sin egen tilsvarende låsing).
    `result_type` er strukket til samme regel her: å endre den i
    etterkant ville gjort eksisterende lenkers `points_table`/
    `money_payout_table`-konfig meningsløs uten en egen migreringssti --
    opprett en ny OOM i stedet."""

    name: str | None = Field(default=None, min_length=1, max_length=200)
    aggregation_mode: _AGGREGATION_MODES | None = None
    count_best_n: int | None = None
    min_results_required: int | None = None
    birth_year_from: int | None = None
    birth_year_to: int | None = None
    public_visible: bool | None = None


class OrderOfMeritOut(BaseModel):
    id: str
    name: str
    kind: str
    result_type: str
    aggregation_mode: str
    count_best_n: int | None
    min_results_required: int | None
    birth_year_from: int | None
    birth_year_to: int | None
    public_visible: bool


_OOM_COLUMNS = """
    id::text, name, kind, result_type, aggregation_mode, count_best_n,
    min_results_required, birth_year_from, birth_year_to, public_visible
"""


@router.get("/orgs/{organization_id}/order-of-merits", response_model=list[OrderOfMeritOut])
async def list_order_of_merits(
    organization_id: str = Depends(get_authorized_org),
) -> list[OrderOfMeritOut]:
    async with org_connection(organization_id) as conn:
        rows = await conn.fetch(f"SELECT {_OOM_COLUMNS} FROM order_of_merit ORDER BY created_at DESC")
    return [OrderOfMeritOut(**dict(r)) for r in rows]


@router.post("/orgs/{organization_id}/order-of-merits", response_model=OrderOfMeritOut, status_code=201)
async def create_order_of_merit(
    body: OrderOfMeritCreate,
    organization_id: str = Depends(get_authorized_org),
) -> OrderOfMeritOut:
    if body.kind == "team" and body.aggregation_mode == "eclectic":
        raise app_error(400, "VALIDATION_FAILED", "Eclectic er ikke støttet for lag-OOM ennå.")
    async with org_connection(organization_id) as conn, translate_db_errors():
        row = await conn.fetchrow(
            f"""
            INSERT INTO order_of_merit
                (organization_id, name, kind, result_type, aggregation_mode,
                 count_best_n, min_results_required, birth_year_from, birth_year_to, public_visible)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
            RETURNING {_OOM_COLUMNS}
            """,
            organization_id,
            body.name,
            body.kind,
            body.result_type,
            body.aggregation_mode,
            body.count_best_n,
            body.min_results_required,
            body.birth_year_from,
            body.birth_year_to,
            body.public_visible,
        )
    return OrderOfMeritOut(**dict(row))


@router.patch("/orgs/{organization_id}/order-of-merits/{oom_id}", response_model=OrderOfMeritOut)
async def update_order_of_merit(
    oom_id: str,
    body: OrderOfMeritUpdate,
    organization_id: str = Depends(get_authorized_org),
) -> OrderOfMeritOut:
    updates = body.model_dump(exclude_unset=True)
    if not updates:
        raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")

    set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
    values = list(updates.values())
    values.append(oom_id)

    async with org_connection(organization_id) as conn, translate_db_errors():
        existing = await conn.fetchrow("SELECT kind, result_type FROM order_of_merit WHERE id = $1", oom_id)
        if existing is None:
            raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.")
        if updates.get("aggregation_mode") == "eclectic" and existing["kind"] == "team":
            raise app_error(400, "VALIDATION_FAILED", "Eclectic er ikke støttet for lag-OOM ennå.")
        result_type = existing["result_type"]
        effective_mode = updates.get("aggregation_mode")
        if effective_mode == "eclectic" and result_type not in ("stableford", "gross", "net"):
            raise app_error(
                400, "VALIDATION_FAILED", "Eclectic krever resultattype Stableford, brutto eller netto."
            )
        row = await conn.fetchrow(
            f"""
            UPDATE order_of_merit SET {', '.join(set_clauses)}, updated_at = now()
            WHERE id = ${len(values)}
            RETURNING {_OOM_COLUMNS}
            """,
            *values,
        )
    return OrderOfMeritOut(**dict(row))


@router.delete("/orgs/{organization_id}/order-of-merits/{oom_id}", status_code=204)
async def delete_order_of_merit(
    oom_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> None:
    async with org_connection(organization_id) as conn:
        result = await conn.execute("DELETE FROM order_of_merit WHERE id = $1", oom_id)
        if result == "DELETE 0":
            raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.")


# ---------------------------------------------------------------------------
# Lenkede turneringer
# ---------------------------------------------------------------------------

class OrderOfMeritLinkCreate(BaseModel):
    tournament_id: str
    # Kun gyldig (og påkrevd) når OOM-ens result_type='points': liste av
    # heltall, indeks 0 = poeng for 1. plass.
    points_table: list[int] | None = None
    # Kun gyldig (og påkrevd) når result_type='money'.
    money_pool_total: float | None = None
    money_payout_table: list[dict] | None = None


class OrderOfMeritLinkOut(BaseModel):
    id: str
    tournament_id: str
    tournament_name: str
    points_table: list[int] | None
    money_pool_total: float | None
    money_payout_table: list[dict] | None


_LINK_COLUMNS = """
    l.id::text, l.tournament_id::text, t.name AS tournament_name,
    l.points_table::text AS points_table, l.money_pool_total,
    l.money_payout_table::text AS money_payout_table
"""


@router.get(
    "/orgs/{organization_id}/order-of-merits/{oom_id}/tournaments",
    response_model=list[OrderOfMeritLinkOut],
)
async def list_order_of_merit_links(
    oom_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> list[OrderOfMeritLinkOut]:
    async with org_connection(organization_id) as conn:
        rows = await conn.fetch(
            f"""
            SELECT {_LINK_COLUMNS}
            FROM order_of_merit_link l
            JOIN tournament t ON t.id = l.tournament_id
            WHERE l.order_of_merit_id = $1
            ORDER BY l.created_at
            """,
            oom_id,
        )
    return [OrderOfMeritLinkOut(**_parse_link_row(r)) for r in rows]


def _parse_link_row(r) -> dict:
    d = dict(r)
    d["points_table"] = json.loads(d["points_table"]) if d.get("points_table") else None
    d["money_payout_table"] = json.loads(d["money_payout_table"]) if d.get("money_payout_table") else None
    if d.get("money_pool_total") is not None:
        d["money_pool_total"] = float(d["money_pool_total"])
    return d


@router.post(
    "/orgs/{organization_id}/order-of-merits/{oom_id}/tournaments",
    response_model=OrderOfMeritLinkOut,
    status_code=201,
)
async def link_tournament_to_order_of_merit(
    oom_id: str,
    body: OrderOfMeritLinkCreate,
    organization_id: str = Depends(get_authorized_org),
) -> OrderOfMeritLinkOut:
    async with org_connection(organization_id) as conn, translate_db_errors():
        oom = await conn.fetchrow("SELECT result_type FROM order_of_merit WHERE id = $1", oom_id)
        if oom is None:
            raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.")
        tournament = await conn.fetchrow(
            "SELECT format_type FROM tournament WHERE id = $1", body.tournament_id
        )
        if tournament is None:
            raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
        if tournament["format_type"] != "individual":
            raise app_error(
                400, "VALIDATION_FAILED", "Kun individuelle turneringer kan lenkes til en Order of Merit."
            )

        result_type = oom["result_type"]
        if result_type == "points" and not body.points_table:
            raise app_error(400, "VALIDATION_FAILED", "points_table er påkrevd for en poeng-basert Order of Merit.")
        if result_type != "points" and body.points_table:
            raise app_error(400, "VALIDATION_FAILED", "points_table er kun gyldig for en poeng-basert Order of Merit.")
        if result_type == "money" and not (body.money_pool_total and body.money_payout_table):
            raise app_error(
                400, "VALIDATION_FAILED", "money_pool_total og money_payout_table er påkrevd for en pengeliste-basert Order of Merit."
            )
        if result_type != "money" and (body.money_pool_total or body.money_payout_table):
            raise app_error(400, "VALIDATION_FAILED", "Pengefelt er kun gyldig for en pengeliste-basert Order of Merit.")

        row = await conn.fetchrow(
            """
            WITH inserted AS (
                INSERT INTO order_of_merit_link
                    (organization_id, order_of_merit_id, tournament_id,
                     points_table, money_pool_total, money_payout_table)
                VALUES ($1, $2, $3, $4::jsonb, $5, $6::jsonb)
                RETURNING id, tournament_id, points_table, money_pool_total, money_payout_table
            )
            SELECT inserted.id::text, inserted.tournament_id::text, t.name AS tournament_name,
                   inserted.points_table::text AS points_table, inserted.money_pool_total,
                   inserted.money_payout_table::text AS money_payout_table
            FROM inserted JOIN tournament t ON t.id = inserted.tournament_id
            """,
            organization_id,
            oom_id,
            body.tournament_id,
            json.dumps(body.points_table) if body.points_table else None,
            body.money_pool_total,
            json.dumps(body.money_payout_table) if body.money_payout_table else None,
        )
    return OrderOfMeritLinkOut(**_parse_link_row(row))


@router.delete(
    "/orgs/{organization_id}/order-of-merits/{oom_id}/tournaments/{link_id}",
    status_code=204,
)
async def unlink_tournament_from_order_of_merit(
    oom_id: str,
    link_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> None:
    async with org_connection(organization_id) as conn:
        result = await conn.execute(
            "DELETE FROM order_of_merit_link WHERE id = $1 AND order_of_merit_id = $2", link_id, oom_id
        )
        if result == "DELETE 0":
            raise app_error(404, "NOT_FOUND", "Lenken finnes ikke.")


# ---------------------------------------------------------------------------
# Leaderboard -- "regn ut ved lesing", ingen cache-tabell (samme filosofi
# som Nassau/High-low-high/individual_leaderboard selv).
# ---------------------------------------------------------------------------

class OrderOfMeritLeaderboardEntry(BaseModel):
    player_id: str
    player_name: str
    results_counted: int
    results_available: int
    value: float | None
    position: str | None
    eligible: bool
    ineligible_reason: str | None = None


def _money_for_position(position_label: str, payout_table: list[dict]) -> float:
    """Samme delt-plassering-regel som order_of_merit_points_for_position
    (uavgjorte deler samme utbetaling ved sin felles plassering) -- egen
    liten oppslagsfunksjon her (ikke i handicap_engine.py) siden dette er
    et rent JSON-konfig-oppslag, ikke golf-domenematematikk om uavgjort."""
    position = int(position_label.lstrip("T"))
    for entry in payout_table:
        if entry.get("position") == position:
            return float(entry.get("amount") or 0)
    return 0.0


@router.get(
    "/orgs/{organization_id}/order-of-merits/{oom_id}/leaderboard",
    response_model=list[OrderOfMeritLeaderboardEntry],
)
async def order_of_merit_leaderboard(
    oom_id: str,
    organization_id: str = Depends(get_authorized_org),
) -> list[OrderOfMeritLeaderboardEntry]:
    async with org_connection(organization_id) as conn:
        oom = await conn.fetchrow(
            """
            SELECT result_type, aggregation_mode, count_best_n, min_results_required,
                   birth_year_from, birth_year_to, kind
            FROM order_of_merit WHERE id = $1
            """,
            oom_id,
        )
        if oom is None:
            raise app_error(404, "NOT_FOUND", "Order of Merit finnes ikke.")
        if oom["kind"] != "player":
            raise app_error(400, "VALIDATION_FAILED", "Lag-OOM-leaderboard er ikke bygget ennå.")
        if oom["aggregation_mode"] == "eclectic":
            raise app_error(400, "VALIDATION_FAILED", "Eclectic-aggregering er ikke bygget ennå.")

        result_type = oom["result_type"]
        lower_is_better = result_type in ("gross", "net")

        links = await conn.fetch(
            """
            SELECT tournament_id::text, points_table::text AS points_table,
                   money_payout_table::text AS money_payout_table
            FROM order_of_merit_link WHERE order_of_merit_id = $1
            """,
            oom_id,
        )

        # player_id -> [rå bidrags-verdier, ett per lenket turnering spilleren har deltatt i]
        values_by_player: dict[str, list[float]] = {}
        names_by_player: dict[str, str] = {}

        for link in links:
            _, entries = await _compute_individual_standings(conn, link["tournament_id"])
            points_table = _decode_json(link["points_table"])
            money_payout_table = _decode_json(link["money_payout_table"])
            for e in entries:
                contribution = _contribution_for_entry(e, result_type, points_table, money_payout_table)
                if contribution is None:
                    continue
                values_by_player.setdefault(e.player_id, []).append(contribution)
                names_by_player[e.player_id] = e.player_name

        # Aldersgrense: filtrert FØR aggregering (spillere utenfor
        # fødselsårs-intervallet er ikke rangert i det hele tatt, ikke
        # bare gitt 0 poeng -- de skal ikke telle med som "sist").
        eligible_player_ids = set(values_by_player.keys())
        birth_years: dict[str, int | None] = {}
        if oom["birth_year_from"] is not None or oom["birth_year_to"] is not None:
            if eligible_player_ids:
                rows = await conn.fetch(
                    "SELECT id::text, EXTRACT(YEAR FROM birth_date)::int AS birth_year FROM player WHERE id = ANY($1::uuid[])",
                    list(eligible_player_ids),
                )
                birth_years = {r["id"]: r["birth_year"] for r in rows}

        results: list[OrderOfMeritLeaderboardEntry] = []
        for player_id, raw_values in values_by_player.items():
            results_available = len(raw_values)
            eligible = True
            ineligible_reason = None

            if oom["birth_year_from"] is not None or oom["birth_year_to"] is not None:
                by = birth_years.get(player_id)
                if by is None:
                    eligible = False
                    ineligible_reason = "Ingen registrert fødselsdato."
                elif oom["birth_year_from"] is not None and by < oom["birth_year_from"]:
                    eligible = False
                    ineligible_reason = "Utenfor aldersgrensen."
                elif oom["birth_year_to"] is not None and by > oom["birth_year_to"]:
                    eligible = False
                    ineligible_reason = "Utenfor aldersgrensen."

            if eligible and oom["min_results_required"] is not None and results_available < oom["min_results_required"]:
                eligible = False
                ineligible_reason = f"Trenger minst {oom['min_results_required']} tellende resultater, har {results_available}."

            values_for_aggregate = [(-v if lower_is_better else v) for v in raw_values]
            aggregated = order_of_merit_aggregate(values_for_aggregate, oom["aggregation_mode"], oom["count_best_n"])
            value = (-aggregated if lower_is_better and aggregated is not None else aggregated)
            counted = min(oom["count_best_n"], results_available) if oom["count_best_n"] is not None else results_available

            results.append(
                OrderOfMeritLeaderboardEntry(
                    player_id=player_id,
                    player_name=names_by_player[player_id],
                    results_counted=counted,
                    results_available=results_available,
                    value=value,
                    position=None,
                    eligible=eligible,
                    ineligible_reason=ineligible_reason,
                )
            )

        # Rangering: kun blant de kvalifiserte, høyest verdi øverst for
        # poeng/stableford/pengeliste, LAVEST for brutto/netto -- men
        # `value` er allerede tilbake-negert til ekte visningsverdi over,
        # så sorter på `value` med riktig retning per resultattype.
        eligible_results = [r for r in results if r.eligible and r.value is not None]
        ineligible_results = [r for r in results if not (r.eligible and r.value is not None)]
        eligible_results.sort(key=lambda r: r.value, reverse=not lower_is_better)

        i, n = 0, len(eligible_results)
        while i < n:
            j = i
            while j < n and eligible_results[j].value == eligible_results[i].value:
                j += 1
            rank = i + 1
            tied = (j - i) > 1
            for k in range(i, j):
                eligible_results[k].position = f"T{rank}" if tied else str(rank)
            i = j

    return eligible_results + ineligible_results


def _decode_json(value):
    return json.loads(value) if value else None


def _contribution_for_entry(entry, result_type: str, points_table, money_payout_table) -> float | None:
    """Utleder ÉN spillers bidrags-tall fra én lenket turnerings egen,
    allerede beregnede leaderboard-entry -- `None` betyr "spilte ikke
    denne turneringen" (skal ikke telle som et 0-resultat, kalleren
    hopper over den).

    `entry.position` (posisjon i DENNE turneringen) er KUN populert for
    scoring_method i (stroke_gross, stroke_net, stableford) -- se
    `_attach_stroke_play_columns` i individual_tournaments.py. Er OOM-en
    poeng- eller pengeliste-basert og den lenkede turneringen bruker en
    annen scoring_method (København/BBB/Flagg), er det bevisst ingen
    posisjon å hente poeng/penger fra -- spilleren telles fortsatt som
    "deltok" (rounds_played > 0) men bidrar med 0, ikke ekskludert helt."""
    if entry.rounds_played == 0:
        return None
    if result_type == "points":
        return float(order_of_merit_points_for_position(entry.position, points_table)) if entry.position and points_table else 0.0
    if result_type == "money":
        return _money_for_position(entry.position, money_payout_table) if entry.position and money_payout_table else 0.0
    if result_type == "stableford":
        return float(entry.stableford_total) if entry.stableford_total is not None else 0.0
    if result_type == "gross":
        return float(entry.gross_total) if entry.gross_total is not None else 0.0
    if result_type == "net":
        return float(entry.net_total) if entry.net_total is not None else 0.0
    return 0.0
