from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _


# ── Choices ───────────────────────────────────────────────────────────────────

class GearStatus(models.TextChoices):
    PENDING  = 'PENDING',  _('Pending Verification')
    VERIFIED = 'VERIFIED', _('Verified')
    REJECTED = 'REJECTED', _('Rejected')


class GearType(models.TextChoices):
    FIREARM    = 'FIREARM',    _('Firearm')
    SCOPE      = 'SCOPE',      _('Scope')
    SUPPRESSOR = 'SUPPRESSOR', _('Suppressor')
    BIPOD      = 'BIPOD',      _('Bipod')
    MAGAZINE   = 'MAGAZINE',   _('Magazine')


class FirearmType(models.TextChoices):
    RIFLE    = 'RIFLE',    _('Rifle')
    PISTOL   = 'PISTOL',   _('Pistol')
    SHOTGUN  = 'SHOTGUN',  _('Shotgun')
    REVOLVER = 'REVOLVER', _('Revolver')
    CARBINE  = 'CARBINE',  _('Carbine')



class ReticleType(models.TextChoices):
    DUPLEX      = 'DUPLEX',      _('Duplex')
    MILDOT      = 'MILDOT',      _('Mil-Dot')
    BDC         = 'BDC',         _('BDC')
    ILLUMINATED = 'ILLUMINATED', _('Illuminated')
    ETCHED      = 'ETCHED',      _('Etched Glass')


class AdjustmentUnit(models.TextChoices):
    MOA  = 'MOA',  _('MOA (Minute of Angle)')
    MRAD = 'MRAD', _('MRAD (Milliradian)')


class FocalPlane(models.TextChoices):
    FFP = 'FFP', _('First Focal Plane (FFP)')
    SFP = 'SFP', _('Second Focal Plane (SFP)')


class AttachmentType(models.TextChoices):
    PICATINNY  = 'PICATINNY',  _('Picatinny Rail')
    SLING_STUD = 'SLING_STUD', _('Sling Stud')
    ARCA_SWISS = 'ARCA_SWISS', _('Arca-Swiss')
    M_LOK      = 'M_LOK',      _('M-LOK')
    KEYMOD     = 'KEYMOD',     _('KeyMod')


class RigRole(models.TextChoices):
    PRIMARY    = 'PRIMARY',    _('Primary Firearm')
    OPTIC      = 'OPTIC',      _('Optic / Scope')
    SUPPRESSOR = 'SUPPRESSOR', _('Suppressor')
    BIPOD      = 'BIPOD',      _('Bipod')
    MAGAZINE   = 'MAGAZINE',   _('Magazine')
    OTHER      = 'OTHER',      _('Other Accessory')


class BulletType(models.TextChoices):
    FMJ   = 'FMJ',   _('Full Metal Jacket')
    HP    = 'HP',    _('Hollow Point')
    BTHP  = 'BTHP',  _('Boat Tail Hollow Point')
    SP    = 'SP',    _('Soft Point')
    HPBT  = 'HPBT',  _('Hollow Point Boat Tail')
    SMK   = 'SMK',   _('Sierra MatchKing')
    A_TIP = 'A_TIP', _('Hornady A-Tip')
    MONO  = 'MONO',  _('Monolithic / Solid')


class PrimerSize(models.TextChoices):
    SMALL_PISTOL    = 'SP',  _('Small Pistol')
    LARGE_PISTOL    = 'LP',  _('Large Pistol')
    SMALL_RIFLE     = 'SR',  _('Small Rifle')
    LARGE_RIFLE     = 'LR',  _('Large Rifle')
    LARGE_RIFLE_MAG = 'LRM', _('Large Rifle Magnum')


class CaseMaterial(models.TextChoices):
    BRASS         = 'BRASS',    _('Brass')
    STEEL         = 'STEEL',    _('Steel')
    ALUMINUM      = 'ALUMINUM', _('Aluminum')
    NICKEL_PLATED = 'NICKEL',   _('Nickel-Plated Brass')


class PowderType(models.TextChoices):
    BALL     = 'BALL',     _('Ball / Spherical')
    EXTRUDED = 'EXTRUDED', _('Extruded / Stick')
    FLAKE    = 'FLAKE',    _('Flake')


