using System.Globalization;

using System.Runtime.Versioning;

using System.Text.RegularExpressions;

using System.Xml.Serialization;



using ColumnizerLib;



using LogExpert.Core.Helpers;



using Newtonsoft.Json;



[assembly: SupportedOSPlatform("windows")]

namespace RegexColumnizer;



/// <summary>

/// Provides a base class for columnizing log lines using regular expressions, supporting configurable column

/// definitions and integration with log line memory interfaces.

/// </summary>

/// <remarks>This abstract class implements core logic for splitting log lines into columns based on regular

/// expression group matches. It supports configuration loading and saving, and is intended to be extended by concrete

/// columnizer implementations. The class ensures that columnized output always matches the expected column count, and

/// provides mechanisms for both memory-efficient and string-based log line processing. Thread safety is not guaranteed;

/// instances should not be shared across threads without external synchronization.</remarks>

public abstract class BaseRegexColumnizer : ILogLineMemoryColumnizer, IColumnizerConfiguratorMemory

{

    #region Fields



    private readonly XmlSerializer _xml = new(typeof(RegexColumnizerConfig));

    private string[] _columns;

    private RegexColumnizerConfig _config;



    // Seam for error notification.

    // Defaults to a modal dialog, in tests replaced so they can assert without blocking on a headless runner.

    internal Action<string, string, MessageBoxIcon> ShowError { get; set; } = static (message, title, icon) => MessageBox.Show(message, title, MessageBoxButtons.OK, icon);

    #endregion



    #region Properties



    public Regex Regex { get; private set; }



    #endregion



    #region Public methods



    /// <summary>

    /// Gets the configured name, or a default name if no configuration is set.

    /// </summary>

    /// <returns>A string containing the configured name if available; otherwise, a default name.</returns>

    public string GetName ()

    {

        return string.IsNullOrWhiteSpace(_config?.Name)

            ? GetNameInternal()

            : _config.Name;

    }



    /// <summary>

    /// Gets the custom name if specified; otherwise, returns the default name.

    /// </summary>

    /// <returns>A string containing the custom name if it is set and not empty; otherwise, the default name.</returns>

    public string GetCustomName ()

    {

        return string.IsNullOrWhiteSpace(_config?.CustomName)

            ? GetNameInternal()

            : _config.CustomName;

    }



    /// <summary>

    /// Gets a localized description of the regular expression columnizer.

    /// </summary>

    /// <returns>A string containing the localized description text for the regular expression columnizer.</returns>

    public string GetDescription () => Resources.RegexColumnizer_Description;



    /// <summary>

    /// Gets the number of columns in the collection.

    /// </summary>

    /// <returns>The total number of columns contained in the collection.</returns>

    public int GetColumnCount () => _columns.Length;



    /// <summary>

    /// Returns the names of all columns in the current schema.

    /// </summary>

    /// <returns>An array of strings containing the names of the columns. The array will be empty if no columns are defined.</returns>

    public string[] GetColumnNames () => _columns;



    /// <summary>

    /// Returns a slice of the specified memory corresponding to the matched group, or an empty memory if the group was

    /// not successful or has zero length.

    /// </summary>

    /// <remarks>This method avoids allocating a new string by returning a memory slice over the original

    /// input. The returned memory is valid as long as the underlying memory of <paramref name="lineMemory"/> remains

    /// valid.</remarks>

    /// <param name="lineMemory">The memory region containing the original input text from which the group was matched.</param>

    /// <param name="group">The regular expression group whose matched value is to be extracted from the memory. The group must have been

    /// matched against the input represented by <paramref name="lineMemory"/>.</param>

    /// <returns>A <see cref="ReadOnlyMemory{Char}"/> representing the portion of <paramref name="lineMemory"/> matched by

    /// <paramref name="group"/>. Returns <see cref="ReadOnlyMemory{Char}.Empty"/> if the group was not successful or

    /// has zero length.</returns>

    private static ReadOnlyMemory<char> GetGroupMemory (ReadOnlyMemory<char> lineMemory, Group group)

    {

        if (!group.Success || group.Length == 0)

        {

            return ReadOnlyMemory<char>.Empty;

        }



        // Use group's Index and Length to slice original memory

        // This avoids allocating a new string for the group value

        return lineMemory.Slice(group.Index, group.Length);

    }



    /// <summary>

    /// Splits the specified log line into columns according to the configured columnization logic.

    /// </summary>

    /// <remarks>If the log line does not match the configured regular expression, the entire line is placed

