using Meezi.Core.Discover;

namespace Meezi.API.Services;

public record DiscoverFilterParams(
    string? City = null,
    string? Q = null,
    double? MinRating = null,
    string? Sort = null,
    IReadOnlyList<string>? Themes = null,
    IReadOnlyList<string>? Vibes = null,
    IReadOnlyList<string>? Occasions = null,
    IReadOnlyList<string>? SpaceFeatures = null,
    string? NoiseLevel = null,
    string? PriceTier = null,
    string? Size = null,
    bool RequireProfile = false,
    bool OpenNow = false)
{
    public static DiscoverFilterParams FromQuery(
        string? city,
        string? q,
        double? minRating,
        string? sort,
        string? themes,
        string? vibes,
        string? occasions,
        string? spaceFeatures,
        string? noise,
        string? priceTier,
        string? size,
        bool requireProfile,
        bool openNow)
    {
        // Parse explicit CSV filter chips
        var explicitThemes   = SplitCsv(themes);
        var explicitVibes    = SplitCsv(vibes);
        var explicitOcc      = SplitCsv(occasions);
        var explicitSpace    = SplitCsv(spaceFeatures);
        var explicitNoise    = NormalizeToken(noise);
        var explicitPrice    = NormalizeToken(priceTier);
        var explicitSize     = NormalizeToken(size);

        // AI-parse the free-text query and merge hints (explicit chips take priority)
        if (!string.IsNullOrWhiteSpace(q))
        {
            var hints = DiscoverNlpParser.Parse(q);
            explicitThemes   = Merge(explicitThemes, hints.Themes);
            explicitVibes    = Merge(explicitVibes,  hints.Vibes);
            explicitOcc      = Merge(explicitOcc,    hints.Occasions);
            explicitSpace    = Merge(explicitSpace,  hints.SpaceFeatures);
            explicitNoise  ??= hints.NoiseLevel;
            explicitPrice  ??= hints.PriceTier;
            explicitSize   ??= hints.Size;
        }

        return new(
            city,
            q,
            minRating,
            sort,
            explicitThemes,
            explicitVibes,
            explicitOcc,
            explicitSpace,
            explicitNoise,
            explicitPrice,
            explicitSize,
            requireProfile,
            openNow);
    }

    private static IReadOnlyList<string>? SplitCsv(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var parts = value
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(NormalizeToken)
            .Where(p => !string.IsNullOrEmpty(p))
            .Select(p => p!)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();
        return parts.Count == 0 ? null : parts;
    }

    private static IReadOnlyList<string>? Merge(IReadOnlyList<string>? existing, IReadOnlyList<string> hints)
    {
        if (hints.Count == 0) return existing;
        if (existing is null or { Count: 0 }) return hints.Count > 0 ? hints : null;
        var merged = existing.Union(hints, StringComparer.OrdinalIgnoreCase).ToList();
        return merged.Count > 0 ? merged : null;
    }

    private static string? NormalizeToken(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
}

public static class DiscoverProfileMatcher
{
    public static bool HasMeaningfulProfile(CafeDiscoverProfile profile) =>
        profile.Themes.Count > 0
        || profile.Vibes.Count > 0
        || profile.Occasions.Count > 0
        || profile.SpaceFeatures.Count > 0
        || !string.IsNullOrWhiteSpace(profile.Size)
        || !string.IsNullOrWhiteSpace(profile.NoiseLevel)
        || !string.IsNullOrWhiteSpace(profile.PriceTier);

    /// <summary>
    /// Relevance score in [0, 1]. Returns 1.0 when no profile filters are set (text-only search).
    /// Each filter dimension that matches contributes 1 / totalDimensions to the score.
    /// Score ≥ <see cref="MinScoreThreshold"/> passes the soft-filter.
    /// </summary>
    public const double MinScoreThreshold = 0.25;

    public static double Score(CafeDiscoverProfile profile, DiscoverFilterParams filters)
    {
        int total   = 0;
        int matched = 0;

        if (filters.Themes is { Count: > 0 } themes)
        {
            total++;
            if (themes.Any(t => profile.Themes.Contains(t, StringComparer.OrdinalIgnoreCase)))
                matched++;
        }

        if (filters.Vibes is { Count: > 0 } vibes)
        {
            total++;
            if (vibes.Any(v => profile.Vibes.Contains(v, StringComparer.OrdinalIgnoreCase)))
                matched++;
        }

        if (filters.Occasions is { Count: > 0 } occasions)
        {
            total++;
            if (occasions.Any(o => profile.Occasions.Contains(o, StringComparer.OrdinalIgnoreCase)))
                matched++;
        }

        if (filters.SpaceFeatures is { Count: > 0 } space)
        {
            total++;
            if (space.Any(s => profile.SpaceFeatures.Contains(s, StringComparer.OrdinalIgnoreCase)))
                matched++;
        }

        if (!string.IsNullOrEmpty(filters.NoiseLevel))
        {
            total++;
            if (string.Equals(profile.NoiseLevel, filters.NoiseLevel, StringComparison.OrdinalIgnoreCase))
                matched++;
        }

        if (!string.IsNullOrEmpty(filters.PriceTier))
        {
            total++;
            if (string.Equals(profile.PriceTier, filters.PriceTier, StringComparison.OrdinalIgnoreCase))
                matched++;
        }

        if (!string.IsNullOrEmpty(filters.Size))
        {
            total++;
            if (string.Equals(profile.Size, filters.Size, StringComparison.OrdinalIgnoreCase))
                matched++;
        }

        return total == 0 ? 1.0 : (double)matched / total;
    }

    /// <summary>
    /// Legacy hard-AND match still used for explicit chip-only searches (no free text).
    /// Free-text searches use <see cref="Score"/> ≥ threshold instead.
    /// </summary>
    public static bool Matches(CafeDiscoverProfile profile, DiscoverFilterParams filters)
    {
        if (filters.RequireProfile && !HasMeaningfulProfile(profile))
            return false;

        if (filters.Themes is { Count: > 0 } themes
            && !themes.Any(t => profile.Themes.Contains(t, StringComparer.OrdinalIgnoreCase)))
            return false;

        if (filters.Vibes is { Count: > 0 } vibes
            && !vibes.Any(v => profile.Vibes.Contains(v, StringComparer.OrdinalIgnoreCase)))
            return false;

        if (filters.Occasions is { Count: > 0 } occasions
            && !occasions.Any(o => profile.Occasions.Contains(o, StringComparer.OrdinalIgnoreCase)))
            return false;

        if (filters.SpaceFeatures is { Count: > 0 } space
            && !space.Any(s => profile.SpaceFeatures.Contains(s, StringComparer.OrdinalIgnoreCase)))
            return false;

        if (!string.IsNullOrEmpty(filters.NoiseLevel)
            && !string.Equals(profile.NoiseLevel, filters.NoiseLevel, StringComparison.OrdinalIgnoreCase))
            return false;

        if (!string.IsNullOrEmpty(filters.PriceTier)
            && !string.Equals(profile.PriceTier, filters.PriceTier, StringComparison.OrdinalIgnoreCase))
            return false;

        if (!string.IsNullOrEmpty(filters.Size)
            && !string.Equals(profile.Size, filters.Size, StringComparison.OrdinalIgnoreCase))
            return false;

        return true;
    }
}