class CrimpType(models.TextChoices):
    NONE  = 'NONE',  _('No Crimp')
    TAPER = 'TAPER', _('Taper Crimp')
    ROLL  = 'ROLL',  _('Roll Crimp')


# ── Gear catalog (MTI) ────────────────────────────────────────────────────────

class Gear(models.Model):
    """
    Base catalog entry shared by all gear types.
    Concrete entries live in child tables via multi-table inheritance.
    gear_type acts as a discriminator and is set automatically by each subclass.
    """
    brand       = models.CharField(_('brand'), max_length=100)
    model_name  = models.CharField(_('model name'), max_length=150)
    description = models.TextField(_('description'), blank=True)
    gear_type   = models.CharField(
        _('gear type'), max_length=20, choices=GearType.choices, editable=False
    )
    status = models.CharField(
        _('status'), max_length=10, choices=GearStatus.choices, default=GearStatus.PENDING
    )
    submitted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL, related_name='submitted_gears',
        verbose_name=_('submitted by'),
    )
    reviewed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL, related_name='reviewed_gears',
        verbose_name=_('reviewed by'),
    )
    reviewed_at = models.DateTimeField(_('reviewed at'), null=True, blank=True)
    created_at  = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at  = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name = _('gear')
        verbose_name_plural = _('gears')
        ordering = ['brand', 'model_name']
        constraints = [
            models.UniqueConstraint(
                fields=['brand', 'model_name'], name='unique_gear_brand_model'
            )
        ]

    def __str__(self):
        return f"{self.brand} {self.model_name}"

    def verify(self, reviewed_by):
        self.status = GearStatus.VERIFIED
        self.reviewed_by = reviewed_by
        self.reviewed_at = timezone.now()
        self.save(update_fields=['status', 'reviewed_by', 'reviewed_at'])

    def reject(self, reviewed_by):
        self.status = GearStatus.REJECTED
        self.reviewed_by = reviewed_by
        self.reviewed_at = timezone.now()
        self.save(update_fields=['status', 'reviewed_by', 'reviewed_at'])


class Firearm(Gear):
    firearm_type      = models.CharField(_('firearm type'), max_length=10, choices=FirearmType.choices)
    caliber           = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('caliber'),
    )
    barrel_length_mm  = models.DecimalField(
        _('barrel length (mm)'), max_digits=6, decimal_places=1, null=True, blank=True
    )
    magazine_capacity = models.PositiveSmallIntegerField(
        _('magazine capacity'), null=True, blank=True
    )

    def save(self, *args, **kwargs):
        self.gear_type = GearType.FIREARM
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('firearm')
        verbose_name_plural = _('firearms')


class Scope(Gear):
    magnification_min     = models.DecimalField(_('min magnification'), max_digits=5, decimal_places=1)
    magnification_max     = models.DecimalField(_('max magnification'), max_digits=5, decimal_places=1)
    objective_diameter_mm = models.DecimalField(
        _('objective diameter (mm)'), max_digits=5, decimal_places=1
    )
    tube_diameter_mm = models.DecimalField(
        _('tube diameter (mm)'), max_digits=5, decimal_places=1, default=30
    )
    reticle_type    = models.CharField(
        _('reticle type'), max_length=20, choices=ReticleType.choices, blank=True
    )
    adjustment_unit = models.CharField(
        _('adjustment unit'), max_length=4, choices=AdjustmentUnit.choices, blank=True
    )
    focal_plane     = models.CharField(
        _('focal plane'), max_length=3, choices=FocalPlane.choices, blank=True
    )

    def save(self, *args, **kwargs):
        self.gear_type = GearType.SCOPE
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('scope')
        verbose_name_plural = _('scopes')


class Suppressor(Gear):
    max_caliber  = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('max caliber'),
    )
    thread_pitch = models.CharField(_('thread pitch'), max_length=20, blank=True)
    length_mm    = models.DecimalField(
        _('length (mm)'), max_digits=6, decimal_places=1, null=True, blank=True
    )
    weight_g = models.DecimalField(
        _('weight (g)'), max_digits=6, decimal_places=1, null=True, blank=True
    )

    def save(self, *args, **kwargs):
        self.gear_type = GearType.SUPPRESSOR
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('suppressor')
        verbose_name_plural = _('suppressors')


