// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
using System.Text;
using Duplicati.Library.Utility;
using Duplicati.Server;
using Duplicati.WebserverCore.Abstractions;

namespace Duplicati.WebserverCore.Services;

public class CommandlineRunService(IQueueRunnerService queueRunnerService, ILogWriteHandler logWriteHandler) : ICommandlineRunService
{
    private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<CommandlineRunService>();

    private class LogWriter(ActiveRun target, ILogWriteHandler logWriteHandler) : TextWriter
    {
        private readonly StringBuilder sb = new StringBuilder();
        private int m_newlinechars = 0;

        public override Encoding Encoding => Encoding.UTF8;

        public override void Write(char value)
        {
            lock (target.Lock)
            {
                sb.Append(value);
                if (NewLine[m_newlinechars] == value)
                {
                    m_newlinechars++;
                    if (m_newlinechars == NewLine.Length)
                        WriteLine(string.Empty);
                }
                else
                    m_newlinechars = 0;
            }
        }

        public override void WriteLine(string? value)
        {
            value ??= string.Empty;
            lock (target.Lock)
            {
                target.LastAccess = DateTime.Now;

                //Avoid writing the log if it does not exist
                if (target.IsLogDisposed)
                {
                    logWriteHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
                    return;
                }

                try
                {
                    if (sb.Length != 0)
                    {
                        target.Log.Add(sb + value);
                        sb.Length = 0;
                        m_newlinechars = 0;
                    }
                    else
                    {
                        target.Log.Add(value);
                    }
                }
                catch (Exception ex)
                {
                    // This can happen on a very unlucky race where IsLogDisposed is set right after the check
                    logWriteHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
                }
            }
        }
    }

    private class ActiveRun : ICommandlineRunService.IActiveRun
    {
        public string ID { get; } = Guid.NewGuid().ToString();
        public DateTime LastAccess { get; set; } = DateTime.Now;
        public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList();
        public Runner.IRunnerData? Task;
        public LogWriter? Writer;
        public object Lock { get; } = new object();
        public bool Finished { get; set; } = false;
        public bool Started { get; set; } = false;
        public bool IsLogDisposed { get; set; } = false;

        public IEnumerable<string> GetLog() => Log;

        public void Abort()
        {
            var tt = this.Task;
            if (tt != null)
                tt.Abort();
        }
    }

    private readonly Dictionary<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
    private Task? m_cleanupTask;

    public ICommandlineRunService.IActiveRun? GetActiveRun(string id)
    {
        ActiveRun? t;
        lock (m_activeItems)
            m_activeItems.TryGetValue(id, out t);

        if (t != null)
            lock (t.Lock)
                t.LastAccess = DateTime.Now;

        return t;
    }

    public string StartTask(string[] args)
    {
        var k = new ActiveRun();
        k.Writer = new LogWriter(k, logWriteHandler);

        m_activeItems[k.ID] = k;
        StartCleanupTask();

        k.Task = Runner.CreateCustomTask((sink) =>
        {
            try
            {
                k.Started = true;

                // Expand sources for supporting %DOCUMENTS% and similar
                if (args.Length > 2 && args[0].Equals("backup", StringComparison.OrdinalIgnoreCase))
                {
                    var options = args.Skip(1).Where(x => x.StartsWith("--", StringComparison.OrdinalIgnoreCase))
                        .ToArray();

                    var allargs =
                        args.Skip(1)
                        .Where(x => !x.StartsWith("--", StringComparison.OrdinalIgnoreCase));

                    if (allargs.Count() > 1)
                    {
                        var sources = allargs
                            .Skip(1)
                            .Select(SpecialFolders.ExpandEnvironmentVariables)
                            .WhereNotNullOrWhiteSpace()
                            .ToArray();

                        args = [args[0], allargs.First(), .. sources, .. options];
                    }
                }

                var code = CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c =>
                {
                    k.Task!.SetController(c);
                    c.AppendSink(sink);
                }, args);
                k.Writer.WriteLine("Return code: {0}", code);
            }
            catch (Exception ex)
            {
                var rx = ex;
                if (rx is System.Reflection.TargetInvocationException)
                    rx = rx.InnerException;

                if (rx is Library.Interface.UserInformationException)
                    k.Log.Add(rx.Message);
                else
                    k.Log.Add(rx?.ToString() ?? "null exception?");

                throw rx ?? new Exception("null exception?");
            }
            finally
            {
                k.Finished = true;
            }
        });

        queueRunnerService.AddTask(k.Task);
        return k.ID;
    }

    private void StartCleanupTask()
    {
        if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled)
            m_cleanupTask = RunCleanupAsync();
    }

    private async Task RunCleanupAsync()
    {
        while (m_activeItems.Count > 0)
        {
            var oldest = m_activeItems.Values
                .OrderBy(x => x.LastAccess)
                .FirstOrDefault();

            if (oldest != null)
            {
                // If the task has finished, we just wait a little to allow the UI to pick it up
                var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1);
                if (DateTime.Now - oldest.LastAccess > timeout)
                {
                    oldest.IsLogDisposed = true;
                    m_activeItems.Remove(oldest.ID);
                    oldest.Log.Dispose();

                    // Fix all expired, or stop running
                    continue;
                }
            }

            await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
        }
    }
}
