@startuml ShooterHub Data Model

skinparam classAttributeIconSize 0
skinparam classFontSize 12
skinparam packageFontSize 13
skinparam linetype ortho
hide empty members

' ══════════════════════════════════════════════
' PACKAGE: users
' ══════════════════════════════════════════════
package "users" #EBF5FB {
  class User {
    + id : int
    + email : str <<unique>>
    + username : str
    + display_name : str
    + language : str
    + avatar : FK → Photo <<nullable>>
    + is_active : bool
    + is_staff : bool
    + date_joined : datetime
  }
}

' ══════════════════════════════════════════════
' PACKAGE: calibers
' ══════════════════════════════════════════════
package "calibers" #E9F7EF {
  class Caliber {
    + id : int
    + name : str
    + bullet_diameter_mm : decimal
    + case_length_mm : decimal
    + status : enum {PENDING, VERIFIED, REJECTED}
    + submitted_by : FK → User <<nullable>>
    + reviewed_by : FK → User <<nullable>>
    + created_at : datetime
  }
}

' ══════════════════════════════════════════════
' PACKAGE: photos
' ══════════════════════════════════════════════
package "photos" #FEF9E7 {
  class Photo {
    + id : int
    + data : bytea
    + mime_type : str
    + uploaded_at : datetime
    + uploaded_by : FK → User <<nullable>>
  }

  class GroupPhoto {
    + id : int
    + photo : OneToOne → Photo
    + shot_group : FK → ShotGroup <<nullable>>
    + caption : str
    + is_public : bool
    + created_at : datetime
  }

  class GroupPhotoAnalysis {
    + id : int
    + group_photo : OneToOne → GroupPhoto
    + group_size_mm : decimal <<nullable>>
    + group_size_moa : decimal <<nullable>>
    + mean_radius_mm : decimal <<nullable>>
    + mean_radius_moa : decimal <<nullable>>
    + windage_offset_mm : decimal <<nullable>>
    + windage_offset_moa : decimal <<nullable>>
    + elevation_offset_mm : decimal <<nullable>>
    + elevation_offset_moa : decimal <<nullable>>
    + computed_at : datetime
  }

  class PointOfImpact {
    + id : int
    + group_photo : FK → GroupPhoto
    + shot : OneToOne → Shot <<nullable>>
    + x_px : float
    + y_px : float
  }
}

' ══════════════════════════════════════════════
' PACKAGE: tools
' ══════════════════════════════════════════════
package "tools" #F4ECF7 {
  class ChronographAnalysis {
    + id : int
    + user : FK → User <<nullable>>
    + label : str
    + distance_m : decimal <<nullable>>
    + is_public : bool
    + created_at : datetime
  }

  class ShotGroup {
    + id : int
    + analysis : FK → ChronographAnalysis
    + user : FK → User <<nullable>>
    + label : str
    + distance_m : decimal <<nullable>>
    + ammo : FK → Ammo <<nullable>>
    + ammo_batch : FK → ReloadedAmmoBatch <<nullable>>
    + firearm : FK → Firearm <<nullable>>
  }

  class Shot {
    + id : int
    + group : FK → ShotGroup
    + velocity_ms : decimal
    + sequence : int
    + recorded_at : datetime <<nullable>>
  }
}