class Bipod(Gear):
    min_height_mm   = models.DecimalField(
        _('min height (mm)'), max_digits=6, decimal_places=1, null=True, blank=True
    )
    max_height_mm   = models.DecimalField(
        _('max height (mm)'), max_digits=6, decimal_places=1, null=True, blank=True
    )
    attachment_type = models.CharField(
        _('attachment type'), max_length=20, choices=AttachmentType.choices, blank=True
    )

    def save(self, *args, **kwargs):
        self.gear_type = GearType.BIPOD
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('bipod')
        verbose_name_plural = _('bipods')


class Magazine(Gear):
    caliber  = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('caliber'),
    )
    capacity = models.PositiveSmallIntegerField(_('capacity'))

    def save(self, *args, **kwargs):
        self.gear_type = GearType.MAGAZINE
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('magazine')
        verbose_name_plural = _('magazines')


# ── Helpers ───────────────────────────────────────────────────────────────────

_GEAR_TYPE_ATTR = {
    GearType.FIREARM:    'firearm',
    GearType.SCOPE:      'scope',
    GearType.SUPPRESSOR: 'suppressor',
    GearType.BIPOD:      'bipod',
    GearType.MAGAZINE:   'magazine',
}


def get_concrete_gear(gear):
    """Return the concrete MTI subclass instance for a base Gear instance."""
    attr = _GEAR_TYPE_ATTR.get(gear.gear_type)
    if attr:
        try:
            return getattr(gear, attr)
        except Exception:
            pass
    return gear


# ── User inventory ────────────────────────────────────────────────────────────

class UserGear(models.Model):
    """A user's personal instance of a catalog Gear entry."""
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        related_name='inventory', verbose_name=_('user'),
    )
    gear = models.ForeignKey(
        Gear, on_delete=models.CASCADE,
        related_name='user_instances', verbose_name=_('gear'),
    )
    nickname      = models.CharField(_('nickname'), max_length=100, blank=True)
    serial_number = models.CharField(_('serial number'), max_length=100, blank=True)
    purchase_date = models.DateField(_('purchase date'), null=True, blank=True)
    notes         = models.TextField(_('notes'), blank=True)
    added_at      = models.DateTimeField(_('added at'), auto_now_add=True)

    class Meta:
        verbose_name = _('owned gear')
        verbose_name_plural = _('owned gears')
        ordering = ['-added_at']

    def __str__(self):
        label = self.nickname or str(self.gear)
        return f"{self.user.email} — {label}"


# ── Rigs ──────────────────────────────────────────────────────────────────────

class Rig(models.Model):
    """A named loadout: a collection of UserGear items with optional roles."""
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        related_name='rigs', verbose_name=_('user'),
    )
    name        = models.CharField(_('name'), max_length=100)
    description = models.TextField(_('description'), blank=True)
    is_public   = models.BooleanField(_('public'), default=False)
    photo       = models.ForeignKey(
        'photos.Photo',
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='rig',
        verbose_name=_('photo'),
    )
    items       = models.ManyToManyField(
        UserGear, through='RigItem', related_name='rigs',
        verbose_name=_('items'),
    )
    # Ballistic computation inputs
    zero_distance_m  = models.PositiveSmallIntegerField(
        _('zero distance (m)'), null=True, blank=True,
    )
    scope_height_mm  = models.DecimalField(
        _('scope height above bore (mm)'), max_digits=5, decimal_places=1,
        null=True, blank=True,
    )
    created_at = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name = _('rig')
        verbose_name_plural = _('rigs')
        ordering = ['-created_at']
        constraints = [
            models.UniqueConstraint(fields=['user', 'name'], name='unique_rig_per_user')
        ]

    def __str__(self):
        return f"{self.user.email} — {self.name}"


