from django.db import models
from django.utils import timezone


# Bambu AMS model-code → human-readable type label.
# Source: live H2C MQTT probe — `print.ams.ams[i].info` field.
# Add new codes as they are observed (e.g. AMS Lite, future variants).
AMS_INFO_TO_TYPE = {
    "1001": "AMS",
    "1003": "AMS 2 Pro",
    "1104": "AMS HT",   # observed on production H2C (last 4 of info code 11001104)
    "2104": "AMS HT",   # observed in dev capture (last 4 of info code 11002104)
}

AMS_TYPE_CHOICES = [
    ("AMS", "AMS"),
    ("AMS 2 Pro", "AMS 2 Pro"),
    ("AMS HT", "AMS HT"),
]


def ams_type_from_info(info_code) -> str:
    """Resolve an AMS unit's `info` model code to a human label.

    Real MQTT `info` codes are 8 characters (e.g. "10001003") with the type encoded
    in the last 4 digits — confirmed against a live H2C with AMS 2 Pro / AMS / AMS HT.
    Fall back to an exact match for the bare 4-digit form in case other firmware
    reports it short.
    """
    if not info_code:
        return ""
    code = str(info_code)
    return AMS_INFO_TO_TYPE.get(code[-4:], "") or AMS_INFO_TO_TYPE.get(code, "")


class PrinterManager(models.Manager):
    """Default manager — scopes every query to actual 3D printers.

    `Printer` shares the `infrastructure_device` table with a host project's other
    device rows (RAE stores its NAS, routers and cameras there too). Without this
    scoping, `Printer.objects.filter(is_active=True).first()` can return a NAS.
    """

    def get_queryset(self):
        return super().get_queryset().filter(category=Printer.CATEGORY_3D_PRINTER)


class Printer(models.Model):
    """Represents a Bambu Lab 3D printer device"""

    CATEGORY_3D_PRINTER = "threed_printer"

    name = models.CharField(max_length=200, help_text="Friendly device name")
    model = models.CharField(max_length=100, help_text="Device model (e.g., X1C, P1S)")
    category = models.CharField(
        max_length=50,
        default=CATEGORY_3D_PRINTER,
        help_text=(
            "Device category. Always 'threed_printer' for printers — present because "
            "host projects may share this table with other device types."
        ),
    )
    manufacturer = models.CharField(
        max_length=100, default="Bambu Lab", help_text="e.g., Bambu Lab"
    )
    description = models.TextField(blank=True, null=True)
    serial_number = models.CharField(max_length=100, blank=True, null=True, unique=True)
    ip_address = models.GenericIPAddressField(blank=True, null=True)
    is_active = models.BooleanField(default=True)
    location = models.CharField(
        max_length=200, blank=True, help_text="Physical location"
    )

    first_seen = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    # `all_objects` is declared first so it serves as the base manager for related
    # descriptors (PrinterMetrics.device etc.) — those must never filter, or rows
    # attached to a mis-categorised device become unreachable.
    all_objects = models.Manager()
    objects = PrinterManager()

    class Meta:
        db_table = "infrastructure_device"
        verbose_name = "Printer"
        verbose_name_plural = "Printers"
        ordering = ["name"]
        base_manager_name = "all_objects"
        default_manager_name = "objects"

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


