using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading;
using BO.AppServer.Metadata.Dto;
using BO.AppServer.Metadata.Enums;
using BO.Console.Commands.Base;
using BO.Console.Commands.Workspace.Sync;
using BO.Console.Commands.Workspace.Sync.Diff;
using Quadarax.Foundation.Core.Data.Diff;
using Quadarax.Foundation.Core.IO;
using Quadarax.Foundation.Core.IO.Extensions;
using Quadarax.Foundation.Core.QConsole;
using Quadarax.Foundation.Core.QConsole.Argument;
using Quadarax.Foundation.Core.QConsole.Attributes;
using Quadarax.Foundation.Core.QConsole.Value;
using Quadarax.Foundation.Core.Value;
using Quadarax.Foundation.Core.Value.Extensions;

namespace BO.Console.Commands.Workspace
{
    [CommandDefinition]
    internal class WorkspaceSyncCommand : AbstractWorkspaceCommand
    {
        #region *** Constants ***

        private const string CS_NEW_IN = ".in";
        private const string CS_NEW_OUT = ".out";

        private const string CS_NOTES = "Directory structure:\n" + 
                                        "["+CS_NEW_IN + "]\t- new artifacts directory for Input type\n" +
                                        "\t\t<doc_name>.<artifact_name>.<artifact_ext> - new artifacts file\n" +
                                        "\t\t...\n" +
                                        "[" +CS_NEW_OUT + "]\t- new artifacts directory for Output type\n" +
                                        "[<documentId:8>.<document_name_normalized>]\t- document directory\n" +
                                        "\t\t<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file\n" +
                                        "\t\t...\n" +
                                        "...\n" +
                                        "\n" +
                                        "Process sequence:\n" +
                                        "0. If init flag set, sync clean directory\n" +
                                        "1. Get workspace documents with artifacts -> srv_arts\n" +
                                        "2. Get sync directory documents with artifacts (skip not well formatted, process incomming also)  -> loac_arts\n" +
                                        "3. Resolve differences srv_arts and loc_ars -> changed_arts\n" +
                                        "4. Push changes changed_arts to database (if delete flag set removes documents in database)\n" +
                                        "5. Pull changes changed_arts from database (if delete flag set removes documents in sync directory)\n" +
                                        "6. Cleanup incommings\n" +
                                        "7. If repeat set go to step 1.\n";


        private const string CS_CMD_WRKSPSYNC_NAME = "workspace_sync";
        private const string CS_CMD_WRKSPSYNC_DESCR = "Download or upload documents from local directory from/to workspace. Can be run as contignous process.";

        private const string CS_ARG_NAME_DIR = "directory";
        private const string CS_ARG_HINT_DIR = "<sync_directory>";
        private const string CS_ARG_DESC_DIR = "Defines sync directory, if not defined workspace directory will be created as workspace name.";

        private const string CS_ARG_NAME_WD = "repeat";
        private const string CS_ARG_HINT_WD = "<is_repeat>";
        private const string CS_ARG_DESC_WD = "Flag if defined, enables contignous monitoring on sync directory for changes. If not defined process will be run once.";

        private const string CS_ARG_NAME_DELETE = "delete";
        private const string CS_ARG_HINT_DELETE = "<is_delete_allowed>";
        private const string CS_ARG_DESC_DELETE = "Flag if defined, deletes documents in application server when document is deleted in sync directory.";

        private const string CS_ARG_NAME_INIT = "init";
        private const string CS_ARG_HINT_INIT = "<is_initialization>";
        private const string CS_ARG_DESC_INIT = "Flag if defined, reset sync directory during initialization (all content in sync directory will be deleted on start).";

        private const string CS_ARG_NAME_INT = "interval";
        private const string CS_ARG_HINT_INT = "<repeat_interval>";
        private const string CS_ARG_DESC_INT = "Timespan interval between two iterations. Default is 00:30:00.";

        private const string CS_ARG_DESC_USER = "If defined, gets APIKey and password from defined user default workspace, otherwise use APIkey from argument or configuration.";

        #endregion

        #region *** Properties ***
        public override string Name => CS_CMD_WRKSPSYNC_NAME;
        public override string Description => CS_CMD_WRKSPSYNC_DESCR;

        public override string Notes => CS_NOTES;


        private string SyncDir { get; set; }
        private bool IsRepeat { get; set; }
        private bool IsInit { get; set; }
        private bool IsDeleteAllowed { get; set; }

        private TimeSpan Interval { get; set; }
        #endregion


        #region *** Constructor ***

        public WorkspaceSyncCommand(Engine engine) : base(engine)
        {
        }