class RigItem(models.Model):
    """Through table linking a UserGear to a Rig with an optional role label."""
    rig       = models.ForeignKey(
        Rig, on_delete=models.CASCADE,
        related_name='rig_items', verbose_name=_('rig'),
    )
    user_gear = models.ForeignKey(
        UserGear, on_delete=models.CASCADE,
        related_name='rig_items', verbose_name=_('gear'),
    )
    role = models.CharField(
        _('role'), max_length=20, choices=RigRole.choices, default=RigRole.OTHER
    )

    class Meta:
        verbose_name = _('rig item')
        verbose_name_plural = _('rig items')
        constraints = [
            models.UniqueConstraint(
                fields=['rig', 'user_gear'], name='unique_gear_per_rig'
            )
        ]

    def __str__(self):
        return f"{self.rig.name} / {self.user_gear} [{self.role}]"

    def clean(self):
        if self.user_gear.user_id != self.rig.user_id:
            raise ValidationError(
                {'user_gear': _('This gear does not belong to the rig owner.')}
            )

        if self.role == RigRole.PRIMARY:
            if self.user_gear.gear.gear_type != GearType.FIREARM:
                raise ValidationError(
                    {'role': _('The PRIMARY slot must contain a Firearm.')}
                )
            qs = RigItem.objects.filter(rig=self.rig, role=RigRole.PRIMARY)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.exists():
                raise ValidationError(
                    {'role': _('A rig can only have one primary firearm.')}
                )


# ── Ammo catalog ──────────────────────────────────────────────────────────────

class Ammo(models.Model):
    """
    Commercial/factory ammunition catalog entry.
    Independent of the Gear MTI hierarchy.
    Same PENDING/VERIFIED/REJECTED moderation workflow as Gear.
    """
    brand               = models.CharField(_('brand'), max_length=100)
    name                = models.CharField(_('name'), max_length=150)
    caliber             = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('caliber'),
    )
    bullet_weight_gr    = models.DecimalField(
        _('bullet weight (gr)'), max_digits=6, decimal_places=1
    )
    bullet_type         = models.CharField(
        _('bullet type'), max_length=5, choices=BulletType.choices
    )
    primer_size         = models.CharField(
        _('primer size'), max_length=3, choices=PrimerSize.choices, blank=True
    )
    case_material       = models.CharField(
        _('case material'), max_length=10, choices=CaseMaterial.choices,
        default=CaseMaterial.BRASS
    )
    muzzle_velocity_fps = models.DecimalField(
        _('muzzle velocity (fps)'), max_digits=6, decimal_places=1, null=True, blank=True
    )
    muzzle_energy_ftlb  = models.DecimalField(
        _('muzzle energy (ft·lb)'), max_digits=7, decimal_places=1, null=True, blank=True
    )
    box_count    = models.PositiveSmallIntegerField(_('box count'), null=True, blank=True)
    notes        = models.TextField(_('notes'), blank=True)
    status       = models.CharField(
        _('status'), max_length=10, choices=GearStatus.choices, default=GearStatus.PENDING
    )
    submitted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL, related_name='submitted_ammo',
        verbose_name=_('submitted by'),
    )
    reviewed_by  = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL, related_name='reviewed_ammo',
        verbose_name=_('reviewed by'),
    )
    reviewed_at = models.DateTimeField(_('reviewed at'), null=True, blank=True)
    created_at  = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at  = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name        = _('ammo')
        verbose_name_plural = _('ammo')
        ordering = ['brand', 'name', 'caliber__name']
        constraints = [
            models.UniqueConstraint(
                fields=['brand', 'name', 'caliber'],
                name='unique_ammo_brand_name_caliber'
            )
        ]

    def __str__(self):
        caliber_str = self.caliber.name if self.caliber_id else '?'
        return f"{self.brand} {self.name} ({caliber_str})"

    def verify(self, reviewed_by):
        self.status = GearStatus.VERIFIED
        self.reviewed_by = reviewed_by
        self.reviewed_at = timezone.now()
        self.save(update_fields=['status', 'reviewed_by', 'reviewed_at'])

    def reject(self, reviewed_by):
        self.status = GearStatus.REJECTED
        self.reviewed_by = reviewed_by
        self.reviewed_at = timezone.now()
        self.save(update_fields=['status', 'reviewed_by', 'reviewed_at'])