class PrinterMetrics(models.Model):
    """Time-series metrics for 3D Printer devices (Bambu Lab)"""

    device = models.ForeignKey(
        Printer, on_delete=models.CASCADE, related_name="printer_metrics", db_index=True
    )
    timestamp = models.DateTimeField(
        default=timezone.now, db_index=True, help_text="When this reading was taken"
    )

    # Temperature metrics
    nozzle_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    nozzle_target_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    bed_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    bed_target_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    chamber_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )

    # Nozzle info — single-nozzle / right-side back-compat fields. On dual-nozzle
    # printers (H2C) these mirror the right extruder; the left extruder uses the
    # `_left` columns below.
    nozzle_diameter = models.DecimalField(
        max_digits=3, decimal_places=2, null=True, blank=True
    )
    nozzle_type = models.CharField(max_length=50, null=True, blank=True)

    # H2C dual-nozzle: left-side fields (NULL on single-nozzle printers).
    nozzle_temp_left = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True,
        help_text="Left extruder current temperature (°C). H2C only."
    )
    nozzle_target_temp_left = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True,
        help_text="Left extruder target temperature (°C). H2C only."
    )
    nozzle_diameter_left = models.DecimalField(
        max_digits=3, decimal_places=2, null=True, blank=True,
        help_text="Left nozzle diameter (mm). H2C only."
    )
    nozzle_type_left = models.CharField(
        max_length=50, null=True, blank=True,
        help_text="Left nozzle type (e.g. HS01-0.4). H2C only."
    )

    # Print job status
    gcode_state = models.CharField(
        max_length=50, null=True, blank=True, help_text="FINISH, RUNNING, IDLE, etc."
    )
    print_type = models.CharField(
        max_length=50, null=True, blank=True, help_text="idle, printing, etc."
    )
    print_percent = models.IntegerField(
        null=True, blank=True, help_text="Print progress percentage"
    )
    remaining_time_min = models.IntegerField(
        null=True, blank=True, help_text="Estimated remaining time in minutes"
    )
    layer_num = models.IntegerField(
        null=True, blank=True, help_text="Current layer number"
    )
    total_layer_num = models.IntegerField(
        null=True, blank=True, help_text="Total layers in print"
    )
    print_line_number = models.IntegerField(null=True, blank=True)
    subtask_name = models.CharField(max_length=200, null=True, blank=True)
    gcode_file = models.CharField(max_length=200, null=True, blank=True)

    # Fan speeds (0-100%)
    cooling_fan_speed = models.IntegerField(null=True, blank=True)
    heatbreak_fan_speed = models.IntegerField(null=True, blank=True)
    big_fan1_speed = models.IntegerField(
        null=True, blank=True, help_text="Auxiliary/chamber fan 1 speed"
    )
    big_fan2_speed = models.IntegerField(
        null=True, blank=True, help_text="Auxiliary/chamber fan 2 speed"
    )

    # Speed settings
    spd_lvl = models.IntegerField(
        null=True, blank=True,
        help_text="Speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)",
    )
    spd_mag = models.IntegerField(
        null=True, blank=True, help_text="Speed magnitude percentage"
    )

    # Network & connectivity
    wifi_signal_dbm = models.IntegerField(null=True, blank=True)

    # Error tracking
    print_error = models.IntegerField(default=0)
    has_errors = models.BooleanField(default=False)

    # Chamber light & camera
    chamber_light = models.CharField(
        max_length=20, null=True, blank=True, help_text="on/off"
    )
    ipcam_record = models.CharField(
        max_length=20, null=True, blank=True, help_text="enable/disable"
    )
    timelapse = models.CharField(
        max_length=20, null=True, blank=True, help_text="enable/disable"
    )

    # System info
    stg_cur = models.IntegerField(
        null=True, blank=True, help_text="Current print stage"
    )
    sdcard = models.BooleanField(
        null=True, blank=True, help_text="SD card present"
    )
    gcode_file_prepare_percent = models.CharField(
        max_length=10, null=True, blank=True, help_text="File preparation progress"
    )
    lifecycle = models.CharField(
        max_length=50, null=True, blank=True, help_text="Product lifecycle state"
    )

    # HMS (Health Management System)
    hms = models.JSONField(
        default=list, help_text="Health management system messages (errors/warnings)"
    )

    # AMS (Automatic Material System) status
    ams_unit_count = models.IntegerField(null=True, blank=True)
    ams_status = models.IntegerField(null=True, blank=True)
    ams_rfid_status = models.IntegerField(null=True, blank=True)
    ams_humidity = models.IntegerField(
        null=True, blank=True, help_text="AMS humidity level (processed)"
    )
    ams_humidity_raw = models.IntegerField(
        null=True, blank=True, help_text="AMS raw humidity reading"
    )
    ams_temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    ams_version = models.IntegerField(
        null=True, blank=True, help_text="AMS firmware version"
    )
    tray_is_bbl_bits = models.CharField(
        max_length=20, null=True, blank=True,
        help_text="Which trays have Bambu Lab (OEM) filament",
    )
    tray_read_done_bits = models.CharField(
        max_length=20, null=True, blank=True,
        help_text="RFID read completion status bits",
    )

    # JSON fields for complex nested data
    filaments = models.JSONField(
        default=list,
        help_text="List of filament info [{tray_id, slot, type, sub_type, color, remain_percent, k, ...}]",
    )
    ams_units = models.JSONField(
        default=list,
        help_text="AMS unit info [{unit_id, ams_id, chip_id, humidity, temp, ...}]",
    )
    external_spool = models.JSONField(
        default=dict, help_text="External spool info {type, color, remain}"
    )
    lights_report = models.JSONField(
        default=list, help_text="Light status report [{node, mode}]"
    )

    # Groundwork for H2C's Vortek nozzle-changer rack (6 swappable hotends + 1 fixed
    # left nozzle) — the full MQTT schema for per-slot state isn't confirmed yet, so
    # the raw `print.device` payload is captured here unfiltered to avoid losing data
    # ahead of proper per-slot modeling.
    vortek_raw = models.JSONField(
        default=dict, blank=True, help_text="Raw print.device MQTT payload (Vortek rack groundwork)"
    )

    # Parsed device.nozzle.info[] from this poll, one dict per entry (mirrors
    # HotendInfo.to_dict()). Includes induction-chip hotends *and* non-inductive
    # nozzle positions (e.g. H2C's fixed left nozzle) that have no stable serial
    # number to key a Hotend registry row on — kept here so the dashboard can show
    # their readable type/diameter without claiming an identity/history we don't have.
    nozzle_info = models.JSONField(
        default=list, blank=True, help_text="Parsed per-poll nozzle/hotend info list"
    )

    class Meta:
        db_table = "infrastructure_printer_metrics"
        verbose_name = "Printer Metric"
        verbose_name_plural = "Printer Metrics"
        ordering = ["-timestamp"]
        indexes = [
            models.Index(fields=["device", "-timestamp"], name="printer_dev_time_idx"),
            models.Index(fields=["-timestamp"], name="printer_time_idx"),
        ]

    def __str__(self):
        return f"{self.device.name} @ {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"


