public static class AutoShipStreamConfig
{
    /// <summary>Port the order feed connects to.</summary>
    public const int Port = 9000;

    /// <summary>
    /// Length of one SENDCARTON record. THIS, not a delimiter, is what says
    /// where a record ends.
    ///
    /// The sample record is 1262 characters carrying a CRLF after character
    /// 1023 - which is inside CUSTOM4, not on a field boundary, so that CRLF is
    /// the sender wrapping its output rather than terminating a record.
    /// Splitting on it produces two records that both parse to nonsense. The
    /// framer therefore discards CR and LF wherever they appear and cuts every
    /// RecordLength characters; see SendCartonFramer for why that is safe and
    /// what it costs.
    ///
    /// Changing this to a wrong value does not fail quietly: every record after
    /// the first would fail SendCartonParser's LASTTRAN/LASTCART check.
    /// </summary>
    public const int RecordLength = 1262;

    /// <summary>
    /// How many records one transaction may accumulate before the listener
    /// gives up waiting for LASTTRAN = 'Y' and stores what it has.
    ///
    /// This is a memory guard, not a business rule. A sender that never sets
    /// LASTTRAN would otherwise buffer its whole feed in this process. A
    /// shipment closed this way is marked CloseReason = 'SizeCap' and carries
    /// an error message, because it is probably half a transaction.
    /// </summary>
    public const int MaxRecordsPerShipment = 500;

    /// <summary>
    /// How long an open transaction may sit with no new record before the
    /// listener stores it as it stands.
    ///
    /// Without this a sender that goes quiet mid-transaction, without closing
    /// the connection, holds those cartons in memory indefinitely and nothing
    /// ships. Long enough that a slow sender is not chopped in half; short
    /// enough that a stall is noticed the same shift.
    /// </summary>
    public const int GroupIdleFlushSeconds = 30;

    /// <summary>
    /// Whether LASTTRAN and LASTCART must be populated with Y or N.
    ///
    /// True, and this is load-bearing rather than pedantic. Framing is by
    /// length, so a single wrong-length record from the sender shifts every
    /// record after it - and a shifted record still has an order number, a
    /// carton number and an address, just from the wrong fields. Nothing
    /// downstream can tell. These two flags are the only single-character
    /// fields with a closed set of legal values, so demanding they be populated
    /// is most of what makes a shift detectable. Accepting blank does not: in
    /// testing, a record shifted by three characters landed both flags inside
    /// CUST_NAME, blank on a drop-ship, and sailed through.
    ///
    /// Set false only if a sender genuinely leaves them blank - and understand
    /// that a mis-framed stream will then be shipped rather than rejected.
    /// </summary>
    public static readonly bool RequireTransactionFlags = true;

    /// <summary>
    /// Whether ORDER_EXT is part of the identity of an order.
    ///
    /// False: records are grouped by ORDER alone. The sample carries ORDER_EXT
    /// "0" on its only record, which does not settle whether the field is a
    /// release/extension number that distinguishes two orders sharing an ORDER,
    /// or just a flag. False is the safe default - it keeps one order together
    /// - and the listener logs whenever an order's cartons disagree about
    /// ORDER_EXT, which is the evidence needed to set this true.
    /// </summary>
    public static readonly bool SeparateOrdersByOrderExt = false;

    /// <summary>How many Ready shipments one batch claims.</summary>
    public const int BatchSize = 250;

    /// <summary>Poll intervals, milliseconds.</summary>
    public const int BatchIntervalMs = 5000;
    public const int PrintIntervalMs = 2000;
    public const int DeadlineSweepMs = 15000;

    /// <summary>
    /// How long a carrier call may take before the shipment is failed with
    /// IsTimeOut set.
    ///
    /// This is the deadline the whole design rests on. It guarantees every
    /// shipment reaches a terminal state in bounded time, so a batch always
    /// drains and the printing loop never has to skip one - which is what would
    /// print labels out of order.
    ///
    /// Note it is one deadline for the whole shipment, not one per parcel: a
    /// twelve-carton multi-order shipment is one carrier call and gets the same
    /// 90 seconds as a single parcel. Raise it if large shipments start timing
    /// out.
    /// </summary>
    public const int CarrierDeadlineSeconds = 90;

    /// <summary>
    /// Backstop for a shipment claimed into a batch that the shipping code never
    /// picked up at all. Such a shipment has no ShipStartedAt for the carrier
    /// deadline to measure against, and would otherwise stall its batch forever.
    /// </summary>
    public const int UnstartedDeadlineSeconds = 600;

    /// <summary>
    /// Per-label timeout when handing a job to DocumentJobQueue. Much shorter
    /// than that queue's five minute default, which is sized for Crystal report
    /// renders against a slow database - a raw label write is milliseconds.
    /// </summary>
    public const int PrintJobTimeoutSeconds = 30;
}
public static class ShipStatus
{
    public const string Ready = "Ready";
    public const string Processing = "Processing";
    public const string Shipped = "Shipped";
    public const string Failed = "Failed";
}
public static class PrintStatus
{
    public const string Pending = "Pending";

    /// <summary>
    /// Handed to the printer but not confirmed. A record left here after a crash
    /// is ambiguous - the label may or may not have physically come out - and
    /// must never be auto-resolved in either direction.
    /// </summary>
    public const string Sending = "Sending";

