"""
Slag-for-slag GPS-avstandsmåling for org-turneringer (ADR-103) --
speiler `rounds.py` sin slag-seksjon (ADR-048) for frittstående runder,
men org-scopet (RLS) og forenklet: turneringsmodellen har ALDRI en
side/lag-XOR (kun individuelt), så kun participant-varianten trengs
(ingen `/sides/`-speilet endepunktsett).

Eierskap: `tournament_round_shot` (migrasjon 091) kjenner
`tournament_round_participant_id` + `hole_number` DIREKTE -- IKKE
`tournament_round_hole_id`. `tournament_round_hole` opprettes (ulikt
frittstående `round_hole`) først ved FØRSTE score-innsending
(`update_hole`), så en spiller må kunne måle et slag FØR hullets score
er registrert -- en FK mot en hull-rad som kanskje ikke finnes ennå
ville vært feil. Samme LEFT JOIN-tolerante oppløsning som
`get_tournament_round_hole_target_points`/`list_round_participant_holes`
allerede bruker.

Autorisasjon, bevisst STRENGERE enn ADR-102s kommentartråd: skriving
(POST/DELETE) krever `user_is_own_tournament_participant` (self ELLER
org-admin) -- samme sensitivitetsklasse som selve SCORE-registreringen
(`update_hole`), ikke oppsett-klassen kommentarer hører til. Lesing
(GET) krever kun org-medlemskap, samme mønster som
`list_round_participant_holes`.

Deling: reuser den rundespesifikke kommentartråden (ADR-102,
`tournament_round_message`) i stedet for Banter Board, speiler
`rounds.py::share_shot` sitt Mapbox Static Images-snippet-mønster.
`_mapbox_static_snippet_url`/`_encode_polyline` er en bevisst LOKAL kopi
(ikke importert fra rounds.py) -- org-turnering-subsystemet importerer
aldri fra det frittstående-runde-subsystemet (samme presedens ADR-102
selv fulgte for `_resolve_author_name`).
"""

import urllib.parse
from typing import Literal

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

from .. import storage
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 user_is_own_tournament_participant
from .tournament_round_messages import _resolve_author_name

router = APIRouter(tags=["tournament-round-shots"])

_SHOT_SELECT = """
    SELECT trs.id::text AS id, trs.shot_number, trs.club, trs.distance_meters, trs.start_method,
           trs.start_lat, trs.start_lng, trs.end_method, trs.end_lat, trs.end_lng,
           trs.shared_tournament_round_message_id::text AS shared_tournament_round_message_id,
           trs.captured_at, trm.image_key AS shared_image_key
    FROM tournament_round_shot trs
    LEFT JOIN tournament_round_message trm ON trm.id = trs.shared_tournament_round_message_id
"""


class ShotIn(BaseModel):
    club: str = Field(max_length=50)
    distance_meters: float = Field(gt=0, lt=500)
    start_method: Literal["gps", "map_tap"]
    start_lat: float = Field(ge=-90, le=90)
    start_lng: float = Field(ge=-180, le=180)
    end_method: Literal["gps", "map_tap"] = "gps"
    end_lat: float = Field(ge=-90, le=90)
    end_lng: float = Field(ge=-180, le=180)


class ShotOut(BaseModel):
    id: str
    shot_number: int
    club: str
    distance_meters: float
    start_method: str
    start_lat: float
    start_lng: float
    end_method: str
    end_lat: float
    end_lng: float
    shared_tournament_round_message_id: str | None
    shared_image_url: str | None
    captured_at: str


def _shot_out(row) -> ShotOut:
    return ShotOut(
        id=row["id"],
        shot_number=row["shot_number"],
        club=row["club"],
        distance_meters=float(row["distance_meters"]),
        start_method=row["start_method"],
        start_lat=row["start_lat"],
        start_lng=row["start_lng"],
        end_method=row["end_method"],
        end_lat=row["end_lat"],
        end_lng=row["end_lng"],
        shared_tournament_round_message_id=row["shared_tournament_round_message_id"],
        shared_image_url=storage.public_url(row["shared_image_key"]) if row["shared_image_key"] else None,
        captured_at=row["captured_at"].isoformat(),
    )


async def _resolve_participant_hole(
    conn, tournament_id: str, round_id: str, round_participant_id: str, hole_number: int
) -> str:
    """LEFT JOIN-tolerant: bekrefter at deltakeren/runden/turneringen
    faktisk henger sammen, men krever IKKE at tournament_round_hole
    finnes ennå (spilleren kan måle et slag før de fullfører hullets
    score). Returnerer tournament_participant_id (for autorisasjon)."""
    row = await conn.fetchrow(
        """
        SELECT trp.tournament_participant_id::text AS tournament_participant_id, tr.tournament_id::text AS tournament_id
        FROM tournament_round_participant trp
        JOIN tournament_round tr ON tr.id = trp.tournament_round_id
        WHERE trp.id = $1 AND trp.tournament_round_id = $2
        """,
        round_participant_id, round_id,
    )
    if row is None or row["tournament_id"] != tournament_id:
        raise app_error(404, "NOT_FOUND", "Rundedeltakeren finnes ikke.")
    if hole_number < 1 or hole_number > 18:
        raise app_error(400, "VALIDATION_FAILED", "Ugyldig hullnummer.")
    return row["tournament_participant_id"]


