using Microsoft.AspNetCore.Hosting;
using PlotLine.Models;
using System.Globalization;
using System.Text;

namespace PlotLine.Services;

public interface IHelpService
{
    IReadOnlyList<HelpArticle> GetAllArticles();
    HelpArticle? GetByContextKey(string contextKey);
    HelpArticle? GetByRelativePath(string relativePath);
    IReadOnlyList<HelpArticle> GetByCategory(string category);
    IReadOnlyList<HelpArticle> GetByCategorySlug(string categorySlug);
    string? GetCategoryBySlug(string categorySlug);
    IReadOnlyList<string> GetCategories();
    HelpArticle? GetBySlugs(string categorySlug, string articleSlug);
    IReadOnlyList<HelpArticle> Search(string query);
    IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5);
    IReadOnlyList<HelpCoverageItem> GetCoverage();
}

public sealed class HelpService(IWebHostEnvironment environment) : IHelpService
{
    private readonly object cacheLock = new();
    private IReadOnlyList<HelpArticle>? cachedArticles;

    public IReadOnlyList<HelpArticle> GetAllArticles()
    {
        if (cachedArticles is not null)
        {
            return cachedArticles;
        }

        lock (cacheLock)
        {
            cachedArticles ??= LoadArticles();
            return cachedArticles;
        }
    }

    public HelpArticle? GetByContextKey(string contextKey)
    {
        if (string.IsNullOrWhiteSpace(contextKey))
        {
            return null;
        }

        return GetAllArticles().FirstOrDefault(x => string.Equals(x.ContextKey, contextKey, StringComparison.OrdinalIgnoreCase));
    }

    public HelpArticle? GetByRelativePath(string relativePath)
    {
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            return null;
        }

