using System.Collections.ObjectModel;
using System.Reflection;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
using OF_DL.Models.Config;

namespace OF_DL.Gui.ViewModels;

public partial class ConfigFieldViewModel : ViewModelBase
{
    private const string NoListSelectedValue = "";
    private const string NoListSelectedDisplayName = "(No list selected)";

    private static readonly Regex s_fileNameVariableRegex = new(@"\{([^{}]+)\}", RegexOptions.Compiled);

    private static readonly Dictionary<string, string[]> s_fileNameVariablesByConfigOption =
        new(StringComparer.Ordinal)
        {
            [nameof(Config.PaidPostFileNameFormat)] =
            [
                "id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
            ],
            [nameof(Config.PostFileNameFormat)] =
            [
                "id", "postedAt", "mediaId", "mediaCreatedAt", "filename", "username", "text", "rawText"
            ],
            [nameof(Config.PaidMessageFileNameFormat)] =
            [
                "id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
            ],
            [nameof(Config.MessageFileNameFormat)] =
            [
                "id", "createdAt", "mediaId", "mediaCreatedAt", "filename", "username", "text"
            ]
        };

    private static readonly Dictionary<string, string> s_enumDisplayNames =
        new(StringComparer.Ordinal)
        {
            ["_240"] = "240p",
            ["_720"] = "720p",
            ["source"] = "Source Resolution",
            ["light"] = "Light",
            ["dark"] = "Dark"
        };

    private static readonly Dictionary<string, string> s_displayNameOverridesByProperty =
        new(StringComparer.Ordinal)
        {
            [nameof(Config.PostFileNameFormat)] = "Free Post File Name Format",
            [nameof(Config.MessageFileNameFormat)] = "Free Message File Name Format",
            [nameof(Config.DownloadVideoResolution)] = "Video Resolution",
            [nameof(Config.IgnoredUsersListName)] = "Ignored Users List",
            [nameof(Config.RenameExistingFilesWhenCustomFormatIsSelected)] =
                "Rename Existing Files with Custom Formats",
            [nameof(Config.DownloadPath)] = "Download Folder"
        };

    public ConfigFieldViewModel(
        PropertyInfo propertyInfo,
        object? initialValue,
        IEnumerable<string>? ignoredUsersListNames = null,
        string? helpText = null)
    {
        PropertyInfo = propertyInfo;
        PropertyName = propertyInfo.Name;
        DisplayName = GetDisplayName(propertyInfo.Name);
        PropertyType = propertyInfo.PropertyType;
        HelpText = helpText?.Trim() ?? string.Empty;

        IsBoolean = PropertyType == typeof(bool);
        IsEnum = PropertyType.IsEnum;
        IsDate = PropertyType == typeof(DateTime?);
        IsNumeric = PropertyType == typeof(int) || PropertyType == typeof(int?);
        IsMultiline = PropertyType == typeof(Dictionary<string, CreatorConfig>);
        IsTextInput = !IsBoolean && !IsEnum && !IsDate && !IsNumeric;

        if (IsEnum)
        {
            foreach (string enumName in Enum.GetNames(PropertyType))
            {
                string displayName = s_enumDisplayNames.TryGetValue(enumName, out string? mappedName)
                    ? mappedName
                    : enumName;
                EnumOptions.Add(displayName);
            }
        }

        if (IsFileNameFormatField)
        {
            foreach (string variableName in GetAllowedFileNameVariables())
            {
                AvailableFileNameVariables.Add(variableName);
            }

            SelectedFileNameVariable = AvailableFileNameVariables.FirstOrDefault();
            string availableVariables =
                string.Join(", ", AvailableFileNameVariables.Select(variable => $"{{{variable}}}"));
            string fileNameHelpText =
                $"Available variables: {availableVariables}. Include {{mediaId}} or {{filename}} to avoid collisions.";
            HelpText = string.IsNullOrWhiteSpace(HelpText)
                ? fileNameHelpText
                : $"{HelpText} {fileNameHelpText}";
        }

        LoadInitialValue(initialValue);

        if (IsIgnoredUsersListField)
        {
            SetIgnoredUsersListOptions(ignoredUsersListNames ?? []);
        }

        if (IsFileNameFormatField)
        {
            UpdateFileNameFormatPreview();
        }
    }

