using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using IdokladSdk.Clients;
using IdokladSdk.Requests.Core;
using IdokladSdk.Requests.Core.Modifiers.Filters.Common;
using IdokladSdk.Requests.Core.Modifiers.Sort.Common;
using IdokladSdk.Response;
namespace Idoklad.Client;
///
/// Translates iDoklad-style filter/sort query strings into the SDK's strongly-typed
/// /
/// modifiers, so the REST
/// list endpoints expose the full server-side filtering the SDK supports (operators
/// eq/neq/gt/gte/lt/lte/ct/nct, AND/OR combining, and multi-column sort) instead of only paging.
///
/// Filter format (identical to the public iDoklad API): one or more
/// (Property~operator~value) conditions joined by , — e.g.
/// (DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false). Parentheses are optional for a single
/// condition. The filtertype query parameter (and | or, default and)
/// decides how multiple conditions combine.
///
/// Operators: eq (=), neq (≠), gt (>), gte (≥),
/// lt (<), lte (≤), ct (contains), nct (not contains). The available
/// operators per field follow the SDK filter type (e.g. dates support the compare operators,
/// text fields support contains).
///
/// Sort format: Property~asc|desc, multiple joined by , — e.g.
/// DateOfIssue~desc,Id~asc. Direction defaults to ascending.
///
public static class ListModifiers
{
private static readonly Regex GroupRegex = new(@"\(([^()]*)\)", RegexOptions.Compiled);
private static readonly Dictionary OperatorMethods = new(StringComparer.OrdinalIgnoreCase)
{
["eq"] = "IsEqual",
["neq"] = "IsNotEqual",
["gt"] = "IsGreaterThan",
["gte"] = "IsGreaterThanOrEqual",
["lt"] = "IsLowerThan",
["lte"] = "IsLowerThanOrEqual",
["ct"] = "Contains",
["nct"] = "NotContains",
};
///
/// Applies paging plus optional / to a list
/// request and returns the unwrapped page. Shared by every list endpoint.
///
public static async Task> GetPageAsync(
this BaseList list,
int page,
int pageSize,
string? filter,
string? filterType,
string? sort,
CancellationToken ct)
where TList : BaseList
where TClient : BaseClient
where TFilter : new()
where TSort : new()
where TGetModel : new()
{
BaseList built = list;
if (!string.IsNullOrWhiteSpace(filter))
{
var useOr = IsOr(filterType);
built = built.Filter(f => BuildFilter(f!, filter, useOr));
}
if (!string.IsNullOrWhiteSpace(sort))
{
built = built.Sort(BuildSort(sort));
}
return (await built.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap();
}
/// True when the filtertype query value requests OR combining (default AND).
public static bool IsOr(string? filterType)
=> string.Equals(filterType, "or", StringComparison.OrdinalIgnoreCase);
///
/// Builds a combined from an iDoklad-style filter string
/// against the SDK filter object (passed in by the SDK at call time).
/// Conditions are combined with OR when is true, otherwise AND.
///
public static FilterExpressionBase BuildFilter(object filter, string filterString, bool useOr)
{
FilterExpressionBase? combined = null;
foreach (var group in ExtractConditions(filterString))
{
var expression = BuildCondition(filter, group);
combined = combined is null
? expression
: (useOr ? combined | expression : combined & expression);
}
if (combined is null)
{
throw new ArgumentException($"Filtr '{filterString}' neobsahuje žádnou platnou podmínku.");
}
return combined;
}
/// Builds the SDK sort selectors from a Field~asc|desc,... string.
public static Func[] BuildSort(string sort)
{
var parts = sort.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var selectors = new List>();
foreach (var part in parts)
{
var segments = part.Split('~', StringSplitOptions.TrimEntries);
var field = segments[0];
var descending = segments.Length > 1 && segments[1].Equals("desc", StringComparison.OrdinalIgnoreCase);
selectors.Add(sortObj =>
{
var property = typeof(TSort).GetProperty(field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)
?? throw new ArgumentException($"Řazení podle pole '{field}' není u této agendy podporováno.");
var item = (SortItem)property.GetValue(sortObj)!;
return descending ? item.Desc() : item.Asc();
});
}
return selectors.ToArray();
}
/// Splits the filter string into individual Name~op~value conditions.
private static IEnumerable ExtractConditions(string filterString)
{
filterString = filterString.Trim();
if (filterString.Contains('('))
{
return GroupRegex.Matches(filterString)
.Select(m => m.Groups[1].Value.Trim())
.Where(s => s.Length > 0);
}
return filterString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
private static FilterExpressionBase BuildCondition(object filter, string condition)
{
var firstTilde = condition.IndexOf('~');
var secondTilde = firstTilde < 0 ? -1 : condition.IndexOf('~', firstTilde + 1);
if (firstTilde < 0 || secondTilde < 0)
{
throw new ArgumentException($"Neplatná filtrační podmínka '{condition}'. Očekává se tvar 'Pole~operátor~hodnota'.");
}
var name = condition[..firstTilde].Trim();
var op = condition[(firstTilde + 1)..secondTilde].Trim();
var rawValue = condition[(secondTilde + 1)..].Trim();
if (!OperatorMethods.TryGetValue(op, out var methodName))
{
throw new ArgumentException($"Neznámý filtrační operátor '{op}'. Povolené: eq, neq, gt, gte, lt, lte, ct, nct.");
}
var property = filter.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)
?? throw new ArgumentException($"Filtrování podle pole '{name}' není u této agendy podporováno.");
var item = property.GetValue(filter)
?? throw new ArgumentException($"Filtrační pole '{name}' není dostupné.");
var method = FindOperatorMethod(item.GetType(), methodName)
?? throw new ArgumentException($"Operátor '{op}' není pro pole '{name}' podporován.");
var targetType = method.GetParameters()[0].ParameterType;
var value = ConvertValue(rawValue, targetType, name);
try
{
return (FilterExpressionBase)method.Invoke(item, new[] { value })!;
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
throw new ArgumentException($"Filtr '{name}~{op}~{rawValue}' se nepodařilo sestavit: {ex.InnerException.Message}");
}
}
private static MethodInfo? FindOperatorMethod(Type itemType, string methodName)
{
var candidates = itemType
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == methodName && m.GetParameters().Length == 1)
.ToList();
if (candidates.Count <= 1)
{
return candidates.FirstOrDefault();
}
// Some filter items (e.g. the Id filter) overload Contains with a scalar and a collection —
// prefer the scalar overload for a single query value.
return candidates.FirstOrDefault(m =>
{
var pt = m.GetParameters()[0].ParameterType;
return pt == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(pt);
}) ?? candidates[0];
}
private static object ConvertValue(string raw, Type target, string fieldName)
{
var type = Nullable.GetUnderlyingType(target) ?? target;
try
{
if (type == typeof(string))
{
return raw;
}
if (type.IsEnum)
{
return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)
? Enum.ToObject(type, numeric)
: Enum.Parse(type, raw, ignoreCase: true);
}
if (type == typeof(DateTime))
{
return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.None);
}
if (type == typeof(bool))
{
return bool.Parse(raw);
}
if (type == typeof(Guid))
{
return Guid.Parse(raw);
}
return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture);
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentException)
{
throw new ArgumentException($"Hodnotu '{raw}' filtru '{fieldName}' nelze převést na typ {type.Name}.");
}
}
}