from __future__ import annotations from datetime import date, datetime from typing import Any from pydantic import BaseModel, ConfigDict, Field, field_validator from fan_passport.scoring import SUPPORTED_COLLECTION_TYPES class ProfileCreateRequest(BaseModel): external_subject: str | None = Field(default=None, max_length=160) display_name: str = Field(min_length=1, max_length=80) country_code: str | None = Field(default=None, min_length=2, max_length=3) favorite_team_id: str | None = None avatar_url: str | None = None metadata_json: dict[str, Any] = Field(default_factory=dict) @field_validator("country_code") @classmethod def normalize_country_code(cls, value: str | None) -> str | None: return value.upper() if value else value class MeProfileRequest(BaseModel): display_name: str | None = Field(default=None, max_length=80) country_code: str | None = Field(default=None, min_length=2, max_length=3) favorite_team_id: str | None = None avatar_url: str | None = None metadata_json: dict[str, Any] = Field(default_factory=dict) @field_validator("country_code") @classmethod def normalize_country_code(cls, value: str | None) -> str | None: return value.upper() if value else value class ProfileResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str external_subject: str | None display_name: str country_code: str | None favorite_team_id: str | None avatar_url: str | None points_total: int collection_points: int quiz_points: int prediction_points: int challenge_points: int achievement_points: int metadata_json: dict[str, Any] created_at: datetime updated_at: datetime class CompetitionResponse(BaseModel): model_config = ConfigDict(from_attributes=True) code: str name: str season: str governing_body: str | None starts_at: datetime | None ends_at: datetime | None active: bool metadata_json: dict[str, Any] class TeamResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str name: str short_name: str | None fifa_code: str | None country_code: str | None confederation: str | None group_name: str | None active: bool metadata_json: dict[str, Any] class StadiumResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str name: str city: str country: str capacity: int | None latitude: str | None longitude: str | None active: bool metadata_json: dict[str, Any] class StickerResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str name: str rarity: str team_id: str | None image_url: str | None active: bool metadata_json: dict[str, Any] class MatchResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str fifa_match_no: int | None stage: str group_name: str | None home_team_id: str | None away_team_id: str | None stadium_id: str | None kickoff_at: datetime | None status: str home_score: int | None away_score: int | None winner_team_id: str | None is_giant_killing: bool active: bool metadata_json: dict[str, Any] class ChallengeResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str slug: str title: str description: str category: str points: int repeatable: bool active: bool rules_json: dict[str, Any] metadata_json: dict[str, Any] class BadgeResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str slug: str title: str description: str tier: str icon: str | None points: int active: bool criteria_json: dict[str, Any] metadata_json: dict[str, Any] class CollectionAddRequest(BaseModel): item_type: str item_id: str = Field(min_length=1, max_length=96) source: str = Field(default="manual", max_length=40) metadata_json: dict[str, Any] = Field(default_factory=dict) @field_validator("item_type") @classmethod def validate_item_type(cls, value: str) -> str: normalized = value.lower() if normalized not in SUPPORTED_COLLECTION_TYPES: raise ValueError(f"item_type must be one of {sorted(SUPPORTED_COLLECTION_TYPES)}") return normalized class CollectionItemResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str user_id: str item_type: str item_id: str source: str points_awarded: int acquired_at: datetime metadata_json: dict[str, Any] class QuizQuestionPublicResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str competition_code: str slug: str prompt: str options_json: list[dict[str, Any]] difficulty: str scheduled_date: date | None active: bool metadata_json: dict[str, Any] class QuizAnswerRequest(BaseModel): question_id: str selected_option: str = Field(min_length=1, max_length=40) metadata_json: dict[str, Any] = Field(default_factory=dict) class QuizAnswerResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str user_id: str question_id: str selected_option: str is_correct: bool points_awarded: int answered_at: datetime metadata_json: dict[str, Any] class PredictionSubmitRequest(BaseModel): match_id: str predicted_home_score: int = Field(ge=0, le=30) predicted_away_score: int = Field(ge=0, le=30) predicted_winner_team_id: str | None = None giant_killing_pick: bool = False class PredictionResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str user_id: str match_id: str predicted_home_score: int predicted_away_score: int predicted_winner_team_id: str | None giant_killing_pick: bool submitted_at: datetime locked_at: datetime | None evaluated_at: datetime | None points_awarded: int score_breakdown_json: dict[str, Any] class MatchResultUpdateRequest(BaseModel): status: str = Field(default="final") home_score: int = Field(ge=0, le=30) away_score: int = Field(ge=0, le=30) winner_team_id: str | None = None is_giant_killing: bool = False metadata_json: dict[str, Any] = Field(default_factory=dict) class BadgeUnlockResponse(BaseModel): badge_id: str slug: str title: str tier: str icon: str | None points_awarded: int unlocked_at: datetime reason_json: dict[str, Any] class UserBadgeResponse(BaseModel): user_badge_id: str badge_id: str slug: str title: str description: str tier: str icon: str | None points_awarded: int unlocked_at: datetime reason_json: dict[str, Any] class ChallengeCompletionResponse(BaseModel): challenge_id: str slug: str title: str category: str progress_count: int target_count: int points_awarded: int completed_at: datetime | None class ChallengeProgressResponse(BaseModel): challenge_id: str slug: str title: str description: str category: str progress_count: int target_count: int completed: bool completed_at: datetime | None points_awarded: int state_json: dict[str, Any] class AchievementEventResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str user_id: str event_type: str points_delta: int payload_json: dict[str, Any] created_at: datetime class ProfileCreateResponse(BaseModel): profile: ProfileResponse created: bool new_badges: list[BadgeUnlockResponse] completed_challenges: list[ChallengeCompletionResponse] class CollectionAddResponse(BaseModel): item: CollectionItemResponse duplicate: bool points_awarded: int profile: ProfileResponse new_badges: list[BadgeUnlockResponse] completed_challenges: list[ChallengeCompletionResponse] class QuizAnswerResultResponse(BaseModel): answer: QuizAnswerResponse correct: bool explanation: str | None points_awarded: int profile: ProfileResponse new_badges: list[BadgeUnlockResponse] completed_challenges: list[ChallengeCompletionResponse] class PredictionSubmitResponse(BaseModel): prediction: PredictionResponse profile: ProfileResponse class MatchResultEvaluationResponse(BaseModel): match: MatchResponse evaluated_predictions: int points_awarded_total: int new_badges: list[BadgeUnlockResponse] completed_challenges: list[ChallengeCompletionResponse] class PassportProgressResponse(BaseModel): profile: ProfileResponse collection_counts: dict[str, int] content_totals: dict[str, int] collection_completion: dict[str, float] quiz: dict[str, int] predictions: dict[str, int] challenges: list[ChallengeProgressResponse] badges: list[UserBadgeResponse] recent_events: list[AchievementEventResponse] class LeaderboardEntryResponse(BaseModel): rank: int user_id: str display_name: str country_code: str | None metric: str score: int class SeedImportResponse(BaseModel): counts: dict[str, int] created: dict[str, int] updated: dict[str, int] class ModerationReportCreateRequest(BaseModel): reporter_user_id: str | None = None target_type: str = Field(min_length=1, max_length=40) target_id: str = Field(min_length=1, max_length=96) reason: str = Field(min_length=3, max_length=2000) details_json: dict[str, Any] = Field(default_factory=dict) class ModerationReportResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: str reporter_user_id: str | None target_type: str target_id: str reason: str status: str details_json: dict[str, Any] created_at: datetime resolved_at: datetime | None resolution_note: str | None