class FilamentType(models.Model):
    """Central registry of filament types (material + sub-type + brand)"""

    type = models.CharField(max_length=50, help_text="Base material: PLA, PETG, ABS, etc.")
    sub_type = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="Sub-type: PLA Basic, PLA Matte, etc."
    )
    brand = models.CharField(
        max_length=100, default='Bambu Lab',
        help_text="Manufacturer name"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "infrastructure_filament_type"
        verbose_name = "Filament Type"
        verbose_name_plural = "Filament Types"
        ordering = ['type', 'sub_type', 'brand']
        unique_together = [['type', 'sub_type', 'brand']]

    def __str__(self):
        sub = f" {self.sub_type}" if self.sub_type else ""
        return f"{self.type}{sub} ({self.brand})"


class FilamentColor(models.Model):
    """Master database of Bambu Lab filament colors for auto-matching"""

    color_code = models.CharField(
        max_length=6,
        help_text="Hex color code without padding (e.g., '000000' not '000000FF')"
    )
    color_name = models.CharField(
        max_length=100,
        help_text="Human-readable color name (e.g., 'Black', 'Orange')"
    )

    filament_type_fk = models.ForeignKey(
        'FilamentType', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='colors',
        help_text="Link to FilamentType registry"
    )

    filament_type = models.CharField(
        max_length=50,
        help_text="Base material type: PLA, PETG, ABS, TPU, etc."
    )
    filament_sub_type = models.CharField(
        max_length=100,
        null=True,
        blank=True,
        help_text="Material sub-type: 'PLA Basic', 'PLA Matte', 'ABS GF', etc."
    )
    brand = models.CharField(
        max_length=100,
        default='Bambu Lab',
        help_text="Manufacturer name"
    )
    is_transparent = models.BooleanField(
        default=False,
        help_text="True for clear/transparent filaments — display as checkerboard, not solid color"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "infrastructure_filament_color"
        verbose_name = "Filament Color"
        verbose_name_plural = "Filament Colors"
        ordering = ['filament_type', 'filament_sub_type', 'color_name']
        indexes = [
            models.Index(fields=['color_code', 'filament_type', 'filament_sub_type', 'brand']),
            models.Index(fields=['filament_type']),
        ]
        unique_together = [['color_code', 'filament_type', 'filament_sub_type', 'brand']]

    def __str__(self):
        sub_type_info = f" {self.filament_sub_type}" if self.filament_sub_type else ""
        return f"{self.filament_type}{sub_type_info}: {self.color_name} (#{self.color_code})"

    def get_hex_color(self):
        """Return color code with # prefix for display"""
        return f"#{self.color_code}"


class Filament(models.Model):
    """Master inventory of filament spools owned by user"""

    # Unique identification
    tray_uuid = models.CharField(
        max_length=100, unique=True, null=True, blank=True, db_index=True,
        help_text="Spool serial number from MQTT"
    )
    tag_uid = models.CharField(
        max_length=100, null=True, blank=True, db_index=True,
        help_text="RFID chip unique identifier"
    )
    tag_id = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="User-defined unique identifier (barcode, label, etc.)"
    )

    # Creation tracking
    created_by = models.CharField(
        max_length=20, default='Manual',
        choices=[
            ('Auto Detection', 'Auto Detection'),
            ('Manual', 'Manual'),
        ],
        help_text="How this filament was added to inventory"
    )

    # FK to FilamentType registry
    filament_type = models.ForeignKey(
        'FilamentType', null=True, blank=True, on_delete=models.SET_NULL,
        related_name='filaments',
        help_text="Link to FilamentType registry"
    )

    # Filament specifications
    type = models.CharField(max_length=50, help_text="PLA, PETG, ABS, TPU, etc.")
    sub_type = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="Material sub-type from MQTT: 'PLA Matte', 'PLA Basic', etc."
    )
    brand = models.CharField(max_length=100, help_text="Manufacturer name")
    color = models.CharField(max_length=50, help_text="Color name")
    color_hex = models.CharField(
        max_length=7, null=True, blank=True,
        help_text="Color hex code for display (#RRGGBB)"
    )
    is_transparent = models.BooleanField(
        default=False,
        help_text="True for clear/transparent filaments — display as checkerboard, not solid color"
    )

    # Physical properties
    diameter = models.DecimalField(
        max_digits=4, decimal_places=2, default=1.75,
        help_text="Filament diameter in mm (1.75 or 2.85)"
    )
    initial_weight_grams = models.IntegerField(
        null=True, blank=True,
        help_text="Spool weight when new (typically 1000g)"
    )

    # Current status
    remaining_percent = models.IntegerField(
        default=100,
        help_text="Estimated remaining filament (0-100%)"
    )
    remaining_weight_grams = models.IntegerField(
        null=True, blank=True,
        help_text="Calculated remaining weight"
    )

    # Current location in AMS
    is_loaded_in_ams = models.BooleanField(
        default=False,
        help_text="Is this spool currently loaded in AMS?"
    )
    current_tray_id = models.IntegerField(
        null=True, blank=True,
        help_text="Tray slot index within its AMS unit (0-3 for AMS/AMS 2 Pro, 0 for AMS HT)"
    )
    ams_unit_id = models.PositiveSmallIntegerField(
        null=True, blank=True, db_index=True,
        help_text="Which physical AMS unit this spool is loaded in (matches MQTT ams[i].id; 128 = AMS HT)"
    )
    ams_type = models.CharField(
        max_length=32, blank=True, default="",
        choices=AMS_TYPE_CHOICES,
        help_text="Type of the AMS unit this spool is loaded in (AMS / AMS 2 Pro / AMS HT)"
    )
    last_loaded_date = models.DateTimeField(
        null=True, blank=True,
        help_text="When was this spool loaded into AMS"
    )

    # Purchase/inventory tracking
    purchase_date = models.DateField(null=True, blank=True)
    purchase_price = models.DecimalField(
        max_digits=8, decimal_places=2, null=True, blank=True
    )
    supplier = models.CharField(max_length=100, null=True, blank=True)
    notes = models.TextField(blank=True, help_text="Custom notes about this spool")

    # Timestamps
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    last_used = models.DateTimeField(
        null=True, blank=True,
        help_text="Last time this spool was used in a print"
    )

    class Meta:
        db_table = "infrastructure_filament"
        verbose_name = "Filament Spool"
        verbose_name_plural = "Filament Spools"
        ordering = ['type', 'brand', 'color', '-remaining_percent']
        indexes = [
            models.Index(fields=['type', 'brand', 'color']),
            models.Index(fields=['tray_uuid']),
            models.Index(fields=['tag_uid']),
            models.Index(fields=['tag_id']),
            models.Index(fields=['is_loaded_in_ams', 'current_tray_id']),
            models.Index(fields=['remaining_percent']),
            models.Index(fields=['created_by']),
        ]

    def __str__(self):
        sn_info = f"[SN:{self.tray_uuid[:8]}...] " if self.tray_uuid else ""
        return f"{sn_info}{self.brand} {self.type} - {self.color} ({self.remaining_percent}%)"

    def update_remaining_weight(self):
        """Calculate remaining weight based on percentage"""
        if self.initial_weight_grams:
            self.remaining_weight_grams = int(
                self.initial_weight_grams * (self.remaining_percent / 100.0)
            )