    public const string LabelPrinted = "LabelPrinted";

    /// <summary>
    /// Terminal state for a record with nothing to print: a failure or a timeout.
    /// It still occupies its place in the print order.
    /// </summary>
    public const string NoLabel = "NoLabel";

    public const string PrintFailed = "PrintFailed";
}
public static class BatchStatus
{
    public const string Processing = "Processing";
    public const string Printing = "Printing";
    public const string LabelPrinted = "LabelPrinted";

    /// <summary>Printing halted and needs a human. Never cleared automatically.</summary>
    public const string Failed = "Failed";
}
public static class AutoShipStreamControl
{
    // Latched, because Config.CancelIndependentThreadFormula is an AutoResetEvent:
    // it releases exactly ONE waiter and then resets itself. With three loops
    // waiting on it, a single Set() would stop one of them and leave the other two
    // running. The first loop to observe it records the fact here so all three
    // see it.
    private static volatile bool _stopLatched;

    public static bool ShouldStop()
    {
        if (_stopLatched)
        {
            return true;
        }

        System.Threading.AutoResetEvent cancel = Config.CancelIndependentThreadFormula;
        if (cancel != null && cancel.WaitOne(0))
        {
            _stopLatched = true;
        }

        return _stopLatched;
    }

    /// <summary>Waits, but wakes early on shutdown. Returns true if it is time to stop.</summary>
    public static bool Sleep(int milliseconds)
    {
        const int Slice = 250;
        int waited = 0;

        while (waited < milliseconds)
        {
            if (ShouldStop())
            {
                return true;
            }

            System.Threading.Thread.Sleep(Slice);
            waited += Slice;
        }

        return ShouldStop();
    }

    /// <summary>Lets a restart of the formulas run again after a stop.</summary>
    public static void Reset()
    {
        _stopLatched = false;
    }
}
public static class AutoShipDb
{
    private const int CommandTimeoutSeconds = 120;

    public static SqlConnection Open()
    {
        SqlConnection connection = new SqlConnection(ConnectionString.ShipLinkDB);
        connection.Open();
        return connection;
    }

    public static SqlCommand Command(SqlConnection connection, string sql, SqlTransaction transaction)
    {
        SqlCommand command = new SqlCommand(sql, connection, transaction);
        command.CommandTimeout = CommandTimeoutSeconds;
        return command;
    }

    /// <summary>Runs a statement and returns the rows affected.</summary>
    public static int Execute(string sql, params SqlParameter[] parameters)
    {
        using (SqlConnection connection = Open())
        using (SqlCommand command = Command(connection, sql, null))
        {
            if (parameters != null)
            {
                command.Parameters.AddRange(parameters);
            }

            return command.ExecuteNonQuery();
        }
    }

    /// <summary>Runs a query and returns the first column of the first row, or null.</summary>
    public static object Scalar(string sql, params SqlParameter[] parameters)
    {
        using (SqlConnection connection = Open())
        using (SqlCommand command = Command(connection, sql, null))
        {
            if (parameters != null)
            {
                command.Parameters.AddRange(parameters);
            }

            object value = command.ExecuteScalar();
            return value == DBNull.Value ? null : value;
        }
    }

    public static DataTable Query(string sql, params SqlParameter[] parameters)
    {
        using (SqlConnection connection = Open())
        using (SqlCommand command = Command(connection, sql, null))
        {
            if (parameters != null)
            {
                command.Parameters.AddRange(parameters);
            }

            DataTable table = new DataTable();
            using (SqlDataAdapter adapter = new SqlDataAdapter(command))
            {
                adapter.Fill(table);
            }

            return table;
        }
    }

    public static SqlParameter P(string name, object value)
    {
        return new SqlParameter(name, value == null ? DBNull.Value : value);
    }

    public static string Truncate(string value, int maxLength)
    {
        if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
        {
            return value;
        }

        return value.Substring(0, maxLength);
    }
}
public static class ShipmentCloseReason
{
    /// <summary>LASTTRAN = 'Y'. The only clean close.</summary>
    public const string LastTran = "LastTran";

    /// <summary>The sender hung up mid-transaction.</summary>
    public const string ConnectionClosed = "ConnectionClosed";

    /// <summary>Nothing arrived for GroupIdleFlushSeconds.</summary>
    public const string IdleTimeout = "IdleTimeout";

    /// <summary>MaxRecordsPerShipment reached.</summary>
    public const string SizeCap = "SizeCap";
}
// -----------------------------------------------------------------------------
// One label of the print queue, without the ZPL - a large batch would otherwise
// pull every label into memory at once.
//
// Print order is (ShipmentData_Id, Id): shipments in arrival order, and the
// parcels within a shipment in the order their cartons arrived.
// -----------------------------------------------------------------------------
public class ShipmentPrintItem
{
    /// <summary>ShipmentParcel.Id. Print order within the shipment.</summary>
    public long Id;

    /// <summary>ShipmentData.Id. Arrival order of the shipment.</summary>
    public long ShipmentDataId;

    /// <summary>The order this carton belongs to. Null if the record never parsed.</summary>
    public long? ShipmentOrderId;

    public string OrderNumber;
    public string CartonId;
    public string ShipStatus;
    public string PrintStatus;
    public long PrintSeq;
}
