from __future__ import annotations import uuid from datetime import datetime, timezone from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Numeric, String, Text, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship from app.extensions import db def utcnow() -> datetime: return datetime.now(timezone.utc) class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class EngineFamily(TimestampMixin, db.Model): __tablename__ = "engine_families" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) engine_name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) part_name: Mapped[str | None] = mapped_column(String(255)) cycle: Mapped[str | None] = mapped_column(String(120)) size_m: Mapped[float | None] = mapped_column(Numeric(8, 3)) entry_cost: Mapped[int | None] = mapped_column(Integer) variants: Mapped[list["EngineVariant"]] = relationship( back_populates="family", cascade="all, delete-orphan", lazy="selectin", order_by="EngineVariant.source_sheet_row", ) class EngineVariant(TimestampMixin, db.Model): __tablename__ = "engine_variants" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) family_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("engine_families.id", ondelete="CASCADE"), nullable=False, index=True ) fuel_type: Mapped[str] = mapped_column(String(120), nullable=False) work_env: Mapped[str | None] = mapped_column(String(32)) sl_isp: Mapped[float | None] = mapped_column(Numeric(10, 3)) vac_isp: Mapped[float | None] = mapped_column(Numeric(10, 3)) min_thrust_kn: Mapped[float | None] = mapped_column(Numeric(12, 3)) max_thrust_kn: Mapped[float | None] = mapped_column(Numeric(12, 3)) mass_t: Mapped[float | None] = mapped_column(Numeric(10, 4)) twr: Mapped[float | None] = mapped_column(Numeric(12, 4)) throttle_ratio: Mapped[float | None] = mapped_column(Numeric(8, 4)) tvc_deg: Mapped[float | None] = mapped_column(Numeric(8, 3)) price_kd: Mapped[int | None] = mapped_column(Integer) ignitions: Mapped[int | None] = mapped_column(Integer) has_unlimited_ignitions: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) mod_source: Mapped[str | None] = mapped_column(String(120)) engine_note: Mapped[str | None] = mapped_column(Text) config_name: Mapped[str | None] = mapped_column(String(255), index=True) tech_required: Mapped[str | None] = mapped_column(String(120)) config_note: Mapped[str | None] = mapped_column(Text) source_sheet_row: Mapped[int | None] = mapped_column(Integer) family: Mapped[EngineFamily] = relationship(back_populates="variants") class CommunicationPart(TimestampMixin, db.Model): __tablename__ = "communication_parts" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) part_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) display_name: Mapped[str | None] = mapped_column(String(255)) mass_t: Mapped[float | None] = mapped_column(Numeric(10, 4)) is_active: Mapped[bool | None] = mapped_column(Boolean) is_deployable: Mapped[bool | None] = mapped_column(Boolean) antenna_type: Mapped[str | None] = mapped_column(String(120)) deployed_diameter_m: Mapped[float | None] = mapped_column(Numeric(10, 3)) range_raw: Mapped[float | None] = mapped_column(Numeric(24, 3)) range_km: Mapped[float | None] = mapped_column(Numeric(24, 3)) range_au: Mapped[float | None] = mapped_column(Numeric(18, 6)) range_light_year: Mapped[float | None] = mapped_column(Numeric(18, 6)) angle_deg: Mapped[float | None] = mapped_column(Numeric(10, 3)) speed: Mapped[float | None] = mapped_column(Numeric(10, 3)) idle_power_watt: Mapped[float | None] = mapped_column(Numeric(12, 4)) idle_power_text: Mapped[str | None] = mapped_column(String(64)) transmitting_power_watt: Mapped[float | None] = mapped_column(Numeric(12, 4)) transmitting_power_text: Mapped[str | None] = mapped_column(String(64)) source: Mapped[str | None] = mapped_column(String(120)) entry_cost: Mapped[int | None] = mapped_column(Integer) cost: Mapped[int | None] = mapped_column(Integer) description: Mapped[str | None] = mapped_column(Text) rescale_factor: Mapped[float | None] = mapped_column(Numeric(8, 3)) tweakscale: Mapped[str | None] = mapped_column(String(64)) is_feeder: Mapped[bool | None] = mapped_column(Boolean) tech_required: Mapped[str | None] = mapped_column(String(120)) note: Mapped[str | None] = mapped_column(Text) class TankSpec(TimestampMixin, db.Model): __tablename__ = "tank_specs" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) tank_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) fuel_type: Mapped[str | None] = mapped_column(String(120)) dry_mass_t: Mapped[float | None] = mapped_column(Numeric(10, 4)) fuel_mass_t: Mapped[float | None] = mapped_column(Numeric(10, 4)) wet_mass_t: Mapped[float | None] = mapped_column(Numeric(10, 4)) tank_volume_l: Mapped[float | None] = mapped_column(Numeric(12, 3)) mass_ratio: Mapped[float | None] = mapped_column(Numeric(12, 4)) kiloliters_per_ton: Mapped[float | None] = mapped_column(Numeric(12, 4)) vehicle_name: Mapped[str | None] = mapped_column(String(120)) source: Mapped[str | None] = mapped_column(String(120)) note: Mapped[str | None] = mapped_column(Text) class VehicleCost(TimestampMixin, db.Model): __tablename__ = "vehicle_costs" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) vehicle_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) launch_price: Mapped[int | None] = mapped_column(Integer) source: Mapped[str | None] = mapped_column(String(120)) class Asset(TimestampMixin, db.Model): __tablename__ = "assets" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True) asset_type: Mapped[str] = mapped_column(String(120), nullable=False, index=True) is_retired: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) program: Mapped[str | None] = mapped_column(String(255)) home_region: Mapped[str | None] = mapped_column(String(120)) note: Mapped[str | None] = mapped_column(Text) log_entries: Mapped[list["AssetLogEntry"]] = relationship( back_populates="asset", cascade="all, delete-orphan", lazy="selectin", order_by="AssetLogEntry.start_at", ) docking_children: Mapped[list["DockingEvent"]] = relationship( back_populates="parent_asset", cascade="all, delete-orphan", foreign_keys="DockingEvent.parent_asset_id", lazy="selectin", ) docking_parents: Mapped[list["DockingEvent"]] = relationship( back_populates="child_asset", cascade="all, delete-orphan", foreign_keys="DockingEvent.child_asset_id", lazy="selectin", ) class DockingEvent(TimestampMixin, db.Model): __tablename__ = "docking_events" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) parent_asset_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, index=True ) child_asset_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("assets.id", ondelete="SET NULL"), nullable=True, index=True ) child_label: Mapped[str | None] = mapped_column(String(255)) docked_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) undocked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) dock_note: Mapped[str | None] = mapped_column(Text) undock_note: Mapped[str | None] = mapped_column(Text) parent_asset: Mapped[Asset] = relationship( back_populates="docking_children", foreign_keys=[parent_asset_id], ) child_asset: Mapped[Asset | None] = relationship( back_populates="docking_parents", foreign_keys=[child_asset_id], ) class AssetLogEntry(TimestampMixin, db.Model): __tablename__ = "asset_log_entries" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) asset_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, index=True ) entry_kind: Mapped[str] = mapped_column(String(16), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) mission_label: Mapped[str | None] = mapped_column(String(255)) start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) end_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) summary: Mapped[str | None] = mapped_column(Text) note: Mapped[str | None] = mapped_column(Text) asset: Mapped[Asset] = relationship(back_populates="log_entries") state_nodes: Mapped[list["AssetStateNode"]] = relationship( back_populates="entry", cascade="all, delete-orphan", lazy="selectin", order_by="AssetStateNode.at_time", ) class AssetStateNode(TimestampMixin, db.Model): __tablename__ = "asset_state_nodes" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) entry_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("asset_log_entries.id", ondelete="CASCADE"), nullable=False, index=True ) title: Mapped[str] = mapped_column(String(255), nullable=False) detail: Mapped[str | None] = mapped_column(Text) at_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) target_location: Mapped[str | None] = mapped_column(String(255)) previous_location: Mapped[str | None] = mapped_column(String(255)) transit_location: Mapped[str | None] = mapped_column(String(255)) state_label: Mapped[str | None] = mapped_column(String(120)) entry: Mapped[AssetLogEntry] = relationship(back_populates="state_nodes")