        #endregion

        #region *** EXECUTE ***

        protected override Result OnExecute()
        {

            bool isExitSignal = false;
            int nCnt = 0;

            if (!string.IsNullOrEmpty(UserName))
            {
                WriteDebugInfo($"Getting default workspace info for user '{UserName}'");
                var workspaces = Client.GetWorkspacesAsync(WorkspaceScopeEnums.MyDefault, UserName).Result;
                if (!workspaces.Any())
                    throw new Exception($"No default workspace for user '{UserName}' set.");
                var userWorkspace = workspaces.First();
                WriteInfo($"User has default workspace named '{userWorkspace.Name}' [{userWorkspace.Id}] (Created:{userWorkspace.Audit.Created.ToFileShortDateTimeString()}, Changed:{userWorkspace.Audit.Changed?.ToFileShortDateTimeString()})");
                WriteCaption($"Using APIKey: {userWorkspace.ApiKeyNormalized}");
                SetApiKey(userWorkspace.ApiKeyNormalized, userWorkspace.Apipassword);
            }


            var workspace = Client.GetWorkspaceAsync(WorkspaceIdentificationTypeEnum.ApiKey, ApiKey).Result;
            if (workspace == null)
                return new Result(new Exception($"Workspace with API-KEY: {ApiKey} not found."));
            var dirInfo = OpenDirectory(workspace);

            var iteration = 0;

            while (!isExitSignal)
            {
                if (!IsRepeat)
                    isExitSignal = true;

                WriteInfo($"Iteration {iteration} started.");

                var documents = Client.GetWorkspaceDocumentsAsync(DocumentsScopeEnums.All, ApiKey, ApiKeyPassword, UserName).Result;
                
                var factory = new DocumentEntryComparer();
                foreach (var dir in dirInfo.EnumerateDirectories())
                {
                    if (string.Equals(dir.Name, CS_NEW_IN, StringComparison.CurrentCultureIgnoreCase))
                    {
                        foreach (var file in dir.GetFiles())
                            factory.AddLocalDocumentNew(file.Name,true);

                        continue;
                    }

                    if (string.Equals(dir.Name, CS_NEW_OUT, StringComparison.CurrentCultureIgnoreCase))
                    {
                        foreach (var file in dir.GetFiles())
                            factory.AddLocalDocumentNew(file.Name,false);

                        continue;
                    }

                    if (factory.ValidateDocumentDirectory(dir.Name))
                    {

                        foreach (var file in dir.GetFiles())
                        {

                            if (factory.ValidateArtifactFileName(file.Name))
                            {
                                factory.AddLocalDocument(dir.Name, file.Name, file.Length);
                            }
                            else
                            {
                                WriteWarning($"Invalid artifact file '{file.Name}' name format! Expects '<arifact_id>.<artifact_type>.<artifact_name>.<artifaxt_ext>'. Skip");
                            }
                        }
                    }
                    else
                    {
                        WriteWarning($"Invalid document directory '{dir.Name}' name format! Expects '<documentId:8>.<document_name_normalized>'. Skip");
                    }
                }

                foreach (var document in documents)
                {
                    factory.AddRemoteDocument(document);
                }

                var diff = factory.GetComparator();
                var locals = diff.CompareRightSide().Where(x => x.CompareState != DiffEntry.CompareStateEnum.Equals);
                foreach (var local in locals)
                {
                    var item = local.GetItem<DocumentItem>();
                    if (local.CompareState == DiffEntry.CompareStateEnum.CurrentNull ||
                        local.CompareState == DiffEntry.CompareStateEnum.CurrentSmaller)
                    {
                        WriteInfo($"Artifact '{item.ArtName}' will be uploaded");
                        
                    }
                    else if (local.CompareState == DiffEntry.CompareStateEnum.CurrentGrater ||
                             local.CompareState == DiffEntry.CompareStateEnum.OtherNull)
                    {
                        WriteInfo($"Artifact '{item.ArtName}' will be downloaded");
                        DownloadArtifact(dirInfo, item);
                    }
                }

                if (IsRepeat)
                    Thread.CurrentThread.Join(Interval);
            }

            
            return new Result();
        }