    /// in the last column and other columns are set to empty. The method ensures that the returned object always

    /// contains the expected number of columns, avoiding null values in the column array.</remarks>

    /// <param name="callback">A callback interface used to provide additional context or services during columnization. Cannot be null.</param>

    /// <param name="logLine">The log line to be split into columns. Cannot be null.</param>

    /// <returns>An object representing the columnized version of the input log line, with each column containing its

    /// corresponding value.</returns>

    public IColumnizedLogLineMemory SplitLine (ILogLineMemoryColumnizerCallback callback, ILogLineMemory logLine)

    {

        ArgumentNullException.ThrowIfNull(logLine, nameof(logLine));

        ArgumentNullException.ThrowIfNull(callback, nameof(callback));



        var columnizedLogLine = new ColumnizedLogLine

        {

            LogLine = logLine

        };



        if (Regex != null)

        {

            if (Regex.IsMatch(logLine.FullLine.Span))

            {

                // To extract regex group captures, we must convert to string.

                // This is an unavoidable allocation - .NET Regex doesn't provide

                // a way to get group capture positions from ReadOnlySpan<char>.

                // However, GetGroupMemory() will slice the original ReadOnlyMemory,

                // so we avoid allocating strings for each captured group.

                var lineString = logLine.FullLine.ToString();

                var match = Regex.Match(lineString);



                var groupCount = Math.Min(match.Groups.Count - 1, _columns.Length);



                var cols = Column.CreateColumns(_columns.Length, columnizedLogLine);



                for (var i = 0; i < groupCount; i++)

                {

                    cols[i].FullValue = GetGroupMemory(logLine.FullLine, match.Groups[i + 1]);

                }



                columnizedLogLine.ColumnValues = [.. cols.Select(c => c as IColumnMemory)];

            }

            else

            {

                //Fill other columns with empty string to avoid null pointer exceptions in unexpected places

                var columns = Column.CreateColumns(_columns.Length, columnizedLogLine);



                // Set last column to full line

                columns[^1].FullValue = logLine.FullLine;



                //Move non matching lines in the last column

                columnizedLogLine.ColumnValues = [.. columns.Select(c => c as IColumnMemory)];

            }

        }

        else //Regex is null, just put the full line in the first column

        {

            var cols = Column.CreateColumns(_columns.Length, columnizedLogLine);

            cols[0].FullValue = logLine.FullLine;



            columnizedLogLine.ColumnValues = [.. cols.Select(c => c as IColumnMemory)];

        }



        return columnizedLogLine;

    }



    /// <summary>

    /// Determines whether timeshift functionality is implemented.

    /// </summary>

    /// <returns><see langword="true"/> if timeshift is implemented; otherwise, <see langword="false"/>.</returns>

    public bool IsTimeshiftImplemented () => false;



    /// <summary>

    /// Sets the time offset, in milliseconds, to be applied to time calculations or operations.

    /// </summary>

    /// <param name="msecOffset">The time offset, in milliseconds. Positive values indicate a forward offset; negative values indicate a backward

    /// offset.</param>

    /// <exception cref="NotImplementedException">The method is not implemented.</exception>

    public void SetTimeOffset (int msecOffset)

    {

        throw new NotImplementedException();

    }



    /// <summary>

    /// Gets the time offset, in seconds, between the local system time and Coordinated Universal Time (UTC).

    /// </summary>

    /// <returns>The number of seconds that the local time is offset from UTC. A positive value indicates the local time is ahead

    /// of UTC; a negative value indicates it is behind.</returns>

    /// <exception cref="NotImplementedException">Thrown in all cases. This method is not implemented.</exception>

    public int GetTimeOffset ()

    {

        throw new NotImplementedException();

    }



    /// <summary>

    /// Loads the configuration for the columnizer from the specified directory, using either a JSON or XML

    /// configuration file if available.

    /// </summary>

    /// <remarks>If both JSON and XML configuration files are missing or invalid, a default configuration is

    /// created. The method displays an error message if deserialization fails.</remarks>

    /// <param name="configDir">The path to the directory containing the configuration files. Must not be null or empty.</param>

    public void LoadConfig (string configDir)