' ══════════════════════════════════════════════
' PACKAGE: gears
' ══════════════════════════════════════════════
package "gears" #FDEDEC {

  abstract class Gear {
    + id : int
    + brand : str
    + model : str
    + status : enum {PENDING, VERIFIED, REJECTED}
    + submitted_by : FK → User <<nullable>>
    + reviewed_by : FK → User <<nullable>>
    + created_at : datetime
  }

  class Firearm {
    + gear_ptr : OneToOne → Gear
    + caliber : FK → Caliber
    + barrel_length_mm : decimal <<nullable>>
    + action : str
    + firearm_type : str
  }

  class Scope {
    + gear_ptr : OneToOne → Gear
    + min_magnification : decimal <<nullable>>
    + max_magnification : decimal <<nullable>>
    + objective_mm : decimal <<nullable>>
    + reticle : str
    + click_value_mrad : decimal <<nullable>>
  }

  class Suppressor {
    + gear_ptr : OneToOne → Gear
    + caliber : FK → Caliber <<nullable>>
    + length_mm : decimal <<nullable>>
    + weight_g : decimal <<nullable>>
  }

  class Bipod {
    + gear_ptr : OneToOne → Gear
    + min_height_mm : decimal <<nullable>>
    + max_height_mm : decimal <<nullable>>
  }

  class Magazine {
    + gear_ptr : OneToOne → Gear
    + caliber : FK → Caliber <<nullable>>
    + capacity : int <<nullable>>
  }

  class UserGear {
    + id : int
    + user : FK → User
    + gear : FK → Gear
    + serial_number : str
    + notes : str
    + acquired_at : date <<nullable>>
  }

  class Rig {
    + id : int
    + user : FK → User
    + label : str
    + is_active : bool
    + notes : str
  }

  class RigItem {
    + id : int
    + rig : FK → Rig
    + user_gear : FK → UserGear
    + role : str
  }

  class Ammo {
    + id : int
    + brand : str
    + name : str
    + caliber : FK → Caliber <<nullable>>
    + bullet_weight_gr : decimal <<nullable>>
    + status : enum {PENDING, VERIFIED, REJECTED}
    + submitted_by : FK → User <<nullable>>
  }

  abstract class ComponentMixin {
    + id : int
    + brand : str
    + name : str
    + status : enum {PENDING, VERIFIED, REJECTED}
    + submitted_by : FK → User <<nullable>>
  }

  class Primer {
    + type : str
  }

  class Brass {
    + caliber : FK → Caliber <<nullable>>
  }

  class Bullet {
    + caliber : FK → Caliber <<nullable>>
    + weight_gr : decimal <<nullable>>
    + bc_g7 : decimal <<nullable>>
    + bc_g1 : decimal <<nullable>>
  }

  class Powder {
    + burn_rate : str
  }

  class ReloadRecipe {
    + id : int
    + user : FK → User
    + name : str
    + caliber : FK → Caliber
    + bullet : FK → Bullet
    + primer : FK → Primer <<nullable>>
    + brass : FK → Brass <<nullable>>
    + powder : FK → Powder
    + powder_charge_gr : decimal
    + coal_mm : decimal <<nullable>>
    + notes : str
    + created_at : datetime
  }

  class ReloadedAmmoBatch {
    + id : int
    + recipe : FK → ReloadRecipe
    + user : FK → User
    + quantity : int
    + lot_number : str
    + produced_at : date
    + notes : str
  }
}

' ══════════════════════════════════════════════
' PACKAGE: sessions
' ══════════════════════════════════════════════
package "sessions" #EBF5FB {
  abstract class AbstractSession {
    + id : int
    + user : FK → User
    + date : date
    + label : str
    + location : str
    + weather : str
    + notes : str
    + is_public : bool
    + firearm : FK → Firearm <<nullable>>
    + ammo : FK → Ammo <<nullable>>
    + ammo_batch : FK → ReloadedAmmoBatch <<nullable>>
    + created_at : datetime
  }

  class PRSSession {
    + total_points : int <<nullable>>
    + max_points : int <<nullable>>
  }

  class PRSStage {
    + id : int
    + session : FK → PRSSession
    + stage_number : int
    + points_scored : int <<nullable>>
    + max_points : int <<nullable>>
    + distance_m : decimal <<nullable>>
    + notes : str
  }

  class FreePracticeSession {
    + distance_m : decimal <<nullable>>
    + round_count : int <<nullable>>
  }

  class SpeedShootingSession {
    + target_count : int <<nullable>>
    + time_seconds : decimal <<nullable>>
  }
}

' ══════════════════════════════════════════════
' PACKAGE: social
' ══════════════════════════════════════════════
package "social" #FEF9E7 {
  class Message {
    + id : int
    + sender : FK → User
    + recipient : FK → User
    + subject : str
    + body : text
    + sent_at : datetime
    + read_at : datetime <<nullable>>
    + deleted_by_sender : bool
    + deleted_by_recipient : bool
  }

  class BlogPost {
    + id : int
    + author : FK → User
    + title : str
    + body : text
    + is_public : bool
    + created_at : datetime
    + updated_at : datetime
  }

  class Bug {
    + id : int
    + reporter : FK → User <<nullable>>
    + title : str
    + description : text
    + severity : enum {LOW, MEDIUM, HIGH, CRITICAL}
    + status : enum {OPEN, IN_PROGRESS, RESOLVED, CLOSED}
    + created_at : datetime
    + resolved_at : datetime <<nullable>>
  }

  class Friendship {
    + id : int
    + from_user : FK → User
    + to_user : FK → User
    + status : enum {PENDING, ACCEPTED, BLOCKED}
    + created_at : datetime
    + <<unique>> (from_user, to_user)
  }
}