# ── Reloading components ───────────────────────────────────────────────────────

class ComponentMixin(models.Model):
    """
    Shared moderation fields for user-submitted reload components.
    Existing catalog entries default to VERIFIED for backward compatibility.
    """
    status = models.CharField(
        _('status'), max_length=10, choices=GearStatus.choices, default=GearStatus.VERIFIED
    )
    submitted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='submitted_%(class)ss',
        verbose_name=_('submitted by'),
    )

    class Meta:
        abstract = True

    def verify(self, reviewed_by=None):
        self.status = GearStatus.VERIFIED
        self.save(update_fields=['status'])

    def reject(self, reviewed_by=None):
        self.status = GearStatus.REJECTED
        self.save(update_fields=['status'])


class Primer(ComponentMixin):
    """Primer reference — can be user-submitted (PENDING) or admin-verified."""
    brand = models.CharField(_('brand'), max_length=100)
    name  = models.CharField(_('name'), max_length=100)
    size  = models.CharField(_('size'), max_length=3, choices=PrimerSize.choices)
    notes = models.TextField(_('notes'), blank=True)

    class Meta:
        verbose_name        = _('primer')
        verbose_name_plural = _('primers')
        ordering = ['brand', 'name']
        constraints = [
            models.UniqueConstraint(fields=['brand', 'name'], name='unique_primer_brand_name')
        ]

    def __str__(self):
        return f"{self.brand} {self.name} ({self.get_size_display()})"


class Brass(ComponentMixin):
    """Brass/case reference — can be user-submitted (PENDING) or admin-verified."""
    brand          = models.CharField(_('brand'), max_length=100)
    caliber        = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('caliber'),
    )
    primer_pocket  = models.CharField(
        _('primer pocket'), max_length=3, choices=PrimerSize.choices, blank=True
    )
    trim_length_mm = models.DecimalField(
        _('trim-to length (mm)'), max_digits=6, decimal_places=2, null=True, blank=True
    )
    notes = models.TextField(_('notes'), blank=True)

    class Meta:
        verbose_name        = _('brass')
        verbose_name_plural = _('brass')
        ordering = ['brand', 'caliber__name']
        constraints = [
            models.UniqueConstraint(
                fields=['brand', 'caliber'], name='unique_brass_brand_caliber'
            )
        ]

    def __str__(self):
        caliber_str = self.caliber.name if self.caliber_id else '?'
        return f"{self.brand} {caliber_str}"


class Bullet(ComponentMixin):
    """Bullet/projectile reference — can be user-submitted (PENDING) or admin-verified."""
    brand       = models.CharField(_('brand'), max_length=100)
    model_name  = models.CharField(_('model name'), max_length=150)
    weight_gr   = models.DecimalField(_('weight (gr)'), max_digits=6, decimal_places=1)
    bullet_type = models.CharField(_('bullet type'), max_length=5, choices=BulletType.choices)
    diameter_mm = models.DecimalField(
        _('diameter (mm)'), max_digits=5, decimal_places=3, null=True, blank=True
    )
    length_mm = models.DecimalField(
        _('length (mm)'), max_digits=5, decimal_places=2, null=True, blank=True
    )
    bc_g1 = models.DecimalField(
        _('BC (G1)'), max_digits=5, decimal_places=4, null=True, blank=True
    )
    bc_g7 = models.DecimalField(
        _('BC (G7)'), max_digits=5, decimal_places=4, null=True, blank=True
    )

    class Meta:
        verbose_name        = _('bullet')
        verbose_name_plural = _('bullets')
        ordering = ['brand', 'model_name', 'weight_gr']
        constraints = [
            models.UniqueConstraint(
                fields=['brand', 'model_name', 'weight_gr'],
                name='unique_bullet_brand_model_weight'
            )
        ]

    def __str__(self):
        return f"{self.brand} {self.model_name} {self.weight_gr}gr"