    {

        var configFile = GetConfigFileJSON(configDir);



        if (!File.Exists(configFile))

        {

            configFile = GetConfigFileXML(configDir);



            if (!File.Exists(configFile))

            {

                _config = new RegexColumnizerConfig

                {

                    Name = GetName()

                };

            }

            else

            {

                try

                {

                    using var reader = new StreamReader(configFile);

                    _config = _xml.Deserialize(reader) as RegexColumnizerConfig;

                }

                catch (Exception ex) when (ex is InvalidOperationException or

                                                 IOException or

                                                 ArgumentException or

                                                 ArgumentNullException or

                                                 FileNotFoundException or

                                                 DirectoryNotFoundException)

                {

                    ShowError(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize, MessageBoxIcon.Error);

                    _config = new RegexColumnizerConfig

                    {

                        Name = GetName()

                    };

                }

            }

        }

        else

        {

            try

            {

                string jsonContent = File.ReadAllText(configFile);



                _config = JsonConvert.DeserializeObject<RegexColumnizerConfig>(jsonContent) ?? new RegexColumnizerConfig { Name = GetName() };



            }

            catch (JsonException ex)

            {

                ShowError(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize, MessageBoxIcon.Error);

                _config = new RegexColumnizerConfig

                {

                    Name = GetName()

                };

            }

        }



        Init();

    }



    /// <summary>

    /// Validates that the columnizer name contains no path separators or invalid characters

    /// to prevent path traversal attacks (SEC-02)

    /// </summary>

    private static void ValidateColumnizerName (string name)