        private IDirectoryInfo OpenDirectory(WorkspaceRDto workspace)
        {
            if (workspace == null)
                throw new ArgumentNullException(nameof(workspace));

            if (string.IsNullOrEmpty(SyncDir))
            {
                // Current dir
                SyncDir = FileSystem.Path.Combine(FileSystem.Directory.GetCurrentDirectory(), FileUtils.SanitizedFileName(FileSystem, workspace.Name));
            }

            if (IsInit)
            {
                if (FileSystem.Directory.Exists(SyncDir))
                {
                    FileSystem.Directory.Delete(SyncDir,true);
                    WriteDebugInfo($"Directory '{SyncDir}' deleted due 'init' mode.");
                }

                IsInit = false;
            }


            if (!FileSystem.Directory.Exists(SyncDir))
            {
                FileSystem.Directory.CreateDirectory(SyncDir);
                WriteInfo($"Sync directory '{SyncDir}' created.");

                var inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_IN);
                FileSystem.Directory.CreateDirectory(inDir);
                WriteDebugInfo($"Directory '{inDir}' created.");
                inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_OUT);
                FileSystem.Directory.CreateDirectory(inDir);
                WriteDebugInfo($"Directory '{inDir}' created.");
            }

            var di = FileSystem.DirectoryInfo.FromDirectoryName(SyncDir);
            WriteCaption($"Sync directory '{SyncDir}' opened.");
            return di;
        }


        private void DownloadArtifact(IDirectoryInfo outputDir, DocumentItem artifactItem)
        {
            if (outputDir == null)
                throw new ArgumentNullException(nameof(outputDir));
            if (artifactItem == null)
                throw new ArgumentNullException(nameof(artifactItem));
            var currentDir = outputDir;
            using (var artFs = Client.GetWorkspaceDocumentContentAsync(ApiKey, ApiKeyPassword,
                artifactItem.DocId.GetValueOrDefault(), artifactItem.ArtId.GetValueOrDefault(),string.Empty).Result)
            {
                long fileSize = 0;
                var fileName = GetArtifactPhysicalFileName(artifactItem);
                var dirName = GetDocumentPhysicalDirName(artifactItem);
                currentDir = currentDir.GetDirectory(dirName) ?? outputDir.CreateSubdirectory(dirName);
                using (var outFs = currentDir.CreateFile(fileName))
                {
                    //CopyStream(artFs, outFs);
                    artFs.CopyTo(outFs);
                    outFs.Flush();
                    fileSize = outFs.Length;
                }
                WriteInfo($"{fileName} downloaded [{artifactItem.ArtLength}/{fileSize} bytes]");
            }

        }
        public static void CopyStream(Stream input, Stream output)
        {
            byte[] buffer = new byte[16 * 1024];
            int read;
            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
            {
                output.Write(buffer, 0, read);
            }
        }
        private string GetArtifactPhysicalFileName(DocumentItem artifactItem)
        {
            //<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file
            return $"{artifactItem.ArtId.GetValueOrDefault():D8}.{artifactItem.ArtType}.{FileUtils.SanitizedFileName(FileSystem,artifactItem.ArtName)}{artifactItem.ArtExt}";
        }

        private string GetDocumentPhysicalDirName(DocumentItem documentItem)
        {
            //[<documentId:8>.<document_name_normalized>]
            return $"{documentItem.DocId.GetValueOrDefault():D8}.{FileUtils.SanitizedFileName(FileSystem, documentItem.DocName)}";
        }

        #endregion

        #region *** Private operations ***

        protected override void OnValidateArguments()
        {
            base.OnValidateArguments();

            SyncDir = GetArgumentValueOrDefault<string>(CS_ARG_NAME_DIR);
            IsRepeat = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_WD);
            IsInit = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_INIT);
            IsDeleteAllowed = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_DELETE);
            Interval = GetArgumentValueOrDefault<TimeSpan>(CS_ARG_NAME_INT);
        }

        protected override IEnumerable<AbstractArgument> OnSetupArguments()
        {
            var args = base.OnSetupArguments().ToList();
            args.Add(new NamedArgument(CS_ARG_NAME_DIR, CS_ARG_DESC_DIR, CS_ARG_HINT_DIR, TypeValuesEnum.String, string.Empty, false));
            args.Add(new FlagArgument(CS_ARG_NAME_WD, CS_ARG_DESC_WD, CS_ARG_HINT_WD,  false));
            args.Add(new NamedArgument(CS_ARG_NAME_INT, CS_ARG_DESC_INT, CS_ARG_HINT_INT, TypeValuesEnum.TimeSpan, TimeSpan.FromSeconds(30).ToString(), false));
            args.Add(new FlagArgument(CS_ARG_NAME_DELETE, CS_ARG_DESC_DELETE, CS_ARG_HINT_DELETE,  false));
            args.Add(new FlagArgument(CS_ARG_NAME_INIT, CS_ARG_DESC_INIT, CS_ARG_HINT_INIT,  false));
            AppendUserArgument(args, CS_ARG_DESC_USER);
            return args;
        }

        #endregion
    }
}