        var normalisedPath = relativePath.Replace('\\', '/');
        return GetAllArticles().FirstOrDefault(x => string.Equals(x.RelativePath, normalisedPath, StringComparison.OrdinalIgnoreCase));
    }

    public IReadOnlyList<HelpArticle> GetByCategory(string category)
    {
        if (string.IsNullOrWhiteSpace(category))
        {
            return [];
        }

        return GetAllArticles()
            .Where(x => string.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase))
            .OrderBy(x => x.Title)
            .ToList();
    }

    public IReadOnlyList<HelpArticle> GetByCategorySlug(string categorySlug)
    {
        if (string.IsNullOrWhiteSpace(categorySlug))
        {
            return [];
        }

        return GetAllArticles()
            .Where(x => string.Equals(x.CategorySlug, categorySlug, StringComparison.OrdinalIgnoreCase))
            .OrderBy(x => x.Title)
            .ToList();
    }

    public string? GetCategoryBySlug(string categorySlug)
    {
        if (string.IsNullOrWhiteSpace(categorySlug))
        {
            return null;
        }

        return GetAllArticles()
            .Select(x => x.Category)
            .FirstOrDefault(category => string.Equals(Slugify(category), categorySlug, StringComparison.OrdinalIgnoreCase));
    }

    public IReadOnlyList<string> GetCategories() =>
        GetAllArticles()
            .Where(x => !string.IsNullOrWhiteSpace(x.Category))
            .Select(x => x.Category)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(x => x)
            .ToList();

    public HelpArticle? GetBySlugs(string categorySlug, string articleSlug)
    {
        if (string.IsNullOrWhiteSpace(categorySlug) || string.IsNullOrWhiteSpace(articleSlug))
        {
            return null;
        }

        return GetAllArticles().FirstOrDefault(x =>
            string.Equals(x.CategorySlug, categorySlug, StringComparison.OrdinalIgnoreCase)
            && string.Equals(x.Slug, articleSlug, StringComparison.OrdinalIgnoreCase));
    }

    public IReadOnlyList<HelpArticle> Search(string query)
    {
        if (string.IsNullOrWhiteSpace(query))
        {
            return [];
        }

        var terms = query.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
        return GetAllArticles()
            .Select(article => new { Article = article, Score = SearchScore(article, terms) })
            .Where(x => x.Score > 0)
            .OrderByDescending(x => x.Score)
            .ThenBy(x => x.Article.Title)
            .Select(x => x.Article)
            .ToList();
    }

    public IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5)
    {
        var tags = article.Tags.Select(x => x.ToLowerInvariant()).ToHashSet();
        return GetAllArticles()
            .Where(x => !string.Equals(x.ContextKey, article.ContextKey, StringComparison.OrdinalIgnoreCase))
            .Select(x => new
            {
                Article = x,
                Score = (string.Equals(x.Category, article.Category, StringComparison.OrdinalIgnoreCase) ? 2 : 0)
                    + x.Tags.Count(tag => tags.Contains(tag.ToLowerInvariant()))
            })
            .Where(x => x.Score > 0)
            .OrderByDescending(x => x.Score)
            .ThenBy(x => x.Article.Title)
            .Take(maxItems)
            .Select(x => x.Article)
            .ToList();
    }

    public IReadOnlyList<HelpCoverageItem> GetCoverage() =>
        GetAllArticles()
            .Select(x => new HelpCoverageItem
            {
                ContextKey = x.ContextKey,
                FilePath = x.RelativePath,
                IsValid = x.IsValid,
                ValidationIssues = x.ValidationIssues
            })
            .OrderBy(x => x.ContextKey)
            .ThenBy(x => x.FilePath)
            .ToList();

    private IReadOnlyList<HelpArticle> LoadArticles()
    {
        var articleRoot = Path.Combine(environment.ContentRootPath, "Help", "Articles");
        if (!Directory.Exists(articleRoot))
        {
            return [];
        }

        var articles = Directory
            .EnumerateFiles(articleRoot, "*.md", SearchOption.AllDirectories)
            .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
            .Select(path => ParseArticle(articleRoot, path))
            .ToList();
        EnsureUniqueArticleSlugs(articles);
        return articles;
    }

    private static HelpArticle ParseArticle(string articleRoot, string filePath)
    {
        var rawContent = File.ReadAllText(filePath);
        var relativePath = Path.GetRelativePath(articleRoot, filePath).Replace('\\', '/');
        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        var markdownContent = rawContent;
        var issues = new List<string>();

        if (rawContent.StartsWith("---", StringComparison.Ordinal))
        {
            using var reader = new StringReader(rawContent);
            _ = reader.ReadLine();
            var frontMatterLines = new List<string>();
            string? line;
            var frontMatterClosed = false;

            while ((line = reader.ReadLine()) is not null)
            {
                if (line.Trim() == "---")
                {
                    frontMatterClosed = true;
                    break;
                }

                frontMatterLines.Add(line);
            }

            if (frontMatterClosed)
            {
                metadata = ParseFrontMatter(frontMatterLines);
                markdownContent = reader.ReadToEnd().TrimStart();
            }
            else
            {
                issues.Add("Front matter was opened but not closed.");
            }
        }
        else
        {
            issues.Add("Front matter is missing.");
        }

        var article = new HelpArticle
        {
            Title = GetValue(metadata, "title"),
            Category = GetValue(metadata, "category"),
            CategorySlug = Slugify(GetValue(metadata, "category")),
            Tags = SplitTags(GetValue(metadata, "tags")),
            ContextKey = GetValue(metadata, "contextKey"),
            Slug = GetArticleSlug(metadata, relativePath),
            Micro = GetValue(metadata, "micro"),
            QuickTitle = GetValue(metadata, "quickTitle"),
            QuickSummary = GetValue(metadata, "quickSummary"),
            Screenshot = GetValue(metadata, "screenshot"),
            MarkdownContent = markdownContent,
            RelativePath = relativePath,
            LastModifiedUtc = File.GetLastWriteTimeUtc(filePath)
        };

        issues.AddRange(Validate(article));
        article.ValidationIssues = issues;
        return article;
    }

    private static Dictionary<string, string> ParseFrontMatter(IEnumerable<string> lines)
    {
        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var line in lines)
        {
            var separatorIndex = line.IndexOf(':');
            if (separatorIndex <= 0)
            {
                continue;
            }

            var key = line[..separatorIndex].Trim();
            var value = line[(separatorIndex + 1)..].Trim().Trim('"');
            metadata[key] = value;
        }

        return metadata;
    }

    private static IReadOnlyList<string> Validate(HelpArticle article)
    {
        var issues = new List<string>();
        if (string.IsNullOrWhiteSpace(article.Title)) issues.Add("Title is required.");
        if (string.IsNullOrWhiteSpace(article.ContextKey)) issues.Add("ContextKey is required.");
        if (string.IsNullOrWhiteSpace(article.QuickTitle)) issues.Add("QuickTitle is required.");
        if (string.IsNullOrWhiteSpace(article.QuickSummary)) issues.Add("QuickSummary is required.");
        if (string.IsNullOrWhiteSpace(article.CategorySlug)) issues.Add("Category slug could not be generated.");
        if (string.IsNullOrWhiteSpace(article.Slug)) issues.Add("Article slug could not be generated.");
        return issues;
    }

    private static string GetValue(IReadOnlyDictionary<string, string> metadata, string key) =>
        metadata.TryGetValue(key, out var value) ? value : string.Empty;

    private static IReadOnlyList<string> SplitTags(string tags) =>
        string.IsNullOrWhiteSpace(tags)
            ? []
            : tags.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);

    private static int SearchScore(HelpArticle article, IEnumerable<string> terms)
    {
        var score = 0;
        foreach (var term in terms)
        {
            if (Contains(article.Title, term)) score += 8;
            if (Contains(article.Category, term)) score += 4;
            if (article.Tags.Any(tag => Contains(tag, term))) score += 5;
            if (Contains(article.QuickSummary, term)) score += 3;
            if (Contains(article.MarkdownContent, term)) score += 1;
        }

        return score;
    }

    private static string GetArticleSlug(IReadOnlyDictionary<string, string> metadata, string relativePath)
    {
        var explicitSlug = Slugify(GetValue(metadata, "slug"));
        if (!string.IsNullOrWhiteSpace(explicitSlug))
        {
            return explicitSlug;
        }

        var titleSlug = Slugify(GetValue(metadata, "title"));
        if (!string.IsNullOrWhiteSpace(titleSlug))
        {
            return titleSlug;
        }

        var contextKey = GetValue(metadata, "contextKey");
        var finalContextSegment = contextKey.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).LastOrDefault();
        var contextSlug = Slugify(finalContextSegment ?? string.Empty);
        if (!string.IsNullOrWhiteSpace(contextSlug))
        {
            return contextSlug;
        }

        return Slugify(Path.GetFileNameWithoutExtension(relativePath));
    }

    private static void EnsureUniqueArticleSlugs(IReadOnlyList<HelpArticle> articles)
    {
        foreach (var group in articles.GroupBy(x => x.CategorySlug, StringComparer.OrdinalIgnoreCase))
        {
            var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var article in group.OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase))
            {
                var baseSlug = string.IsNullOrWhiteSpace(article.Slug) ? "article" : article.Slug;
                var slug = baseSlug;
                if (used.Contains(slug))
                {
                    var contextSuffix = Slugify(article.ContextKey.Replace('.', ' '));
                    slug = string.IsNullOrWhiteSpace(contextSuffix) ? baseSlug : $"{baseSlug}-{contextSuffix}";
                }

                var sequence = 2;
                var uniqueSlug = slug;
                while (!used.Add(uniqueSlug))
                {
                    uniqueSlug = $"{slug}-{sequence}";
                    sequence++;
                }

                article.Slug = uniqueSlug;
            }
        }
    }

    public static string Slugify(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var normalised = value.Normalize(NormalizationForm.FormD);
        var builder = new StringBuilder();
        var pendingSeparator = false;
        foreach (var character in normalised)
        {
            var category = CharUnicodeInfo.GetUnicodeCategory(character);
            if (category == UnicodeCategory.NonSpacingMark)
            {
                continue;
            }

            if (char.IsLetterOrDigit(character))
            {
                if (pendingSeparator && builder.Length > 0)
                {
                    builder.Append('-');
                }

                builder.Append(char.ToLowerInvariant(character));
                pendingSeparator = false;
                continue;
            }

            pendingSeparator = builder.Length > 0;
        }

        return builder.ToString();
    }

    private static bool Contains(string? source, string value) =>
        !string.IsNullOrWhiteSpace(source)
        && source.Contains(value, StringComparison.OrdinalIgnoreCase);
}