    {

        if (string.IsNullOrWhiteSpace(name))

        {

            throw new InvalidOperationException(Resources.RegexColumnizer_Error_Message_ColumnizerNameCannotBeNullOrEmpty);

        }



        // Check for path separators (both Windows and Unix)

        if (name.Contains(Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||

            name.Contains(Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||

            name.Contains('/', StringComparison.OrdinalIgnoreCase) ||

            name.Contains('\\', StringComparison.OrdinalIgnoreCase))

        {

            throw new InvalidOperationException(Resources.RegexColumnizer_Error_Message_ColumnizerNameNameContainsPathSeparatorsWhichAreNotAllowed);

        }



        // Check for invalid filename characters

        char[] invalidChars = Path.GetInvalidFileNameChars();

        if (name.IndexOfAny(invalidChars) >= 0)

        {

            throw new InvalidOperationException(Resources.RegexColumnizer_Error_Message_ColumnizerNameNameContainsInvalidFilenameCharacters);

        }



        // Check for path traversal patterns

        if (name.Contains("..", StringComparison.OrdinalIgnoreCase) || name.Contains('~', StringComparison.OrdinalIgnoreCase))

        {

            throw new InvalidOperationException(Resources.RegexColumnizer_Error_Message_ColumnizerNameNameContainsPathTraversalPatternsWhichAreNotAllowed);

        }

    }



    [Obsolete("XML Configuration is deprecated, use JSON instead")]

    private string GetConfigFileXML (string configDir)

    {

        var name = GetType().Name;

        ValidateColumnizerName(name);



        var configPath = Path.Join(configDir, name);

        configPath = Path.ChangeExtension(configPath, "xml");

        return configPath;

    }



    private string GetConfigFileJSON (string configDir)

    {

        var name = GetType().Name;

        ValidateColumnizerName(name);



        var configPath = Path.Join(configDir, name);

        configPath = Path.ChangeExtension(configPath, "json");

        return configPath;

    }



    #endregion



    /// <summary>

    /// ToString, this is displayed in the columnizer picker combobox only in the FilterSelectionDialog

    /// </summary>

    /// <returns></returns>

    public override string ToString ()

    {

        return GetName();

    }



    #region Private Methods



    protected abstract string GetNameInternal ();



    public void Init ()

    {

        try

        {

            Regex = RegexHelper.GetOrCreateCached(_config.Expression, RegexOptions.Compiled);

            var skip = Regex.GetGroupNames().Length == 1

                ? 0

                : 1;



            _columns = [.. Regex.GetGroupNames().Skip(skip)];

        }

        catch (Exception ex) when (ex is ArgumentException or

                                         ArgumentNullException or

                                         OverflowException or

                                         RegexParseException)

        {

            Regex = null;

            _columns = ["text"];

        }

    }



    /// <summary>

    /// Retrieves the timestamp associated with the specified log line.

    /// </summary>

    /// <param name="callback">An object that provides callback methods for columnizer operations. Used to access additional context or

    /// services required during timestamp extraction.</param>

    /// <param name="logLine">The log line from which to extract the timestamp. Cannot be null.</param>

    /// <returns>A DateTime value representing the timestamp of the specified log line.</returns>

    /// <exception cref="NotImplementedException">The method is not implemented.</exception>

    public DateTime GetTimestamp (ILogLineMemoryColumnizerCallback callback, ILogLineMemory logLine)

    {

        throw new NotImplementedException();

    }



    /// <summary>

    /// Notifies the callback of a value change for a specific column.

    /// </summary>

    /// <param name="callback">The callback interface to receive the value change notification. Cannot be null.</param>

    /// <param name="column">The zero-based index of the column whose value has changed.</param>

    /// <param name="value">The new value to be associated with the specified column.</param>

    /// <param name="oldValue">The previous value of the specified column before the change.</param>

    /// <exception cref="NotImplementedException">The method is not implemented.</exception>

    public void PushValue (ILogLineMemoryColumnizerCallback callback, int column, string value, string oldValue)

    {

        throw new NotImplementedException();

    }



    public void PushValue (ILogLineMemoryColumnizerCallback callback, int column, string value, ReadOnlyMemory<char> oldValue)

    {

        throw new NotImplementedException();

    }



    /// <summary>

    /// Displays a configuration dialog for the columnizer and saves the updated settings to the specified configuration

    /// directory.

    /// </summary>

    /// <remarks>If the specified configuration directory does not exist, the method attempts to create it. If

    /// the user cancels the configuration dialog, no changes are made. Any errors encountered during directory creation

    /// or file saving are displayed to the user via a message box. The callback parameter is not used in the current

    /// implementation.</remarks>

    /// <param name="callback">A callback interface for columnizer memory operations. This parameter is reserved for future use and can be

    /// null.</param>

    /// <param name="configDir">The path to the directory where the configuration file will be stored. Cannot be null, empty, or consist only of

    /// white-space characters.</param>

    /// <exception cref="ArgumentException">Thrown if configDir is null, empty, or consists only of white-space characters.</exception>

    /// <exception cref="InvalidOperationException">Thrown if the columnizer name is null or empty.</exception>

    public void Configure (ILogLineMemoryColumnizerCallback callback, string configDir)

    {

        // Validate inputs

        if (string.IsNullOrWhiteSpace(configDir))

        {

            throw new ArgumentException(Resources.RegexColumnizer_Configuration_DirectoryCannotBeNullOrEmpty, nameof(configDir));

        }



        string name = GetName();



        if (string.IsNullOrWhiteSpace(name))

        {

            throw new InvalidOperationException(Resources.RegexColumnizer_Error_Message_ColumnizerNameCannotBeNullOrEmpty);

        }



        // Ensure directory exists

        if (!Directory.Exists(configDir))

        {

            try

            {

                _ = Directory.CreateDirectory(configDir);

            }

            catch (Exception ex) when (ex is IOException or

                                             UnauthorizedAccessException)

            {

                ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToCreateConfigurationDirectory, ex.Message),

                          Resources.RegexColumnizer_UI_Title_Error,

                          MessageBoxIcon.Error);

                return;

            }

        }



        string filePath = Path.Join(configDir, $"{name}Columnizer.json");



        _config ??= new RegexColumnizerConfig

        {

            Name = GetName()

        };



        RegexColumnizerConfigDialog dlg = new(_config);

        if (dlg.ShowDialog() == DialogResult.OK)

        {

            try

            {

                // Only validate regex if expression is provided (empty is allowed and uses default)

                if (!string.IsNullOrWhiteSpace(dlg.Config.Expression))

                {

                    // Test regex compilation to catch errors early

                    _ = RegexHelper.CreateSafeRegex(dlg.Config.Expression);

                }



                // Save configuration

                string json = JsonConvert.SerializeObject(dlg.Config, Formatting.Indented);

                File.WriteAllText(filePath, json);



                _config = dlg.Config;

                Init();

            }

            catch (RegexMatchTimeoutException ex)

            {

                ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_RegexTimeout, ex.Message), Resources.RegexColumnizer_UI_Title_Warning, MessageBoxIcon.Error);

            }

            catch (ArgumentException ex)

            {

                ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_InvalidRegexPattern, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxIcon.Error);

            }

            catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)

            {

                ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToSaveConfiguration, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxIcon.Error);

            }

        }

    }



    #endregion

}



#region RegexColumnizer Implementations



public class Regex1Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex1";

}



public class Regex2Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex2";

}



public class Regex3Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex3";

}



public class Regex4Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex4";

}



public class Regex5Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex5";

}



public class Regex6Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex6";

}



public class Regex7Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex7";

}



public class Regex8Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex8";

}



public class Regex9Columnizer : BaseRegexColumnizer

{

    protected override string GetNameInternal () => "Regex9";

}



#endregion