    public PropertyInfo PropertyInfo { get; }

    public string PropertyName { get; }

    public string DisplayName { get; }

    public Type PropertyType { get; }

    public bool IsBoolean { get; }

    public bool IsEnum { get; }

    public bool IsDate { get; }

    public bool IsNumeric { get; }

    public bool IsNumericAndNotTimeout => IsNumeric && !IsTimeoutField;

    public bool IsTextInput { get; }

    public bool IsMultiline { get; }

    public bool IsFileNameFormatField => s_fileNameVariablesByConfigOption.ContainsKey(PropertyName);

    public bool IsCreatorConfigsField =>
        string.Equals(PropertyName, nameof(Config.CreatorConfigs), StringComparison.Ordinal);

    public bool IsIgnoredUsersListField =>
        string.Equals(PropertyName, nameof(Config.IgnoredUsersListName), StringComparison.Ordinal);

    public bool IsTimeoutField =>
        string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal);

    public bool IsRegularTextInput =>
        IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField;

    public bool HasHelpText => !string.IsNullOrWhiteSpace(HelpText);

    public double TextBoxMinHeight => IsMultiline ? 150 : 36;

    public ObservableCollection<string> EnumOptions { get; } = [];

    public ObservableCollection<string> AvailableFileNameVariables { get; } = [];

    public ObservableCollection<ConfigSelectOptionViewModel> IgnoredUsersListOptions { get; } = [];

    public ObservableCollection<FileNameFormatSegmentViewModel> FileNameFormatSegments { get; } = [];

    [ObservableProperty] private bool _boolValue;

    [ObservableProperty] private string? _enumValue;

    [ObservableProperty] private DateTimeOffset? _dateValue;

    [ObservableProperty] private decimal? _numericValue;

    private string _actualTextValue = string.Empty;

    [ObservableProperty] private string _textValue = string.Empty;

    private bool _isNormalizingFileNameFormatInput;

    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))]
    private string? _selectedFileNameVariable;

    private bool IsPathField =>
        string.Equals(PropertyName, nameof(Config.FFmpegPath), StringComparison.Ordinal) ||
        string.Equals(PropertyName, nameof(Config.DownloadPath), StringComparison.Ordinal);

    [ObservableProperty] private ConfigSelectOptionViewModel? _selectedIgnoredUsersListOption;

    [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasHelpText))]
    private string _helpText = string.Empty;

    [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasUnknownFileNameVariables))]
    private string _unknownFileNameVariablesMessage = string.Empty;

    [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
    private string _errorMessage = string.Empty;

    public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);

    public bool HasUnknownFileNameVariables => !string.IsNullOrWhiteSpace(UnknownFileNameVariablesMessage);

    public bool TryGetTypedValue(out object? value, out string? error)
    {
        value = null;
        error = null;

        if (IsBoolean)
        {
            value = BoolValue;
            return true;
        }

        if (IsEnum)
        {
            if (string.IsNullOrWhiteSpace(EnumValue))
            {
                error = $"{DisplayName} is required.";
                return false;
            }

            string actualEnumValue = s_enumDisplayNames
                .FirstOrDefault(kvp => string.Equals(kvp.Value, EnumValue, StringComparison.Ordinal))
                .Key ?? EnumValue;

            if (!Enum.TryParse(PropertyType, actualEnumValue, true, out object? enumResult))
            {
                error = $"{DisplayName} must be one of: {string.Join(", ", EnumOptions)}.";
                return false;
            }

            value = enumResult;
            return true;
        }

        if (PropertyType == typeof(string))
        {
            // Use actual value for path fields when privacy mode is enabled
            string textToUse = Program.HidePrivateInfo && IsPathField ? _actualTextValue : TextValue;
            value = textToUse.Trim();
            return true;
        }

        if (PropertyType == typeof(int))
        {
            if (!NumericValue.HasValue)
            {
                error = $"{DisplayName} must be a whole number.";
                return false;
            }

            if (decimal.Truncate(NumericValue.Value) != NumericValue.Value)
            {
                error = $"{DisplayName} must be a whole number.";
                return false;
            }

            value = (int)NumericValue.Value;
            return true;
        }

        if (PropertyType == typeof(int?))
        {
            if (!NumericValue.HasValue)
            {
                value = null;
                return true;
            }

            if (decimal.Truncate(NumericValue.Value) != NumericValue.Value)
            {
                error = $"{DisplayName} must be a whole number.";
                return false;
            }

            value = (int)NumericValue.Value;
            return true;
        }

        if (PropertyType == typeof(DateTime?))
        {
            if (!DateValue.HasValue)
            {
                value = null;
                return true;
            }

            value = DateValue.Value.DateTime;
            return true;
        }

        if (PropertyType == typeof(Dictionary<string, CreatorConfig>))
        {
            if (string.IsNullOrWhiteSpace(TextValue))
            {
                value = new Dictionary<string, CreatorConfig>();
                return true;
            }

            try
            {
                Dictionary<string, CreatorConfig>? parsed =
                    JsonConvert.DeserializeObject<Dictionary<string, CreatorConfig>>(TextValue);
                value = parsed ?? new Dictionary<string, CreatorConfig>();
                return true;
            }
            catch (JsonException)
            {
                error = $"{DisplayName} must be valid JSON.";
                return false;
            }
        }

        error = $"{DisplayName} has an unsupported field type.";
        return false;
    }

    public void ClearError() => ErrorMessage = string.Empty;

    public void SetError(string message) => ErrorMessage = message;

    public void SetIgnoredUsersListOptions(IEnumerable<string> listNames)
    {
        if (!IsIgnoredUsersListField)
        {
            return;
        }

        string selectedValue = SelectedIgnoredUsersListOption?.Value ?? TextValue.Trim();
        ConfigSelectOptionViewModel noSelectionOption =
            new(NoListSelectedValue, NoListSelectedDisplayName);

        IgnoredUsersListOptions.Clear();
        IgnoredUsersListOptions.Add(noSelectionOption);

        IEnumerable<string> distinctNames = listNames
            .Where(listName => !string.IsNullOrWhiteSpace(listName))
            .Distinct(StringComparer.Ordinal)
            .OrderBy(listName => listName, StringComparer.OrdinalIgnoreCase);

        foreach (string listName in distinctNames)
        {
            IgnoredUsersListOptions.Add(new ConfigSelectOptionViewModel(listName, listName));
        }

        ConfigSelectOptionViewModel selectedOption =
            IgnoredUsersListOptions.FirstOrDefault(option =>
                string.Equals(option.Value, selectedValue, StringComparison.Ordinal)) ?? noSelectionOption;

        SelectedIgnoredUsersListOption = selectedOption;
        TextValue = selectedOption.Value;
    }

    [RelayCommand(CanExecute = nameof(CanInsertSelectedFileNameVariable))]
    private void InsertSelectedFileNameVariable()
    {
        if (string.IsNullOrWhiteSpace(SelectedFileNameVariable))
        {
            return;
        }

        string placeholder = $"{{{SelectedFileNameVariable}}}";
        TextValue += placeholder;
    }

    partial void OnTextValueChanged(string value)
    {
        if (IsFileNameFormatField && !_isNormalizingFileNameFormatInput)
        {
            string trimmedValue = value.Trim();
            if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
            {
                _isNormalizingFileNameFormatInput = true;
                TextValue = trimmedValue;
                _isNormalizingFileNameFormatInput = false;
                return;
            }
        }

        // Store actual value if not the privacy placeholder
        if (value != "[Hidden for Privacy]")
        {
            _actualTextValue = value;
        }

        if (!IsFileNameFormatField)
        {
            return;
        }

        UpdateFileNameFormatPreview();
    }

    private bool CanInsertSelectedFileNameVariable() =>
        IsFileNameFormatField && !string.IsNullOrWhiteSpace(SelectedFileNameVariable);

    partial void OnSelectedIgnoredUsersListOptionChanged(ConfigSelectOptionViewModel? value)
    {
        if (!IsIgnoredUsersListField)
        {
            return;
        }

        TextValue = value?.Value ?? NoListSelectedValue;
    }

    private void LoadInitialValue(object? initialValue)
    {
        if (IsBoolean)
        {
            BoolValue = initialValue is bool boolValue && boolValue;
            return;
        }

        if (IsEnum)
        {
            string? enumName = initialValue?.ToString();
            if (!string.IsNullOrEmpty(enumName))
            {
                string displayName = s_enumDisplayNames.TryGetValue(enumName, out string? mappedName)
                    ? mappedName
                    : enumName;
                EnumValue = displayName;
            }
            else
            {
                EnumValue = EnumOptions.FirstOrDefault();
            }

            return;
        }

        if (PropertyType == typeof(Dictionary<string, CreatorConfig>))
        {
            Dictionary<string, CreatorConfig> creatorConfigs =
                initialValue as Dictionary<string, CreatorConfig> ?? new Dictionary<string, CreatorConfig>();
            TextValue = JsonConvert.SerializeObject(creatorConfigs, Formatting.Indented);
            return;
        }

        if (PropertyType == typeof(DateTime?))
        {
            DateTime? date = initialValue is DateTime dt ? dt : null;
            DateValue = date.HasValue ? new DateTimeOffset(date.Value) : null;
            return;
        }

        if (PropertyType == typeof(int))
        {
            NumericValue = initialValue is int intValue ? intValue : 0;
            return;
        }

        if (PropertyType == typeof(int?))
        {
            NumericValue = initialValue is int nullableIntValue ? nullableIntValue : null;
            return;
        }

        string initialText = initialValue?.ToString() ?? string.Empty;
        _actualTextValue = initialText;

        // Show privacy placeholder for path fields if flag is set
        if (Program.HidePrivateInfo && IsPathField && !string.IsNullOrWhiteSpace(initialText))
        {
            TextValue = "[Hidden for Privacy]";
        }
        else
        {
            TextValue = initialText;
        }
    }

    private IEnumerable<string> GetAllowedFileNameVariables() =>
        s_fileNameVariablesByConfigOption.TryGetValue(PropertyName, out string[]? variables)
            ? variables
            : [];

    private void UpdateFileNameFormatPreview()
    {
        FileNameFormatSegments.Clear();
        UnknownFileNameVariablesMessage = string.Empty;

        if (string.IsNullOrEmpty(TextValue))
        {
            return;
        }

        HashSet<string> allowedVariables = new(GetAllowedFileNameVariables(), StringComparer.OrdinalIgnoreCase);
        HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase);
        (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors();

        MatchCollection matches = s_fileNameVariableRegex.Matches(TextValue);
        int currentIndex = 0;
        foreach (Match match in matches)
        {
            if (match.Index > currentIndex)
            {
                string plainText = TextValue[currentIndex..match.Index];
                FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor));
            }

            string variableName = match.Groups[1].Value;
            bool isAllowedVariable = allowedVariables.Contains(variableName);
            FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(match.Value,
                isAllowedVariable ? AllowedVariableColor : InvalidVariableColor));

            if (!isAllowedVariable)
            {
                unknownVariables.Add(variableName);
            }

            currentIndex = match.Index + match.Length;
        }

        if (currentIndex < TextValue.Length)
        {
            string trailingText = TextValue[currentIndex..];
            FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor));
        }

        if (unknownVariables.Count > 0)
        {
            string tokens = string.Join(", ", unknownVariables.Select(variable => $"{{{variable}}}"));
            UnknownFileNameVariablesMessage = $"Unknown variable(s): {tokens}";
        }
    }

    private static (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor)
        GetFileNamePreviewColors()
    {
        bool isDarkTheme = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark;
        return isDarkTheme
            ? ("#DCE6F7", "#66A6FF", "#FF8C8C")
            : ("#1F2A44", "#2E6EEA", "#D84E4E");
    }

    private static string ToDisplayName(string propertyName)
    {
        if (string.IsNullOrWhiteSpace(propertyName))
        {
            return propertyName;
        }

        return string.Concat(propertyName.Select((character, index) =>
            index > 0 && char.IsUpper(character) && !char.IsUpper(propertyName[index - 1])
                ? $" {character}"
                : character.ToString()));
    }

    private static string GetDisplayName(string propertyName) =>
        s_displayNameOverridesByProperty.TryGetValue(propertyName, out string? displayName)
            ? displayName
            : ToDisplayName(propertyName);
}