async def _list_shots(conn, round_participant_id: str, hole_number: int) -> list[ShotOut]:
    rows = await conn.fetch(
        _SHOT_SELECT + " WHERE trs.tournament_round_participant_id = $1 AND trs.hole_number = $2 ORDER BY trs.shot_number",
        round_participant_id, hole_number,
    )
    return [_shot_out(r) for r in rows]


@router.get(
    "/orgs/{organization_id}/tournaments/{tournament_id}/rounds/{round_id}"
    "/participants/{round_participant_id}/holes/{hole_number}/shots",
    response_model=list[ShotOut],
)
async def list_tournament_round_shots(
    tournament_id: str, round_id: str, round_participant_id: str, hole_number: int,
    organization_id: str = Depends(get_authorized_org),
) -> list[ShotOut]:
    async with org_connection(organization_id) as conn:
        await _resolve_participant_hole(conn, tournament_id, round_id, round_participant_id, hole_number)
        return await _list_shots(conn, round_participant_id, hole_number)


@router.post(
    "/orgs/{organization_id}/tournaments/{tournament_id}/rounds/{round_id}"
    "/participants/{round_participant_id}/holes/{hole_number}/shots",
    response_model=ShotOut,
    status_code=201,
)
async def create_tournament_round_shot(
    tournament_id: str, round_id: str, round_participant_id: str, hole_number: int, body: ShotIn,
    organization_id: str = Depends(get_authorized_org),
    user: CurrentUser = Depends(get_current_user),
) -> ShotOut:
    async with org_connection(organization_id) as conn, translate_db_errors():
        tournament_participant_id = await _resolve_participant_hole(
            conn, tournament_id, round_id, round_participant_id, hole_number
        )
        if not await user_is_own_tournament_participant(conn, organization_id, tournament_participant_id, user.user_id):
            raise app_error(
                403, "NOT_TOURNAMENT_PARTICIPANT",
                "Du kan kun måle slag for deg selv (eller være organisasjonsadministrator).",
            )
        row = await conn.fetchrow(
            """
            INSERT INTO tournament_round_shot (
                organization_id, tournament_round_participant_id, hole_number, recorded_by_user_id,
                shot_number, club, distance_meters, start_method, start_lat, start_lng, end_method, end_lat, end_lng
            )
            VALUES (
                $1, $2, $3, $4,
                COALESCE((SELECT MAX(shot_number) FROM tournament_round_shot
                          WHERE tournament_round_participant_id = $2 AND hole_number = $3), 0) + 1,
                $5, $6, $7, $8, $9, $10, $11, $12
            )
            RETURNING id::text AS id
            """,
            organization_id, round_participant_id, hole_number, user.user_id,
            body.club, body.distance_meters, body.start_method, body.start_lat, body.start_lng,
            body.end_method, body.end_lat, body.end_lng,
        )
        full_row = await conn.fetchrow(_SHOT_SELECT + " WHERE trs.id = $1", row["id"])
    return _shot_out(full_row)


@router.delete(
    "/orgs/{organization_id}/tournaments/{tournament_id}/rounds/{round_id}/shots/{shot_id}",
    status_code=204,
)
async def delete_tournament_round_shot(
    tournament_id: str, round_id: str, shot_id: str,
    organization_id: str = Depends(get_authorized_org),
    user: CurrentUser = Depends(get_current_user),
) -> None:
    async with org_connection(organization_id) as conn:
        row = await conn.fetchrow(
            """
            SELECT trs.tournament_round_participant_id::text AS round_participant_id,
                   trp.tournament_participant_id::text AS tournament_participant_id
            FROM tournament_round_shot trs
            JOIN tournament_round_participant trp ON trp.id = trs.tournament_round_participant_id
            JOIN tournament_round tr ON tr.id = trp.tournament_round_id
            WHERE trs.id = $1 AND tr.id = $2 AND tr.tournament_id = $3
            """,
            shot_id, round_id, tournament_id,
        )
        if row is None:
            raise app_error(404, "NOT_FOUND", "Slaget finnes ikke på denne runden.")
        if not await user_is_own_tournament_participant(
            conn, organization_id, row["tournament_participant_id"], user.user_id
        ):
            raise app_error(
                403, "NOT_TOURNAMENT_PARTICIPANT",
                "Du kan kun slette dine egne målte slag (eller være organisasjonsadministrator).",
            )
        await conn.execute("DELETE FROM tournament_round_shot WHERE id = $1", shot_id)


