import { BaseEntity } from '@edr/api-common';
import {
  TrainScheduleStatus as TrainScheduleStatusEnum,
  WagonAllocationSnapshot,
} from '@edr/types';
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';

import { Yard } from '../../rule-engine/entities/yard.entity';
import { Route } from '../../routes/entities/route.entity';
import { TrainSet } from '../../train-sets/entities/train-set.entity';
import { TrainScheduleBooking } from './train-schedule-booking.entity';

export const TRAIN_SCHEDULE_STATUSES = [
  TrainScheduleStatusEnum.Draft,
  TrainScheduleStatusEnum.Scheduled,
  TrainScheduleStatusEnum.Dispatched,
  TrainScheduleStatusEnum.Arrived,
  TrainScheduleStatusEnum.Cancelled,
] as const;

export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];

@Entity({ schema: 'freight', name: 'train_schedules' })
@Index(['scheduledDepartureDate'])
@Index(['status'])
export class TrainSchedule extends BaseEntity {
  @Column({ name: 'train_set_id', type: 'uuid', unique: true })
  trainSetId!: string;

  @OneToOne(() => TrainSet, (trainSet) => trainSet.trainSchedule)
  @JoinColumn({ name: 'train_set_id' })
  trainSet?: TrainSet;

  @Column({ name: 'route_id', type: 'uuid', nullable: true })
  routeId?: string | null;

  @ManyToOne(() => Route)
  @JoinColumn({ name: 'route_id' })
  route?: Route | null;

  @Column({ name: 'origin_station_id', type: 'uuid' })
  originStationId!: string;

  @ManyToOne(() => Yard)
  @JoinColumn({ name: 'origin_station_id' })
  originStation?: Yard;

  @Column({ name: 'destination_station_id', type: 'uuid' })
  destinationStationId!: string;

  @ManyToOne(() => Yard)
  @JoinColumn({ name: 'destination_station_id' })
  destinationStation?: Yard;

  @Column({ name: 'scheduled_departure_date', type: 'timestamptz' })
  scheduledDepartureDate!: Date;

  @Column({ name: 'scheduled_arrival_date', type: 'timestamptz', nullable: true })
  scheduledArrivalDate?: Date | null;

  @Column({ name: 'status', type: 'varchar', length: 20, default: 'DRAFT' })
  status!: TrainScheduleStatus;

  @Column({ name: 'train_number', type: 'varchar', length: 20, nullable: true })
  trainNumber?: string | null;

  // Human-facing unique schedule reference (S-YYYY-NNNNN). Shown on the schedule
  // list, booking windows, and load lists. Assigned at creation from the highest
  // sequence issued this year (see TrainSchedulesRepository.maxReferenceSequence).
  @Column({ name: 'reference', type: 'varchar', length: 20, nullable: true, unique: true })
  reference?: string | null;

  @Column({ name: 'direction', type: 'varchar', length: 10, nullable: true })
  direction?: string | null;

  /**
   * Reverse the wagon ORDER on this train: when true, the built wagon plan is
   * flipped at build so the physically-last wagon sits at position 1. Only the
   * order (sequenceNo) changes — composition and allocations travel with their
   * slot. Frozen at create; every (re)assignment rebuilds under this flag so the
   * stored train order and the schedule order always match. Default false.
   */
  @Column({ name: 'reverse_wagon_order', type: 'boolean', default: false })
  reverseWagonOrder!: boolean;

  @Column({ name: 'actual_departure_at', type: 'timestamptz', nullable: true })
  actualDepartureAt?: Date | null;

  @Column({ name: 'actual_arrival_at', type: 'timestamptz', nullable: true })
  actualArrivalAt?: Date | null;

  @Column({ name: 'prepared_by_user_id', type: 'uuid', nullable: true })
  preparedByUserId?: string | null;

  @Column({ name: 'checked_by_user_id', type: 'uuid', nullable: true })
  checkedByUserId?: string | null;

  @Column({ name: 'max_wagons', type: 'int', default: 53 })
  maxWagons!: number;

  /** OPEN = accepting/holding bookings; FULL = train filled; CLOSED = manually closed. Orthogonal to `status`. */
  @Column({ name: 'booking_window_status', type: 'varchar', length: 10, default: 'OPEN' })
  bookingWindowStatus!: string;

