namespace Service.Transaction;

public interface ITransactionStatisticsService
{
    Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null);

    Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);

    Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month);

    Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type);

    Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount);

    Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20);

    Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10);

    Task<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
        DateTime startDate,
        DateTime endDate,
        TransactionType type,
        IEnumerable<string> classifies,
        bool groupByMonth = false);

    Task<Dictionary<(string, TransactionType), decimal>> GetAmountGroupByClassifyAsync(DateTime startTime, DateTime endTime);
}

public class TransactionStatisticsService(
    ITransactionRecordRepository transactionRepository
) : ITransactionStatisticsService
{
    public async Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null)
    {
        var startDate = new DateTime(year, month, 1);
        var endDate = startDate.AddMonths(1);

        return await GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
    }

    public async Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null)
    {
        var records = await transactionRepository.QueryAsync(
            startDate: startDate,
            endDate: endDate,
            pageSize: int.MaxValue);

        return records
            .GroupBy(t => t.OccurredAt.ToString("yyyy-MM-dd"))
            .ToDictionary(
                g => g.Key,
                g =>
                {
                    var income = g.Where(t => t.Type == TransactionType.Income).Sum(t => Math.Abs(t.Amount));
                    var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount));

                    var saving = 0m;
                    if (!string.IsNullOrEmpty(savingClassify))
                    {
                        saving = g.Where(t => savingClassify.Split(',').Contains(t.Classify)).Sum(t => Math.Abs(t.Amount));
                    }

                    return (count: g.Count(), expense, income, saving);
                }
            );
    }

    public async Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month)
    {
        var records = await transactionRepository.QueryAsync(
            year: year,
            month: month,
            pageSize: int.MaxValue);

        var statistics = new MonthlyStatistics
        {
            Year = year,
            Month = month
        };

        foreach (var record in records)
        {
            var amount = Math.Abs(record.Amount);

            if (record.Type == TransactionType.Expense)
            {
                statistics.TotalExpense += amount;
                statistics.ExpenseCount++;
            }
            else if (record.Type == TransactionType.Income)
            {
                statistics.TotalIncome += amount;
                statistics.IncomeCount++;
            }
        }

        statistics.Balance = statistics.TotalIncome - statistics.TotalExpense;
        statistics.TotalCount = records.Count;

        return statistics;
    }

    public async Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type)
    {
        var records = await transactionRepository.QueryAsync(
            year: year,
            month: month,
            type: type,
            pageSize: int.MaxValue);

        var categoryGroups = records
            .GroupBy(t => t.Classify)
            .Select(g => new CategoryStatistics
            {
                Classify = g.Key,
                Amount = g.Sum(t => Math.Abs(t.Amount)),
                Count = g.Count()
            })
            .OrderByDescending(c => c.Amount)
            .ToList();

        var total = categoryGroups.Sum(c => c.Amount);
        if (total > 0)
        {
            foreach (var category in categoryGroups)
            {
                category.Percent = Math.Round((category.Amount / total) * 100, 1);
            }
        }

        return categoryGroups;
    }

    public async Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount)
    {
        var trends = new List<TrendStatistics>();

        for (var i = 0; i < monthCount; i++)
        {
            var targetYear = startYear;
            var targetMonth = startMonth + i;

            while (targetMonth > 12)
            {
                targetMonth -= 12;
                targetYear++;
            }

            var records = await transactionRepository.QueryAsync(
                year: targetYear,
                month: targetMonth,
                pageSize: int.MaxValue);

            var expense = records.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount));
            var income = records.Where(t => t.Type == TransactionType.Income).Sum(t => Math.Abs(t.Amount));

            trends.Add(new TrendStatistics
            {
                Year = targetYear,
                Month = targetMonth,
                Expense = expense,
                Income = income,
                Balance = income - expense
            });
        }

        return trends;
    }

    public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
    {
        var records = await transactionRepository.QueryAsync(
            pageSize: int.MaxValue);

        var unclassifiedRecords = records
            .Where(t => !string.IsNullOrEmpty(t.Reason) && string.IsNullOrEmpty(t.Classify))
            .GroupBy(t => t.Reason)
            .Select(g => new
            {
                Reason = g.Key,
                Count = g.Count(),
                TotalAmount = g.Sum(r => r.Amount),
                SampleType = g.First().Type,
                SampleClassify = g.First().Classify,
                TransactionIds = g.Select(r => r.Id).ToList()
            })
            .OrderByDescending(g => Math.Abs(g.TotalAmount))
            .ToList();

        var total = unclassifiedRecords.Count;
        var pagedGroups = unclassifiedRecords
            .Skip((pageIndex - 1) * pageSize)
            .Take(pageSize)
            .Select(g => new ReasonGroupDto
            {
                Reason = g.Reason,
                Count = g.Count,
                SampleType = g.SampleType,
                SampleClassify = g.SampleClassify,
                TransactionIds = g.TransactionIds,
                TotalAmount = Math.Abs(g.TotalAmount)
            })
            .ToList();

        return (pagedGroups, total);
    }

    public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10)
    {
        if (keywords.Count == 0)
        {
            return [];
        }

        var candidates = await transactionRepository.GetClassifiedByKeywordsAsync(keywords, limit: int.MaxValue);

        var scoredResults = candidates
            .Select(record =>
            {
                var matchedCount = keywords.Count(keyword => record.Reason.Contains(keyword, StringComparison.OrdinalIgnoreCase));
                var matchRate = (double)matchedCount / keywords.Count;

                var exactMatchBonus = keywords.Any(k => record.Reason.Equals(k, StringComparison.OrdinalIgnoreCase)) ? 0.2 : 0.0;

                var avgKeywordLength = keywords.Average(k => k.Length);
                var lengthSimilarity = 1.0 - Math.Min(1.0, Math.Abs(record.Reason.Length - avgKeywordLength) / Math.Max(record.Reason.Length, avgKeywordLength));
                var lengthBonus = lengthSimilarity * 0.1;

                var score = matchRate + exactMatchBonus + lengthBonus;
                return (record, score);
            })
            .Where(x => x.score >= minMatchRate)
            .OrderByDescending(x => x.score)
            .ThenByDescending(x => x.record.OccurredAt)
            .Take(limit)
            .ToList();

        return scoredResults;
    }

    public async Task<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
        DateTime startDate,
        DateTime endDate,
        TransactionType type,
        IEnumerable<string> classifies,
        bool groupByMonth = false)
    {
        var records = await transactionRepository.QueryAsync(
            startDate: startDate,
            endDate: endDate,
            type: type,
            classifies: classifies.ToArray(),
            pageSize: int.MaxValue);

        if (groupByMonth)
        {
            return records
                .GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1))
                .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
        }

        return records
            .GroupBy(t => t.OccurredAt.Date)
            .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
    }

    public async Task<Dictionary<(string, TransactionType), decimal>> GetAmountGroupByClassifyAsync(DateTime startTime, DateTime endTime)
    {
        var records = await transactionRepository.QueryAsync(
            startDate: startTime,
            endDate: endTime,
            pageSize: int.MaxValue);

        return records
            .GroupBy(t => new { t.Classify, t.Type })
            .ToDictionary(g => (g.Key.Classify, g.Key.Type), g => g.Sum(t => t.Amount));
    }
}

public record ReasonGroupDto
{
    public string Reason { get; set; } = string.Empty;

    public int Count { get; set; }

    public TransactionType SampleType { get; set; }

    public string SampleClassify { get; set; } = string.Empty;

    public List<long> TransactionIds { get; set; } = [];

    public decimal TotalAmount { get; set; }
}

public record MonthlyStatistics
{
    public int Year { get; set; }

    public int Month { get; set; }

    public decimal TotalExpense { get; set; }

    public decimal TotalIncome { get; set; }

    public decimal Balance { get; set; }

    public int ExpenseCount { get; set; }

    public int IncomeCount { get; set; }

    public int TotalCount { get; set; }
}

public record CategoryStatistics
{
    public string Classify { get; set; } = string.Empty;

    public decimal Amount { get; set; }

    public int Count { get; set; }

    public decimal Percent { get; set; }
}

public record TrendStatistics
{
    public int Year { get; set; }

    public int Month { get; set; }

    public decimal Expense { get; set; }

    public decimal Income { get; set; }

    public decimal Balance { get; set; }
}