"""Tracker-Target link management API routes.""" from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession from ..auth.dependencies import get_current_user from ..database.engine import get_session from ..database.models import ( NotificationTarget, TemplateConfig, Tracker, TrackerTarget, TrackingConfig, User, ) router = APIRouter(prefix="/api/trackers/{tracker_id}/targets", tags=["tracker-targets"]) class TrackerTargetCreate(BaseModel): target_id: int tracking_config_id: int | None = None template_config_id: int | None = None enabled: bool = True quiet_hours_start: str | None = None quiet_hours_end: str | None = None commands_config: dict[str, Any] | None = None class TrackerTargetUpdate(BaseModel): tracking_config_id: int | None = None template_config_id: int | None = None enabled: bool | None = None quiet_hours_start: str | None = None quiet_hours_end: str | None = None commands_config: dict[str, Any] | None = None @router.get("") async def list_tracker_targets( tracker_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """List all target links for a tracker.""" await _get_user_tracker(session, tracker_id, user.id) result = await session.exec( select(TrackerTarget).where(TrackerTarget.tracker_id == tracker_id) ) return [await _tt_response(session, tt) for tt in result.all()] @router.post("", status_code=status.HTTP_201_CREATED) async def create_tracker_target( tracker_id: int, body: TrackerTargetCreate, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Link a target to a tracker with per-link configuration.""" await _get_user_tracker(session, tracker_id, user.id) # Validate target exists and belongs to user target = await session.get(NotificationTarget, body.target_id) if not target or target.user_id != user.id: raise HTTPException(status_code=404, detail="Target not found") # Check for duplicate link result = await session.exec( select(TrackerTarget).where( TrackerTarget.tracker_id == tracker_id, TrackerTarget.target_id == body.target_id, ) ) if result.first(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Target is already linked to this tracker", ) # Validate config ownership if body.tracking_config_id: tc = await session.get(TrackingConfig, body.tracking_config_id) if not tc or tc.user_id != user.id: raise HTTPException(status_code=404, detail="Tracking config not found") if body.template_config_id: tpc = await session.get(TemplateConfig, body.template_config_id) if not tpc or (tpc.user_id != user.id and tpc.user_id != 0): raise HTTPException(status_code=404, detail="Template config not found") tt = TrackerTarget(tracker_id=tracker_id, **body.model_dump()) session.add(tt) await session.commit() await session.refresh(tt) return await _tt_response(session, tt) @router.put("/{tracker_target_id}") async def update_tracker_target( tracker_id: int, tracker_target_id: int, body: TrackerTargetUpdate, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Update a tracker-target link's configuration.""" await _get_user_tracker(session, tracker_id, user.id) tt = await session.get(TrackerTarget, tracker_target_id) if not tt or tt.tracker_id != tracker_id: raise HTTPException(status_code=404, detail="Tracker-target link not found") updates = body.model_dump(exclude_unset=True) # Validate config ownership if being changed if "tracking_config_id" in updates and updates["tracking_config_id"]: tc = await session.get(TrackingConfig, updates["tracking_config_id"]) if not tc or tc.user_id != user.id: raise HTTPException(status_code=404, detail="Tracking config not found") if "template_config_id" in updates and updates["template_config_id"]: tpc = await session.get(TemplateConfig, updates["template_config_id"]) if not tpc or (tpc.user_id != user.id and tpc.user_id != 0): raise HTTPException(status_code=404, detail="Template config not found") for field, value in updates.items(): setattr(tt, field, value) session.add(tt) await session.commit() await session.refresh(tt) return await _tt_response(session, tt) @router.delete("/{tracker_target_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_tracker_target( tracker_id: int, tracker_target_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Remove a target link from a tracker.""" await _get_user_tracker(session, tracker_id, user.id) tt = await session.get(TrackerTarget, tracker_target_id) if not tt or tt.tracker_id != tracker_id: raise HTTPException(status_code=404, detail="Tracker-target link not found") await session.delete(tt) await session.commit() @router.post("/{tracker_target_id}/test") async def test_tracker_target( tracker_id: int, tracker_target_id: int, locale: str = Query("en"), user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Send a test notification to a specific linked target.""" await _get_user_tracker(session, tracker_id, user.id) tt = await session.get(TrackerTarget, tracker_target_id) if not tt or tt.tracker_id != tracker_id: raise HTTPException(status_code=404, detail="Tracker-target link not found") target = await session.get(NotificationTarget, tt.target_id) if not target: raise HTTPException(status_code=404, detail="Target not found") from ..services.notifier import send_test_notification r = await send_test_notification(target, locale=locale) return {"target": target.name, **r} @router.post("/{tracker_target_id}/test-periodic") async def test_periodic_tracker_target( tracker_id: int, tracker_target_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Send a test periodic summary to a specific linked target.""" await _get_user_tracker(session, tracker_id, user.id) tt = await session.get(TrackerTarget, tracker_target_id) if not tt or tt.tracker_id != tracker_id: raise HTTPException(status_code=404, detail="Tracker-target link not found") target = await session.get(NotificationTarget, tt.target_id) if not target: raise HTTPException(status_code=404, detail="Target not found") template_config = None if tt.template_config_id: template_config = await session.get(TemplateConfig, tt.template_config_id) template_str = (template_config.periodic_summary_message if template_config else "") or "" from ..services.notifier import send_test_template_notification r = await send_test_template_notification(target, "periodic_summary", template_str) return {"target": target.name, **r} @router.post("/{tracker_target_id}/test-memory") async def test_memory_tracker_target( tracker_id: int, tracker_target_id: int, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ): """Send a test memory/on-this-day notification to a specific linked target.""" await _get_user_tracker(session, tracker_id, user.id) tt = await session.get(TrackerTarget, tracker_target_id) if not tt or tt.tracker_id != tracker_id: raise HTTPException(status_code=404, detail="Tracker-target link not found") target = await session.get(NotificationTarget, tt.target_id) if not target: raise HTTPException(status_code=404, detail="Target not found") template_config = None if tt.template_config_id: template_config = await session.get(TemplateConfig, tt.template_config_id) template_str = (template_config.memory_mode_message if template_config else "") or "" from ..services.notifier import send_test_template_notification r = await send_test_template_notification(target, "memory_mode", template_str) return {"target": target.name, **r} async def _tt_response(session: AsyncSession, tt: TrackerTarget) -> dict: """Build tracker-target response with target details.""" target = await session.get(NotificationTarget, tt.target_id) return { "id": tt.id, "tracker_id": tt.tracker_id, "target_id": tt.target_id, "target_name": target.name if target else None, "target_type": target.type if target else None, "target_icon": target.icon if target else None, "tracking_config_id": tt.tracking_config_id, "template_config_id": tt.template_config_id, "enabled": tt.enabled, "quiet_hours_start": tt.quiet_hours_start, "quiet_hours_end": tt.quiet_hours_end, "commands_config": tt.commands_config, "created_at": tt.created_at.isoformat(), } async def _get_user_tracker( session: AsyncSession, tracker_id: int, user_id: int ) -> Tracker: tracker = await session.get(Tracker, tracker_id) if not tracker or tracker.user_id != user_id: raise HTTPException(status_code=404, detail="Tracker not found") return tracker