class Powder(ComponentMixin):
    """Propellant powder reference — can be user-submitted (PENDING) or admin-verified."""
    brand           = models.CharField(_('brand'), max_length=100)
    name            = models.CharField(_('name'), max_length=100)
    powder_type     = models.CharField(
        _('powder type'), max_length=10, choices=PowderType.choices, blank=True
    )
    burn_rate_index = models.PositiveSmallIntegerField(
        _('burn rate index'), null=True, blank=True,
        help_text=_('Lower = faster burning. Used for relative ordering only.'),
    )
    notes = models.TextField(_('notes'), blank=True)

    class Meta:
        verbose_name        = _('powder')
        verbose_name_plural = _('powders')
        ordering = ['burn_rate_index', 'brand', 'name']
        constraints = [
            models.UniqueConstraint(
                fields=['brand', 'name'], name='unique_powder_brand_name'
            )
        ]

    def __str__(self):
        return f"{self.brand} {self.name}"


# ── Reload development ────────────────────────────────────────────────────────

class ReloadRecipe(models.Model):
    """
    A reloading recipe: a fixed combination of primer, brass, and bullet
    owned by one user. Batches (different powder charges) hang off this.
    """
    user   = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        related_name='reload_recipes', verbose_name=_('user'),
    )
    name   = models.CharField(_('name'), max_length=150)
    caliber = models.ForeignKey(
        'calibers.Caliber', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name=_('caliber'),
    )
    primer = models.ForeignKey(
        Primer, on_delete=models.PROTECT,
        related_name='recipes', verbose_name=_('primer'),
    )
    brass  = models.ForeignKey(
        Brass, on_delete=models.PROTECT,
        related_name='recipes', verbose_name=_('brass'),
    )
    bullet = models.ForeignKey(
        Bullet, on_delete=models.PROTECT,
        related_name='recipes', verbose_name=_('bullet'),
    )
    notes      = models.TextField(_('notes'), blank=True)
    is_public  = models.BooleanField(_('public'), default=False)
    created_at = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name        = _('reload recipe')
        verbose_name_plural = _('reload recipes')
        ordering = ['-created_at']
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'name'], name='unique_recipe_name_per_user'
            )
        ]

    def __str__(self):
        caliber_str = self.caliber.name if self.caliber_id else '?'
        return f"{self.user.email} — {self.name} ({caliber_str})"


class ReloadedAmmoBatch(models.Model):
    """
    A specific powder charge variant within a ReloadRecipe.
    Multiple batches under one recipe represent the powder charge development workflow.
    ShotGroups in apps.tools can link to a batch to track performance per charge.
    """
    recipe           = models.ForeignKey(
        ReloadRecipe, on_delete=models.CASCADE,
        related_name='batches', verbose_name=_('recipe'),
    )
    powder           = models.ForeignKey(
        Powder, on_delete=models.PROTECT,
        related_name='batches', verbose_name=_('powder'),
    )
    powder_charge_gr = models.DecimalField(
        _('powder charge (gr)'), max_digits=5, decimal_places=2
    )
    quantity         = models.PositiveSmallIntegerField(
        _('quantity loaded'), null=True, blank=True
    )
    oal_mm           = models.DecimalField(
        _('overall length (mm)'), max_digits=6, decimal_places=2, null=True, blank=True
    )
    coal_mm          = models.DecimalField(
        _('cartridge overall length to ogive (mm)'), max_digits=6, decimal_places=2,
        null=True, blank=True
    )
    crimp            = models.CharField(
        _('crimp'), max_length=6, choices=CrimpType.choices, default=CrimpType.NONE
    )
    case_prep_notes  = models.TextField(_('case prep notes'), blank=True)
    notes            = models.TextField(_('notes'), blank=True)
    loaded_at        = models.DateField(_('loaded at'), null=True, blank=True)
    created_at       = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at       = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name        = _('reloaded ammo batch')
        verbose_name_plural = _('reloaded ammo batches')
        ordering = ['recipe', 'powder_charge_gr']
        constraints = [
            models.UniqueConstraint(
                fields=['recipe', 'powder', 'powder_charge_gr'],
                name='unique_batch_charge_per_recipe_powder'
            )
        ]

    def __str__(self):
        return f"{self.recipe.name} / {self.powder} {self.powder_charge_gr}gr"