' ══════════════════════════════════════════════
' RELATIONSHIPS
' ══════════════════════════════════════════════

' users ↔ photos
User "0..1" -- "0..1" Photo : avatar >

' users → calibers
User "1" --> "0..*" Caliber : submitted_by >
User "0..1" --> "0..*" Caliber : reviewed_by >

' photos internal
Photo "1" <-- "0..1" GroupPhoto : photo
GroupPhoto "1" *-- "0..1" GroupPhotoAnalysis : group_photo
GroupPhoto "1" *-- "0..*" PointOfImpact : group_photo

' photos ↔ tools
ShotGroup "0..1" <-- "0..*" GroupPhoto : shot_group
Shot "0..1" <-- "0..1" PointOfImpact : shot

' users → photos
User "0..1" --> "0..*" Photo : uploaded_by >

' tools internal
ChronographAnalysis "1" *-- "0..*" ShotGroup : analysis
ShotGroup "1" *-- "0..*" Shot : group

' tools ↔ users
User "0..1" --> "0..*" ChronographAnalysis : user >
User "0..1" --> "0..*" ShotGroup : user >

' tools ↔ gears
Ammo "0..1" <-- "0..*" ShotGroup : ammo
ReloadedAmmoBatch "0..1" <-- "0..*" ShotGroup : ammo_batch
Firearm "0..1" <-- "0..*" ShotGroup : firearm

' gears inheritance (MTI)
Gear <|-- Firearm
Gear <|-- Scope
Gear <|-- Suppressor
Gear <|-- Bipod
Gear <|-- Magazine

' component inheritance (abstract)
ComponentMixin <|-- Primer
ComponentMixin <|-- Brass
ComponentMixin <|-- Bullet
ComponentMixin <|-- Powder

' gears ↔ calibers
Caliber "0..1" <-- "0..*" Firearm : caliber
Caliber "0..1" <-- "0..*" Suppressor : caliber
Caliber "0..1" <-- "0..*" Magazine : caliber
Caliber "1" <-- "0..*" Ammo : caliber
Caliber "0..1" <-- "0..*" Brass : caliber
Caliber "0..1" <-- "0..*" Bullet : caliber
Caliber "1" <-- "0..*" ReloadRecipe : caliber

' gears ↔ users
User "1" --> "0..*" UserGear : user >
User "1" --> "0..*" Rig : user >
Gear "1" <-- "0..*" UserGear : gear
Rig "1" *-- "0..*" RigItem : rig
UserGear "1" <-- "0..*" RigItem : user_gear

' reload chain
User "1" --> "0..*" ReloadRecipe : user >
User "1" --> "0..*" ReloadedAmmoBatch : user >
ReloadRecipe "1" *-- "0..*" ReloadedAmmoBatch : recipe
ReloadRecipe "1" --> "1" Bullet : bullet >
ReloadRecipe "0..1" --> "0..1" Primer : primer >
ReloadRecipe "0..1" --> "0..1" Brass : brass >
ReloadRecipe "1" --> "1" Powder : powder >

' sessions inheritance
AbstractSession <|-- PRSSession
AbstractSession <|-- FreePracticeSession
AbstractSession <|-- SpeedShootingSession
PRSSession "1" *-- "0..*" PRSStage : session

' sessions ↔ users/gears
User "1" --> "0..*" AbstractSession : user >
Firearm "0..1" <-- "0..*" AbstractSession : firearm
Ammo "0..1" <-- "0..*" AbstractSession : ammo
ReloadedAmmoBatch "0..1" <-- "0..*" AbstractSession : ammo_batch

' social ↔ users
User "1" --> "0..*" Message : sender >
User "1" --> "0..*" Message : recipient >
User "1" --> "0..*" BlogPost : author >
User "0..1" --> "0..*" Bug : reporter >
User "1" --> "0..*" Friendship : from_user >
User "1" --> "0..*" Friendship : to_user >

@enduml
