import secrets from datetime import datetime, timezone from fastapi import APIRouter, Depends, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse import aiosqlite from app.database import get_db from app.dependencies import require_admin from app.templates_config import templates router = APIRouter() async def _get_course_detail(cid: str, db: aiosqlite.Connection): """Returns (course_dict, holes_list, tees_list) or (None, [], []).""" async with db.execute( "SELECT id, name, holes, location FROM courses WHERE id = ?", (cid,) ) as c: course = await c.fetchone() if not course: return None, [], [] async with db.execute( "SELECT hole_number, par, stroke_index FROM course_holes WHERE course_id = ? ORDER BY hole_number", (cid,), ) as c: holes = [dict(r) for r in await c.fetchall()] async with db.execute( "SELECT id, name, slope, rating FROM course_tees WHERE course_id = ? ORDER BY name", (cid,), ) as c: tees = [dict(r) for r in await c.fetchall()] return dict(course), holes, tees # ── List ────────────────────────────────────────────────────────────────────── @router.get("/admin/courses", response_class=HTMLResponse) async def courses_page(request: Request, user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): async with db.execute( "SELECT id, name, holes, location FROM courses ORDER BY name" ) as c: courses = [dict(r) for r in await c.fetchall()] return templates.TemplateResponse( "admin/courses.html", {"request": request, "user": user, "courses": courses}, ) # ── Create ──────────────────────────────────────────────────────────────────── @router.get("/admin/courses/new", response_class=HTMLResponse) async def new_course_page(request: Request, user: dict = Depends(require_admin)): return templates.TemplateResponse( "admin/new_course.html", {"request": request, "user": user} ) @router.post("/admin/courses/new") async def create_course(request: Request, name: str = Form(...), holes: int = Form(...), location: str = Form(""), user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): form = await request.form() name = name.strip() location = location.strip() errors = {} if not name: errors["name"] = "Name is required." if holes not in (9, 18): errors["holes"] = "Holes must be 9 or 18." hole_count = holes if holes in (9, 18) else 18 pars = {} stroke_indices = {} for i in range(1, hole_count + 1): raw = form.get(f"par_{i}", "4") try: p = int(raw) if p < 3 or p > 5: errors[f"par_{i}"] = f"Hole {i}: par must be 3–5." else: pars[i] = p except (ValueError, TypeError): errors[f"par_{i}"] = f"Hole {i}: invalid par." raw_si = form.get(f"si_{i}", "") if raw_si: try: si = int(raw_si) if si < 1 or si > hole_count: errors[f"si_{i}"] = f"Hole {i}: SI must be 1–{hole_count}." else: stroke_indices[i] = si except (ValueError, TypeError): errors[f"si_{i}"] = f"Hole {i}: invalid SI." else: stroke_indices[i] = None if errors: return templates.TemplateResponse( "admin/new_course.html", {"request": request, "user": user, "errors": errors, "form": {"name": name, "holes": holes, "location": location, "pars": pars, "stroke_indices": stroke_indices}}, status_code=422, ) cid = secrets.token_urlsafe(10) now = datetime.now(timezone.utc).isoformat() await db.execute( "INSERT INTO courses (id, name, holes, location, created_at) VALUES (?,?,?,?,?)", (cid, name, holes, location or None, now), ) for hole_num, par in pars.items(): await db.execute( "INSERT INTO course_holes (id, course_id, hole_number, par, stroke_index) VALUES (?,?,?,?,?)", (secrets.token_urlsafe(10), cid, hole_num, par, stroke_indices.get(hole_num)), ) await db.commit() return RedirectResponse( f"/admin/courses/{cid}/edit?success=Course+created.+Add+tees+below.", status_code=302, ) # ── Edit ────────────────────────────────────────────────────────────────────── @router.get("/admin/courses/{cid}/edit", response_class=HTMLResponse) async def edit_course_page(cid: str, request: Request, user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): course, holes, tees = await _get_course_detail(cid, db) if not course: return RedirectResponse("/admin/courses", status_code=302) return templates.TemplateResponse( "admin/edit_course.html", {"request": request, "user": user, "course": course, "holes": holes, "tees": tees}, ) @router.post("/admin/courses/{cid}/edit") async def edit_course_submit(cid: str, request: Request, name: str = Form(...), holes: int = Form(...), location: str = Form(""), user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): form = await request.form() course, existing_holes, tees = await _get_course_detail(cid, db) if not course: return RedirectResponse("/admin/courses", status_code=302) name = name.strip() location = location.strip() errors = {} if not name: errors["name"] = "Name is required." if holes not in (9, 18): errors["holes"] = "Holes must be 9 or 18." hole_count = holes if holes in (9, 18) else 18 pars = {} stroke_indices = {} for i in range(1, hole_count + 1): raw = form.get(f"par_{i}", "4") try: p = int(raw) if p < 3 or p > 5: errors[f"par_{i}"] = f"Hole {i}: par must be 3–5." else: pars[i] = p except (ValueError, TypeError): errors[f"par_{i}"] = f"Hole {i}: invalid par." raw_si = form.get(f"si_{i}", "") if raw_si: try: si = int(raw_si) if si < 1 or si > hole_count: errors[f"si_{i}"] = f"Hole {i}: SI must be 1–{hole_count}." else: stroke_indices[i] = si except (ValueError, TypeError): errors[f"si_{i}"] = f"Hole {i}: invalid SI." else: stroke_indices[i] = None if errors: return templates.TemplateResponse( "admin/edit_course.html", {"request": request, "user": user, "course": course, "holes": existing_holes, "tees": tees, "errors": errors}, status_code=422, ) await db.execute( "UPDATE courses SET name = ?, holes = ?, location = ? WHERE id = ?", (name, holes, location or None, cid), ) for hole_num, par in pars.items(): await db.execute( """INSERT INTO course_holes (id, course_id, hole_number, par, stroke_index) VALUES (?,?,?,?,?) ON CONFLICT(course_id, hole_number) DO UPDATE SET par=excluded.par, stroke_index=excluded.stroke_index""", (secrets.token_urlsafe(10), cid, hole_num, par, stroke_indices.get(hole_num)), ) # Remove holes beyond new count if holes was reduced await db.execute( "DELETE FROM course_holes WHERE course_id = ? AND hole_number > ?", (cid, holes) ) await db.commit() return RedirectResponse(f"/admin/courses/{cid}/edit?success=Course+updated.", status_code=302) # ── Delete course ───────────────────────────────────────────────────────────── @router.post("/admin/courses/{cid}/delete") async def delete_course(cid: str, user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): await db.execute("DELETE FROM course_holes WHERE course_id = ?", (cid,)) await db.execute("DELETE FROM course_tees WHERE course_id = ?", (cid,)) await db.execute("DELETE FROM courses WHERE id = ?", (cid,)) await db.commit() return RedirectResponse("/admin/courses?success=Course+deleted.", status_code=302) # ── Tees ────────────────────────────────────────────────────────────────────── @router.post("/admin/courses/{cid}/tees/new") async def add_tee(cid: str, name: str = Form(...), slope: int = Form(...), rating: float = Form(...), user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): await db.execute( "INSERT INTO course_tees (id, course_id, name, slope, rating) VALUES (?,?,?,?,?)", (secrets.token_urlsafe(10), cid, name.strip(), slope, rating), ) await db.commit() return RedirectResponse(f"/admin/courses/{cid}/edit?success=Tee+added.", status_code=302) @router.post("/admin/courses/{cid}/tees/{tid}/edit") async def edit_tee(cid: str, tid: str, name: str = Form(...), slope: int = Form(...), rating: float = Form(...), user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): await db.execute( "UPDATE course_tees SET name = ?, slope = ?, rating = ? WHERE id = ? AND course_id = ?", (name.strip(), slope, rating, tid, cid), ) await db.commit() return RedirectResponse(f"/admin/courses/{cid}/edit?success=Tee+updated.", status_code=302) @router.post("/admin/courses/{cid}/tees/{tid}/delete") async def delete_tee(cid: str, tid: str, user: dict = Depends(require_admin), db: aiosqlite.Connection = Depends(get_db)): # Nullify references in game_players before deleting await db.execute( "UPDATE game_players SET tee_id = NULL WHERE tee_id = ?", (tid,) ) await db.execute( "DELETE FROM course_tees WHERE id = ? AND course_id = ?", (tid, cid) ) await db.commit() return RedirectResponse(f"/admin/courses/{cid}/edit?success=Tee+deleted.", status_code=302)