class FilamentSnapshot(models.Model):
    """Links PrinterMetrics to Filament inventory with point-in-time AMS data"""

    printer_metric = models.ForeignKey(
        'PrinterMetrics', on_delete=models.CASCADE,
        related_name='filament_snapshots'
    )
    filament = models.ForeignKey(
        'Filament', on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name='usage_snapshots',
        help_text="Matched filament from inventory (null if no match)"
    )

    tray_id = models.IntegerField(help_text="AMS slot number (0-3)")
    slot_name = models.CharField(
        max_length=20, null=True, blank=True,
        help_text="Slot identifier like A00-W1"
    )
    ams_unit_id = models.PositiveSmallIntegerField(
        null=True, blank=True, db_index=True,
        help_text="Which physical AMS unit this tray belongs to (matches MQTT ams[i].id; 128 = AMS HT)"
    )
    ams_type = models.CharField(
        max_length=32, blank=True, default="",
        choices=AMS_TYPE_CHOICES,
        help_text="Type of the AMS unit this tray belongs to (AMS / AMS 2 Pro / AMS HT)"
    )

    type = models.CharField(max_length=50, null=True, blank=True)
    sub_type = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="Material sub-type from MQTT (PLA Basic, PLA Matte, etc.)"
    )
    brand = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="Deprecated: MQTT doesn't provide brand. Use Filament.brand instead."
    )
    color = models.CharField(max_length=50, null=True, blank=True)
    remain_percent = models.IntegerField(null=True, blank=True)
    k_value = models.DecimalField(
        max_digits=6, decimal_places=4, null=True, blank=True
    )

    tag_uid = models.CharField(
        max_length=100, null=True, blank=True, db_index=True,
        help_text="RFID chip unique identifier"
    )
    tray_uuid = models.CharField(
        max_length=100, null=True, blank=True,
        help_text="Tray UUID from MQTT"
    )
    state = models.IntegerField(
        null=True, blank=True,
        help_text="Tray state from MQTT"
    )

    temp = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True
    )
    humidity = models.IntegerField(null=True, blank=True)

    auto_matched = models.BooleanField(
        default=True,
        help_text="Was this auto-matched to inventory or manually set?"
    )
    match_method = models.CharField(
        max_length=20, default='none',
        help_text="tag_id, lowest_remaining, manual, or none"
    )

    class Meta:
        db_table = "infrastructure_filament_snapshot"
        verbose_name = "Filament Snapshot"
        verbose_name_plural = "Filament Snapshots"
        ordering = ['printer_metric', 'ams_unit_id', 'tray_id']
        indexes = [
            models.Index(fields=['printer_metric', 'tray_id']),
            models.Index(fields=['printer_metric', 'ams_unit_id', 'tray_id']),
            models.Index(fields=['filament']),
        ]

    def __str__(self):
        filament_info = str(self.filament) if self.filament else f"{self.brand} {self.type}"
        return f"Tray {self.tray_id}: {filament_info}"