  /**
   * Booking-window lifecycle for the one-booking-day cycle
   * (PRE_WINDOW → OPEN → DOC_REVIEW → PAYMENT → reopen | CLOSED_FOR_DAY | DONE).
   * NULL on legacy and DOMESTIC schedules — the window engine ignores those.
   */
  @Column({ name: 'window_phase', type: 'varchar', length: 20, nullable: true })
  windowPhase?: string | null;

  @Column({ name: 'window_opens_at', type: 'timestamptz', nullable: true })
  windowOpensAt?: Date | null;

  @Column({ name: 'window_closes_at', type: 'timestamptz', nullable: true })
  windowClosesAt?: Date | null;

  @Column({ name: 'doc_review_ends_at', type: 'timestamptz', nullable: true })
  docReviewEndsAt?: Date | null;

  /** Staff finished document review early — starts the batch/payment phase immediately. */
  @Column({ name: 'doc_review_completed_at', type: 'timestamptz', nullable: true })
  docReviewCompletedAt?: Date | null;

  @Column({ name: 'payment_phase_ends_at', type: 'timestamptz', nullable: true })
  paymentPhaseEndsAt?: Date | null;

  /** 1-based count of open→settle cycles run on the booking day. */
  @Column({ name: 'booking_cycle_no', type: 'int', default: 0 })
  bookingCycleNo!: number;

  // ── Booking-window rule snapshot ──────────────────────────────────────────
  // The scheduling rule this train was created with, frozen at creation. A later
  // global-rules edit applies only to FUTURE schedules — an already-open schedule
  // keeps its base rule. The batch board derives its display windows (open time +
  // reopen cycles) from THIS snapshot, never from the live global config. NULL on
  // legacy rows created before the snapshot existed (board falls back to live cfg).
  @Column({ name: 'rule_window_open_hour', type: 'int', nullable: true })
  ruleWindowOpenHour?: number | null;

  /** EAT hour the daily booking desk shuts (equals open hour for a 24h desk). */
  @Column({ name: 'rule_window_close_hour', type: 'int', nullable: true })
  ruleWindowCloseHour?: number | null;

  @Column({ name: 'rule_window_duration_hours', type: 'numeric', precision: 6, scale: 4, nullable: true })
  ruleWindowDurationHours?: number | null;

  /**
   * Frozen reopen gap = doc-review + payment minutes at creation. The board
   * projects each next cycle at close + this delay, then snaps it into office hours.
   */
  @Column({ name: 'rule_reopen_delay_minutes', type: 'int', nullable: true })
  ruleReopenDelayMinutes?: number | null;

  @Column({ name: 'rule_import_window_lead_days', type: 'int', nullable: true })
  ruleImportWindowLeadDays?: number | null;

  @Column({ name: 'rule_export_booking_lead_hours', type: 'int', nullable: true })
  ruleExportBookingLeadHours?: number | null;

  /** Frozen import booking-close offset (minutes before departure). NULL = none. */
  @Column({ name: 'rule_import_close_offset_minutes', type: 'int', nullable: true })
  ruleImportCloseOffsetMinutes?: number | null;

  /** Frozen export booking-close offset (minutes before departure). NULL = none. */
  @Column({ name: 'rule_export_close_offset_minutes', type: 'int', nullable: true })
  ruleExportCloseOffsetMinutes?: number | null;

  // Frozen wagon plan captured once when the schedule leaves the editable
  // DRAFT/SCHEDULED phase (dispatch / arrive / cancel). Admin views of a
  // non-editable schedule read THIS instead of the live wagon↔slot joins, so the
  // historical allocation survives the same physical wagons being re-pinned onto
  // later trains. NULL while DRAFT/SCHEDULED (read live) and on legacy rows.
  @Column({ name: 'wagon_allocation_snapshot', type: 'jsonb', nullable: true })
  wagonAllocationSnapshot?: WagonAllocationSnapshot | null;

  @OneToMany(() => TrainScheduleBooking, (scheduleBooking) => scheduleBooking.trainSchedule)
  scheduleBookings?: TrainScheduleBooking[];
}
