using Journal.Core.Dtos;
using Journal.Core.Repositories;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

namespace Journal.Core.Services.Entries;

public class EntrySearchService(IEntryFileRepository repo) : IEntrySearchService
{
    private readonly IEntryFileRepository _repo = repo;
    private readonly Lock _cacheLock = new();
    private readonly Dictionary<string, CachedEntry> _entryCache = new(StringComparer.Ordinal);

    public Task<IReadOnlyList<EntrySearchResultDto>> SearchEntriesAsync(EntrySearchRequestDto request)
    {
        ArgumentNullException.ThrowIfNull(request);

        var hasQuery = !string.IsNullOrWhiteSpace(request.Query);
        var query = request.Query?.Trim() ?? "";
        var hasSectionFilter = !string.IsNullOrWhiteSpace(request.Section);
        var section = request.Section?.Trim() ?? "";

        var typeSet = NormalizeSet(request.Types);
        var tagSet = NormalizeSet(request.Tags);
        var checkedSet = NormalizeSet(request.Checked);
        var uncheckedSet = NormalizeSet(request.Unchecked);
        var hasFragmentFilters = typeSet.Count > 0 || tagSet.Count > 0;
        var hasCheckboxFilters = checkedSet.Count > 0 || uncheckedSet.Count > 0;

        var startDate = ParseOptionalDate(request.StartDate, nameof(request.StartDate));
        var endDate = ParseOptionalDate(request.EndDate, nameof(request.EndDate));
        if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value)
            throw new ArgumentException("startDate cannot be after endDate.");

        var currentFiles = _repo.ListMarkdownFiles()
            .OrderBy(Path.GetFileName, StringComparer.Ordinal)
            .ToArray();

        var currentFileSet = new HashSet<string>(currentFiles, StringComparer.Ordinal);
        var results = new List<EntrySearchResultDto>();
        foreach (var filePath in currentFiles)
        {
            var fileName = _repo.GetFileName(filePath);
            if (EntryFileNaming.IsTemplateFileName(fileName))
                continue;

            var cached = GetOrBuildCachedEntry(filePath);
            var entry = cached.Result.Entry;

            if (startDate.HasValue || endDate.HasValue)
            {
                if (!DateOnly.TryParseExact(entry.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var entryDate))
                    continue;

                if (startDate.HasValue && entryDate < startDate.Value)
                    continue;
                if (endDate.HasValue && entryDate > endDate.Value)
                    continue;
            }

            var contentMatch = true;
            if (hasQuery)
            {
                var haystack = hasSectionFilter ? GetSection(entry, section) : entry.RawContent;
                contentMatch = haystack.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
            }
            if (!contentMatch)
                continue;

            var fragmentMatch = !hasFragmentFilters || entry.Fragments.Any(fragment =>
                (typeSet.Count == 0 || typeSet.Contains(fragment.Type)) &&
                (tagSet.Count == 0 || fragment.Tags.Any(tagSet.Contains)));
            if (!fragmentMatch)
                continue;

            var checkboxMatch = !hasCheckboxFilters || entry.Sections.Values.Any(sectionValue =>
                sectionValue.Checkboxes.Any(checkbox =>
                    (checkedSet.Count > 0 && checkbox.Value && checkedSet.Contains(checkbox.Key)) ||
                    (uncheckedSet.Count > 0 && !checkbox.Value && uncheckedSet.Contains(checkbox.Key))));
            if (!checkboxMatch)
                continue;

            results.Add(cached.Result);
        }

        RemoveStaleCacheEntries(currentFileSet);

        return Task.FromResult<IReadOnlyList<EntrySearchResultDto>>(results);
    }

    private CachedEntry GetOrBuildCachedEntry(string filePath)
    {
        var diskSignature = TryGetDiskFileSignature(filePath);

        if (diskSignature is not null)
        {
            lock (_cacheLock)
            {
                if (_entryCache.TryGetValue(filePath, out var cached) &&
                    cached.Signature == diskSignature.Value)
                {
                    return cached;
                }
            }
        }

        var fileName = _repo.GetFileName(filePath);
        var fileStem = _repo.GetFileNameWithoutExtension(filePath);
        var rawContent = _repo.ReadFile(filePath);
        var signature = diskSignature ?? BuildContentSignature(rawContent);

        lock (_cacheLock)
        {
            if (_entryCache.TryGetValue(filePath, out var cached) &&
                cached.Signature == signature)
            {
                return cached;
            }
        }

        var entry = JournalParser.ParseJournalContent(rawContent, fileStem).ToDto();
        var built = new CachedEntry(signature, new EntrySearchResultDto(fileName, entry));

        lock (_cacheLock)
        {
            _entryCache[filePath] = built;
        }

        return built;
    }

    private static FileSignature? TryGetDiskFileSignature(string filePath)
    {
        if (filePath.StartsWith("db://", StringComparison.OrdinalIgnoreCase))
            return null;
        if (!File.Exists(filePath))
            return null;

        var info = new FileInfo(filePath);
        return new FileSignature(info.Length, info.LastWriteTimeUtc.Ticks, null);
    }

    private static FileSignature BuildContentSignature(string content)
    {
        var bytes = Encoding.UTF8.GetBytes(content ?? "");
        var hash = Convert.ToHexString(SHA256.HashData(bytes));
        return new FileSignature(bytes.Length, 0, hash);
    }

    private void RemoveStaleCacheEntries(HashSet<string> currentFileSet)
    {
        lock (_cacheLock)
        {
            if (_entryCache.Count == 0)
                return;

            var staleKeys = _entryCache.Keys
                .Where(path => !currentFileSet.Contains(path))
                .ToArray();

            foreach (var key in staleKeys)
                _entryCache.Remove(key);
        }
    }

    private static string GetSection(JournalEntryDto entry, string sectionTitle)
    {
        if (string.IsNullOrWhiteSpace(sectionTitle))
            return "";

        foreach (var (key, value) in entry.Sections)
        {
            if (string.Equals(key, sectionTitle, StringComparison.OrdinalIgnoreCase))
                return string.Join("\n", value.Content);
        }

        return "";
    }

    private static HashSet<string> NormalizeSet(IReadOnlyList<string>? values)
    {
        if (values is null || values.Count == 0)
            return [];

        var set = new HashSet<string>(StringComparer.Ordinal);
        foreach (var value in values)
        {
            if (string.IsNullOrWhiteSpace(value))
                continue;
            set.Add(value.Trim());
        }

        return set;
    }

    private static DateOnly? ParseOptionalDate(string? raw, string argumentName)
    {
        if (string.IsNullOrWhiteSpace(raw))
            return null;

        if (DateOnly.TryParseExact(raw.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
            return date;

        throw new ArgumentException($"Invalid {argumentName} value. Expected yyyy-MM-dd.");
    }

    private readonly record struct FileSignature(long Length, long LastWriteUtcTicks, string? ContentHash);
    private sealed record CachedEntry(FileSignature Signature, EntrySearchResultDto Result);
}