class BambuCloudTask(models.Model):
    """Cloud task record synced from Bambu Cloud API (v1/user-service/my/tasks)."""

    task_id = models.BigIntegerField(unique=True, db_index=True, help_text="Bambu Cloud task ID (matches MQTT task_id)")
    design_id = models.IntegerField(null=True, blank=True, help_text="Makerworld design ID")
    design_title = models.CharField(max_length=500, blank=True, help_text="Human project name from Makerworld (designTitle)")
    plate_title = models.CharField(max_length=500, blank=True, help_text="Plate/variant name (matches MQTT subtask_name)")
    model_id = models.CharField(max_length=100, blank=True)
    profile_id = models.BigIntegerField(null=True, blank=True, help_text="Bambu Cloud profile ID")
    plate_index = models.SmallIntegerField(null=True, blank=True)
    device_serial = models.CharField(max_length=100, blank=True, help_text="Printer serial number from cloud")
    cover_url = models.URLField(max_length=1000, blank=True, help_text="Plate preview image URL from S3")
    weight_grams = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True, help_text="Actual filament weight reported by cloud")
    length_mm = models.IntegerField(null=True, blank=True, help_text="Filament length in mm")
    cost_time_seconds = models.IntegerField(null=True, blank=True, help_text="Cloud-measured print duration in seconds")
    cloud_status = models.SmallIntegerField(null=True, blank=True, help_text="2=finish, 3=failed")
    bed_type = models.CharField(max_length=50, blank=True)
    use_ams = models.BooleanField(default=True)
    print_mode = models.CharField(max_length=50, blank=True, help_text="cloud_file, local, etc.")
    ams_detail_mapping = models.JSONField(default=list, help_text="Per-slot filament weight breakdown from cloud")
    cloud_start_time = models.DateTimeField(null=True, blank=True)
    cloud_end_time = models.DateTimeField(null=True, blank=True)
    raw_data = models.JSONField(default=dict, help_text="Full task response — preserved for future use")
    synced_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "infrastructure_cloud_task"
        verbose_name = "Bambu Cloud Task"
        verbose_name_plural = "Bambu Cloud Tasks"
        ordering = ["-cloud_start_time"]
        indexes = [
            models.Index(fields=["task_id"]),
            models.Index(fields=["design_id"]),
            models.Index(fields=["-cloud_start_time"]),
        ]

    def __str__(self):
        name = self.design_title or self.plate_title or f"task-{self.task_id}"
        return f"{name} ({self.cloud_start_time.strftime('%Y-%m-%d') if self.cloud_start_time else 'unknown date'})"


