using System;
using System.Data.SQLite;
using System.Threading;
using Serilog;

namespace BulkPrinting
{
    public class BatchDownloader : ThreadHelper
    {
        private const int _voucherPageSize = 5000;
        private const int _retryInterval = 30000;

        private DBHelper _db;
        private Action _refreshAction;
        private Action _downloadLogSummarisationAction;
        private bool _checkForNewBatches;

        public BatchDownloader(DBHelper db, Action refreshAction, Action downloadedLogSummarisationAction)
        {
            _db = db;
            _refreshAction = refreshAction;
            _downloadLogSummarisationAction = downloadedLogSummarisationAction;
        }

        public void CheckForNewBatches()
        {
            lock (_lock)
            {
                _checkForNewBatches = true;
                Monitor.Pulse(_lock);
            }
        }

        protected override void Run()
        {
            var lastSyncedBatchId = Utility.GetSavedParameterAsInt(_db, "LastSyncedBatchId");

            bool skipNewBatchCheck = false;
            var isCancelled = false;
            while (!isCancelled)
            {
                if (Globals.SessionMode == SessionModes.Online)
                {
                    var gotErrors = false;

                    Log.Debug("Checking for downloads");

                    // Check for new batches if required.
                    var doRefresh = false;
                    try
                    {
                        while (!isCancelled && !skipNewBatchCheck)
                        {
                            Log.Debug("Querying batches");

                            Page<Batch> BatchPage = new Page<Batch>();
                            if (Utility.RESTRequest(ref BatchPage, String.Format("/api/batches/?minBatchId={0}", lastSyncedBatchId + 1)))
                            {
                                if (BatchPage.Items.Count == 0)
                                {
                                    // No more new batches, so stop checking until an order is placed.
                                    skipNewBatchCheck = true;
                                    Log.Debug("All batches downloaded");
                                }
                                else
                                {
                                    foreach (var batch in BatchPage.Items)
                                    {
                                        doRefresh = true;
                                        Utility.SaveBatch(_db, batch);
                                        lastSyncedBatchId = batch.Id;
                                        if (!Utility.UpdateSavedParameter(_db, "LastSyncedBatchId", lastSyncedBatchId))
                                        {
                                            // Shouldn't happen.
                                            throw new Exception("Failed to update LastSyncedBatchId");
                                        }
                                    }
                                }
                            }
                            else
                            {
                                // Error, so leave skipNewBatchCheck unchanged.
                                gotErrors = true;
                                break;
                            }

                            lock (_lock)
                            {
                                isCancelled = _cancelled;
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        gotErrors = true;
                        Log.Error(e, "Error while downloading batch list");
                    }

                    // TODO: throttle
                    if (!isCancelled && doRefresh)
                    {
                        doRefresh = false;
                        _refreshAction();
                    }

                    // Continue downloading incomplete batches if any.
                    try
                    {
                        using (var command = _db.CreateCommand(
                            "SELECT Id, ReadyForDownload FROM Batch WHERE Downloaded=0 ORDER BY Downloaded, Id"))
                        {
                            using (SQLiteDataReader row = command.ExecuteReader())
                            {
                                while (! isCancelled && row.Read())
                                {
                                    int batchId = (int)row["Id"];
                                    bool oldReadyForDownload = (bool)row["ReadyForDownload"];
                                    try
                                    {
                                        Batch batch = new Batch();
                                        if (Utility.RESTRequest(ref batch, String.Format("/api/batches/{0}", batchId)))
                                        {
                                            if (batch.ReadyForDownload != oldReadyForDownload)
                                            {
                                                doRefresh = true;
                                            }
                                            Utility.SaveBatch(_db, batch);

                                            if (batch.ReadyForDownload)
                                            {
                                                long result = (long)_db.ExecuteScalar(
                                                    "SELECT COUNT(*) FROM Voucher WHERE BatchId=@BatchId",
                                                    new SQLiteParameter("@BatchId", batchId));
                                                int voucherCount = (int)result;

                                                while (!isCancelled && (voucherCount < batch.DeliveredQuantity))
                                                {
                                                    Log.Debug("Downloading vouchers for batch {0} ({1} so far)", batchId, voucherCount);

                                                    int page = voucherCount / _voucherPageSize + 1;
                                                    int offset = voucherCount % _voucherPageSize;

                                                    Page<Voucher> voucherPage = new Page<Voucher>();
                                                    if (!Utility.RESTRequest(ref voucherPage, String.Format(
                                                        "/api/batches/{0}/vouchers/?page={1}&pageSize={2}",
                                                        batchId, page, _voucherPageSize)))
                                                    {
                                                        break;
                                                    }
                                                    if (voucherPage.Items.Count == 0)
                                                    {
                                                        throw new Exception(String.Format("Too few vouchers were returned for batch {0}", batchId));
                                                    }

                                                    Log.Debug("Downloaded vouchers for batch {0} ({1} so far)", batchId, voucherCount);

                                                    lock (_db.WriteLock)
                                                    {
                                                        using (var trans = _db.BeginTransaction())
                                                        {
                                                            using (var insert = _db.CreateCommand(
                                                                "INSERT INTO Voucher (Id, SequenceNumber, ExpiryDate, Serial, EncryptedPIN, BatchId)" +
                                                                             "VALUES (@Id,@SequenceNumber,@ExpiryDate,@Serial,@EncryptedPin,@BatchId)",
                                                                trans))
                                                            {
                                                                for (var i = offset; i < voucherPage.Items.Count; i++)
                                                                {
                                                                    var voucher = voucherPage.Items[i];
                                                                    if (voucher.SequenceNumber != voucherCount + 1)
                                                                    {
                                                                        throw new Exception(String.Format("Vouchers for batch {0} are not sequential: expecting {1}, got {2}", batchId, voucherCount + 1, voucher.SequenceNumber));
                                                                    }
                                                                    insert.Parameters.Clear();
                                                                    insert.Parameters.AddWithValue("@Id", voucher.Id);
                                                                    insert.Parameters.AddWithValue("@SequenceNumber", voucher.SequenceNumber);
                                                                    insert.Parameters.AddWithValue("@ExpiryDate", voucher.ExpiryDate.Date);
                                                                    insert.Parameters.AddWithValue("@Serial", voucher.Serial);
                                                                    insert.Parameters.AddWithValue("@EncryptedPIN", voucher.EncryptedPIN);
                                                                    insert.Parameters.AddWithValue("@BatchId", batchId);
                                                                    insert.ExecuteNonQuery();
                                                                    voucherCount++;
                                                                }
                                                            }
                                                            trans.Commit();
                                                        }
                                                    }

                                                    Log.Debug("Inserted vouchers for batch {0}", batchId);

                                                    lock (_lock)
                                                    {
                                                        isCancelled = _cancelled;
                                                    }
                                                }

                                                if (voucherCount > batch.DeliveredQuantity)
                                                {
                                                    throw new Exception("Batch contains more than the specified number of vouchers");
                                                }
                                                else if (voucherCount == batch.DeliveredQuantity)
                                                {
                                                    Log.Debug("Batch {0} complete", batchId);

                                                    _db.ExecuteNonQuery(
                                                        "UPDATE Batch SET Downloaded=1 WHERE Id=@BatchId",
                                                        new SQLiteParameter("@BatchId", batchId));

                                                    doRefresh = true;
                                                }
                                            }
                                        }
                                        else if (batch == null)
                                        {
                                            // Batch will only be null if we got a 404.
                                            Log.Debug("Removing deleted batch {0}", batchId);

                                            _db.ExecuteNonQuery(
                                                "DELETE FROM Voucher WHERE BatchId=@BatchId",
                                                new SQLiteParameter("@BatchId", batchId));

                                            _db.ExecuteNonQuery(
                                                "DELETE FROM Batch WHERE Id=@BatchId",
                                                new SQLiteParameter("@BatchId", batchId));

                                            doRefresh = true;
                                        }
                                        else
                                        {
                                            gotErrors = true;
                                        }
                                    }
                                    catch (Exception e)
                                    {
                                        gotErrors = true;
                                        Log.Error(e, "Error while downloading vouchers for batch {0}", batchId);
                                    }

                                    // TODO: throttle
                                    if (!isCancelled && doRefresh)
                                    {
                                        doRefresh = false;
                                        _refreshAction();
                                    }

                                    lock(_lock)
                                    {
                                        isCancelled = _cancelled;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        gotErrors = true;
                        Log.Error(e, "Error while downloading vouchers");
                    }

                    if (! isCancelled && ! gotErrors)
                    {
                        Log.Debug("Up to date");

                        // If we weren't cancelled and didn't get any errors then we should be synchronised with the server,
                        // so it is now safe to summarise any downloaded logs as the corresponding batches and vouchers should
                        // all be present.
                        _downloadLogSummarisationAction();
                    }
                }
                else
                {
                    Log.Debug("Offline, skipping download check");
                }

                Log.Debug("Download thread sleeping");

                lock (_lock)
                {
                    while (!_cancelled && !_checkForNewBatches)
                    {
                        if (! Monitor.Wait(_lock, _retryInterval))
                        {
                            // Timed out. Retry downloads.
                            break;
                        }
                    }
                    if (_checkForNewBatches)
                    {
                        _checkForNewBatches = false;
                        skipNewBatchCheck = false;
                    }
                    isCancelled = _cancelled;
                }

                Log.Debug("Download thread woke up, cancelled={0}", isCancelled);
            }
        }
    }
}