def _encode_polyline(points: list[tuple[float, float]]) -> str:
    """Google sin polyline-algoritme (5 desimaler) -- lokal kopi av
    rounds.py sin, se modulens docstring for hvorfor den ikke importeres."""
    result: list[str] = []
    prev_lat = prev_lng = 0
    for lat, lng in points:
        lat_i = round(lat * 1e5)
        lng_i = round(lng * 1e5)
        for value, prev in ((lat_i, prev_lat), (lng_i, prev_lng)):
            delta = value - prev
            shifted = ~(delta << 1) if delta < 0 else (delta << 1)
            chunk = ""
            while shifted >= 0x20:
                chunk += chr((0x20 | (shifted & 0x1F)) + 63)
                shifted >>= 5
            chunk += chr(shifted + 63)
            result.append(chunk)
        prev_lat, prev_lng = lat_i, lng_i
    return "".join(result)


def _mapbox_static_snippet_url(start_lat: float, start_lng: float, end_lat: float, end_lng: float) -> str:
    encoded = urllib.parse.quote(_encode_polyline([(start_lat, start_lng), (end_lat, end_lng)]), safe="")
    overlay = (
        f"pin-s-a+ff5a1f({start_lng},{start_lat}),"
        f"pin-s-b+2f7a3f({end_lng},{end_lat}),"
        f"path-4+ff5a1f-0.9({encoded})"
    )
    return (
        f"https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/static/{overlay}/auto/600x400@2x"
        f"?padding=60&access_token={settings.MAPBOX_SECRET_TOKEN}"
    )


class ShotShareIn(BaseModel):
    body: str = Field(max_length=2000)


@router.post(
    "/orgs/{organization_id}/tournaments/{tournament_id}/rounds/{round_id}/shots/{shot_id}/share",
    response_model=ShotOut,
)
async def share_tournament_round_shot(
    tournament_id: str, round_id: str, shot_id: str, body: ShotShareIn,
    organization_id: str = Depends(get_authorized_org),
    user: CurrentUser = Depends(get_current_user),
) -> ShotOut:
    async with org_connection(organization_id) as conn:
        shot_row = await conn.fetchrow(
            """
            SELECT trs.id::text AS id, trs.start_lat, trs.start_lng, trs.end_lat, trs.end_lng,
                   trp.tournament_participant_id::text AS tournament_participant_id
            FROM tournament_round_shot trs
            JOIN tournament_round_participant trp ON trp.id = trs.tournament_round_participant_id
            JOIN tournament_round tr ON tr.id = trp.tournament_round_id
            WHERE trs.id = $1 AND tr.id = $2 AND tr.tournament_id = $3
            """,
            shot_id, round_id, tournament_id,
        )
        if shot_row is None:
            raise app_error(404, "NOT_FOUND", "Slaget finnes ikke på denne runden.")
        if not await user_is_own_tournament_participant(
            conn, organization_id, shot_row["tournament_participant_id"], user.user_id
        ):
            raise app_error(
                403, "NOT_TOURNAMENT_PARTICIPANT",
                "Du kan kun dele dine egne målte slag (eller være organisasjonsadministrator).",
            )

        author_display_name = await _resolve_author_name(conn, user.user_id)

        image_key = None
        if settings.MAPBOX_SECRET_TOKEN:
            url = _mapbox_static_snippet_url(
                shot_row["start_lat"], shot_row["start_lng"], shot_row["end_lat"], shot_row["end_lng"]
            )
            try:
                async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
                    resp = await client.get(url)
                resp.raise_for_status()
                image_key = await storage.upload_image("tournament_round_messages", round_id, resp.content)
            except httpx.HTTPError:
                # Grasiøs degradering, samme prinsipp som rounds.py::share_shot
                # -- ren tekst uten satellittutsnitt fremfor å blokkere delingen.
                image_key = None

        async with translate_db_errors():
            message_row = await conn.fetchrow(
                """
                INSERT INTO tournament_round_message
                    (organization_id, tournament_round_id, author_user_id, author_display_name, body, image_key)
                VALUES ($1, $2, $3, $4, $5, $6)
                RETURNING id::text AS id
                """,
                organization_id, round_id, user.user_id, author_display_name, body.body, image_key,
            )
            await conn.execute(
                "UPDATE tournament_round_shot SET shared_tournament_round_message_id = $1 WHERE id = $2",
                message_row["id"], shot_id,
            )
        full_row = await conn.fetchrow(_SHOT_SELECT + " WHERE trs.id = $1", shot_id)
    return _shot_out(full_row)