class PrintJob(models.Model):
    """Represents a single print job from start to finish"""

    device = models.ForeignKey(
        'Printer', on_delete=models.CASCADE,
        related_name='print_jobs'
    )

    project_name = models.CharField(
        max_length=200, help_text="From subtask_name field"
    )
    gcode_file = models.CharField(max_length=200, null=True, blank=True)

    cloud_task = models.ForeignKey(
        'BambuCloudTask', on_delete=models.SET_NULL,
        null=True, blank=True, related_name='print_jobs',
        help_text="Linked Bambu Cloud task record (set by bambu_sync_cloud or collector)"
    )
    cloud_task_id_raw = models.BigIntegerField(
        null=True, blank=True, db_index=True,
        help_text="MQTT task_id — captured at job start, used to link cloud task"
    )

    start_time = models.DateTimeField(help_text="When print started")
    end_time = models.DateTimeField(null=True, blank=True, help_text="When print finished/failed")
    duration_minutes = models.IntegerField(null=True, blank=True, help_text="Total print duration")

    total_layers = models.IntegerField(null=True, blank=True)
    final_status = models.CharField(
        max_length=50, null=True, blank=True, help_text="FINISH, FAILED, CANCELLED"
    )
    completion_percent = models.IntegerField(
        default=0, help_text="Final completion percentage"
    )

    start_metric = models.ForeignKey(
        'PrinterMetrics', on_delete=models.SET_NULL,
        null=True, related_name='started_jobs'
    )
    end_metric = models.ForeignKey(
        'PrinterMetrics', on_delete=models.SET_NULL,
        null=True, related_name='ended_jobs'
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "infrastructure_print_job"
        verbose_name = "Print Job"
        verbose_name_plural = "Print Jobs"
        ordering = ['-start_time']
        indexes = [
            models.Index(fields=['device', '-start_time']),
            models.Index(fields=['project_name']),
            models.Index(fields=['-start_time']),
        ]

    def __str__(self):
        status = self.final_status or 'In Progress'
        return f"{self.project_name} ({status}) - {self.start_time.strftime('%Y-%m-%d %H:%M')}"

    @property
    def display_name(self):
        """Human-readable job name: cloud design_title if available, else project_name."""
        if self.cloud_task_id and self.cloud_task and self.cloud_task.design_title:
            return self.cloud_task.design_title
        return self.project_name

    def calculate_duration(self):
        """Calculate print duration if end_time is set"""
        if self.end_time and self.start_time:
            delta = self.end_time - self.start_time
            self.duration_minutes = int(delta.total_seconds() / 60)


class FilamentUsage(models.Model):
    """Tracks filament consumption during print jobs"""

    print_job = models.ForeignKey(
        'PrintJob', on_delete=models.CASCADE,
        related_name='filament_usages'
    )
    filament = models.ForeignKey(
        'Filament', on_delete=models.CASCADE,
        related_name='print_usages'
    )

    tray_id = models.IntegerField(help_text="Which AMS slot was used")
    ams_unit_id = models.PositiveSmallIntegerField(
        null=True, blank=True, db_index=True,
        help_text="Which physical AMS unit the slot belongs to (matches MQTT ams[i].id; 128 = AMS HT)"
    )

    starting_percent = models.IntegerField(help_text="Filament remaining % at job start")
    ending_percent = models.IntegerField(
        null=True, blank=True, help_text="Filament remaining % at job end"
    )
    consumed_percent = models.IntegerField(
        null=True, blank=True, help_text="Amount consumed during print"
    )
    consumed_grams = models.IntegerField(
        null=True, blank=True, help_text="Estimated grams consumed"
    )

    is_primary = models.BooleanField(
        default=True, help_text="Primary filament vs multi-color"
    )

    class Meta:
        db_table = "infrastructure_filament_usage"
        verbose_name = "Filament Usage"
        verbose_name_plural = "Filament Usages"
        ordering = ['print_job', 'tray_id']
        indexes = [
            models.Index(fields=['print_job']),
            models.Index(fields=['filament']),
        ]

    def __str__(self):
        return f"{self.filament} - {self.print_job.project_name} ({self.consumed_percent}%)"

    def calculate_consumed(self):
        """Calculate consumed amount"""
        if self.ending_percent is not None:
            self.consumed_percent = self.starting_percent - self.ending_percent
            if self.filament.initial_weight_grams:
                self.consumed_grams = int(
                    self.filament.initial_weight_grams * (self.consumed_percent / 100.0)
                )


class Hotend(models.Model):
    """Registry of individual Vortek hotends, keyed by serial number.

    A Vortek rack holds up to 6 swappable hotends (bays, MQTT `id` 16-21) plus
    1 mounted on the toolhead at a time (MQTT `id` 0). `raw_id` reflects whichever
    address was last seen on the wire for this hotend; `slot_number` is only set
    when that address falls in the 16-21 rack-bay range — confirmed by watching
    a "Read All" MQTT capture reassign a toolhead-mounted hotend's id from 0 to
    its true bay id.
    """

    printer = models.ForeignKey(
        'Printer', on_delete=models.CASCADE, related_name='hotends'
    )
    serial_number = models.CharField(max_length=100, db_index=True)

    nozzle_type = models.CharField(max_length=50, blank=True, default="")
    diameter = models.DecimalField(
        max_digits=3, decimal_places=2, null=True, blank=True
    )

    raw_id = models.PositiveSmallIntegerField(
        help_text="Last-seen MQTT device.nozzle.info[].id"
    )
    slot_number = models.PositiveSmallIntegerField(
        null=True, blank=True,
        help_text="Rack bay 1-6, derived from raw_id 16-21. Null if currently unknown (e.g. mounted on toolhead and id reports as the 0 sentinel)."
    )
    is_toolhead = models.BooleanField(
        default=False,
        help_text="True if currently mounted on the toolhead under normal polling (raw_id == 0)."
    )

    last_filament_profile_id = models.CharField(
        max_length=20, blank=True, default="",
        help_text="Bambu material profile id of the filament last loaded (MQTT fila_id, e.g. 'GFA01')"
    )
    last_color = models.CharField(
        max_length=6, blank=True, default="",
        help_text="6-char hex of the filament last loaded (MQTT color_m, alpha stripped)"
    )

    used_time_seconds = models.PositiveIntegerField(default=0)
    wear_percent = models.DecimalField(
        max_digits=5, decimal_places=2, default=0,
        help_text="MQTT wear (0-128 scale) converted to a 0-100 percent"
    )

    last_seen_at = models.DateTimeField(auto_now=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "infrastructure_hotend"
        verbose_name = "Hotend"
        verbose_name_plural = "Hotends"
        ordering = ['printer', '-is_toolhead', 'slot_number', 'serial_number']
        unique_together = [['printer', 'serial_number']]

    def __str__(self):
        location = "Toolhead" if self.is_toolhead else (
            f"Slot {self.slot_number}" if self.slot_number else "Rack"
        )
        return f"{self.serial_number} ({location})"

    @property
    def used_time_display(self) -> str:
        hours, remainder = divmod(self.used_time_seconds, 3600)
        minutes = remainder // 60
        return f"{hours}h {minutes}m" if hours else f"{minutes}m"


class HotendSnapshot(models.Model):
    """Point-in-time reading of a Hotend, one row per collector poll."""

    printer_metric = models.ForeignKey(
        'PrinterMetrics', on_delete=models.CASCADE,
        related_name='hotend_snapshots'
    )
    hotend = models.ForeignKey(
        'Hotend', on_delete=models.CASCADE,
        related_name='snapshots'
    )

    raw_id = models.PositiveSmallIntegerField()
    used_time_seconds = models.PositiveIntegerField(default=0)
    wear_percent = models.DecimalField(max_digits=5, decimal_places=2, default=0)
    stat = models.IntegerField(
        null=True, blank=True, help_text="Raw MQTT status code for this hotend"
    )
    timestamp = models.DateTimeField(default=timezone.now, db_index=True)

    class Meta:
        db_table = "infrastructure_hotend_snapshot"
        verbose_name = "Hotend Snapshot"
        verbose_name_plural = "Hotend Snapshots"
        ordering = ['printer_metric', 'hotend']
        indexes = [
            models.Index(fields=['printer_metric', 'hotend']),
            models.Index(fields=['hotend', '-timestamp']),
        ]

    def __str__(self):
        return f"{self.hotend.serial_number} @ {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
