using Couchbase.Lite;
using WorkTracker.Domain;
using WorkTracker.Services.Festivities;
using WorkTracker.Services.Settings;
using WorkTracker.Services.Storage;

namespace WorkTracker.Services.WorkDays;

public sealed class CouchbaseLiteWorkDayService : IWorkDayService
{
    private readonly Collection workDaysCollection;
    private readonly IAppSettingsService appSettingsService;
    private readonly IItalianFestivitySource festivitySource;

    public CouchbaseLiteWorkDayService(
        CouchbaseLiteDatabaseProvider databaseProvider,
        IAppSettingsService appSettingsService,
        IItalianFestivitySource festivitySource)
    {
        workDaysCollection = databaseProvider.WorkDays;
        this.appSettingsService = appSettingsService;
        this.festivitySource = festivitySource;
    }

    public Task<WorkDayDocument?> GetAsync(DateOnly date, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var id = date.ToString("yyyy-MM-dd");
        var doc = workDaysCollection.GetDocument(id);
        return Task.FromResult(doc is not null ? Map(doc) : null);
    }

    public async Task<WorkDayDocument> SaveAsync(WorkDayDocument workDay, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var settings = await appSettingsService.GetAsync(cancellationToken);
        var festivities = festivitySource.GetFestivities(workDay.Date.Year);

        workDay.Id = workDay.Date.ToString("yyyy-MM-dd");
        workDay.IsWeekend = workDay.Date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
        workDay.IsItalianFestivity = festivities.Contains(workDay.Date);

        // Snapshot coefficients from current settings
        workDay.CoeffSnapshot = new CoeffSnapshotDocument
        {
            StandardWorkHoursPerDay = settings.StandardWorkHoursPerDay,
            LunchBreakHours = settings.LunchBreakHours,
            HourlyGrossRate = settings.HourlyGrossRate,
            ProfitabilityCoefficient = settings.ProfitabilityCoefficient,
            InpsRate = settings.InpsRate,
            SubstituteTaxRate = settings.SubstituteTaxRate
        };

        Compute(workDay);

        // Preserve creation timestamp for existing documents
        var existing = workDaysCollection.GetDocument(workDay.Id);
        if (existing is not null)
        {
            workDay.CreatedAtUtc = ReadDateTimeOffset(existing, "createdAtUtc");
        }
        else
        {
            workDay.CreatedAtUtc = DateTimeOffset.UtcNow;
        }

        workDay.UpdatedAtUtc = DateTimeOffset.UtcNow;

        SaveDocument(workDay);
        return workDay;
    }

    public Task<IReadOnlyList<WorkDayDocument>> GetRangeAsync(DateOnly from, DateOnly to, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var results = new List<WorkDayDocument>();
        for (var date = from; date <= to; date = date.AddDays(1))
        {
            var id = date.ToString("yyyy-MM-dd");
            var doc = workDaysCollection.GetDocument(id);
            if (doc is not null)
            {
                results.Add(Map(doc));
            }
        }

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

    public async Task<MonthlySummaryModel> GetMonthlySummaryAsync(int year, int month, CancellationToken cancellationToken = default)
    {
        var from = new DateOnly(year, month, 1);
        var to = from.AddMonths(1).AddDays(-1);
        var days = await GetRangeAsync(from, to, cancellationToken);

        return new MonthlySummaryModel
        {
            Year = year,
            Month = month,
            TotalWorkedHours = days.Sum(d => d.WorkedHoursFinal),
            OfficeDays = days.Count(d => d.DayType == DayType.Work),
            HomeDays = days.Count(d => d.DayType == DayType.Home),
            HolidayDays = days.Count(d => d.DayType == DayType.Holiday),
            SickDays = days.Count(d => d.DayType == DayType.Illness),
            DaysOff = days.Count(d => d.DayType == DayType.DayOff),
            ClosureDays = days.Count(d => d.DayType == DayType.Closure),
            TotalHoursOff = days.Sum(d => d.HoursOff),
            TotalGrossIncome = days.Sum(d => d.GrossIncome),
            TotalNetIncome = days.Sum(d => d.NetIncome),
            TotalWorkingDays = days.Count(d => d.DayType is DayType.Work or DayType.Home)
        };
    }

    private static void Compute(WorkDayDocument day)
    {
        var coeff = day.CoeffSnapshot;

        // Calculate projected exit time
        if (day.StartTime.HasValue && day.DayType is DayType.Work or DayType.Home)
        {
            var totalHours = coeff.StandardWorkHoursPerDay + coeff.LunchBreakHours;
            day.ProjectedExitTime = day.StartTime.Value.Add(TimeSpan.FromHours((double)totalHours));
        }
        else
        {
            day.ProjectedExitTime = null;
        }

        // Calculate worked hours
        day.WorkedHoursBase = day.DayType is DayType.Work or DayType.Home
            ? coeff.StandardWorkHoursPerDay
            : 0m;

        day.WorkedHoursFinal = day.WorkedHoursBase + day.ExtraHoursDelta;

        // Hours off (only for work/home days)
        day.HoursOff = day.DayType is DayType.Work or DayType.Home
            ? Math.Max(0m, coeff.StandardWorkHoursPerDay - day.WorkedHoursFinal)
            : 0m;

        // Income calculations
        day.GrossIncome = day.WorkedHoursFinal * coeff.HourlyGrossRate;
        var taxableBase = day.GrossIncome * coeff.ProfitabilityCoefficient;
        day.NetIncome = day.GrossIncome - (taxableBase * coeff.InpsRate) - (taxableBase * coeff.SubstituteTaxRate);
    }

    private void SaveDocument(WorkDayDocument day)
    {
        var doc = new MutableDocument(day.Id);
        doc.SetString("date", day.Date.ToString("yyyy-MM-dd"));
        doc.SetString("startTime", day.StartTime?.ToString("HH:mm"));
        doc.SetString("projectedExitTime", day.ProjectedExitTime?.ToString("HH:mm"));
        doc.SetString("actualExitTime", day.ActualExitTime?.ToString("HH:mm"));
        doc.SetInt("dayType", (int)day.DayType);
        doc.SetDouble("extraHoursDelta", decimal.ToDouble(day.ExtraHoursDelta));
        doc.SetDouble("workedHoursBase", decimal.ToDouble(day.WorkedHoursBase));
        doc.SetDouble("workedHoursFinal", decimal.ToDouble(day.WorkedHoursFinal));
        doc.SetDouble("hoursOff", decimal.ToDouble(day.HoursOff));
        doc.SetDouble("grossIncome", decimal.ToDouble(day.GrossIncome));
        doc.SetDouble("netIncome", decimal.ToDouble(day.NetIncome));
        doc.SetBoolean("isWeekend", day.IsWeekend);
        doc.SetBoolean("isItalianFestivity", day.IsItalianFestivity);
        doc.SetString("notes", day.Notes);

        // Coefficient snapshot
        doc.SetDouble("coeff_standardWorkHoursPerDay", decimal.ToDouble(day.CoeffSnapshot.StandardWorkHoursPerDay));
        doc.SetDouble("coeff_lunchBreakHours", decimal.ToDouble(day.CoeffSnapshot.LunchBreakHours));
        doc.SetDouble("coeff_hourlyGrossRate", decimal.ToDouble(day.CoeffSnapshot.HourlyGrossRate));
        doc.SetDouble("coeff_profitabilityCoefficient", decimal.ToDouble(day.CoeffSnapshot.ProfitabilityCoefficient));
        doc.SetDouble("coeff_inpsRate", decimal.ToDouble(day.CoeffSnapshot.InpsRate));
        doc.SetDouble("coeff_substituteTaxRate", decimal.ToDouble(day.CoeffSnapshot.SubstituteTaxRate));

        doc.SetString("createdAtUtc", day.CreatedAtUtc.ToString("O"));
        doc.SetString("updatedAtUtc", day.UpdatedAtUtc.ToString("O"));

        workDaysCollection.Save(doc);
    }

    private static WorkDayDocument Map(Document doc)
    {
        return new WorkDayDocument
        {
            Id = doc.Id,
            Date = DateOnly.ParseExact(doc.GetString("date") ?? doc.Id, "yyyy-MM-dd"),
            StartTime = ReadTimeOnly(doc, "startTime"),
            ProjectedExitTime = ReadTimeOnly(doc, "projectedExitTime"),
            ActualExitTime = ReadTimeOnly(doc, "actualExitTime"),
            DayType = (DayType)doc.GetInt("dayType"),
            ExtraHoursDelta = Convert.ToDecimal(doc.GetDouble("extraHoursDelta")),
            WorkedHoursBase = Convert.ToDecimal(doc.GetDouble("workedHoursBase")),
            WorkedHoursFinal = Convert.ToDecimal(doc.GetDouble("workedHoursFinal")),
            HoursOff = Convert.ToDecimal(doc.GetDouble("hoursOff")),
            GrossIncome = Convert.ToDecimal(doc.GetDouble("grossIncome")),
            NetIncome = Convert.ToDecimal(doc.GetDouble("netIncome")),
            IsWeekend = doc.GetBoolean("isWeekend"),
            IsItalianFestivity = doc.GetBoolean("isItalianFestivity"),
            Notes = doc.GetString("notes"),
            CoeffSnapshot = new CoeffSnapshotDocument
            {
                StandardWorkHoursPerDay = ReadDecimal(doc, "coeff_standardWorkHoursPerDay", 8m),
                LunchBreakHours = ReadDecimal(doc, "coeff_lunchBreakHours", 1m),
                HourlyGrossRate = ReadDecimal(doc, "coeff_hourlyGrossRate", 17.5m),
                ProfitabilityCoefficient = ReadDecimal(doc, "coeff_profitabilityCoefficient", 0.67m),
                InpsRate = ReadDecimal(doc, "coeff_inpsRate", 0.2607m),
                SubstituteTaxRate = ReadDecimal(doc, "coeff_substituteTaxRate", 0.15m)
            },
            CreatedAtUtc = ReadDateTimeOffset(doc, "createdAtUtc"),
            UpdatedAtUtc = ReadDateTimeOffset(doc, "updatedAtUtc")
        };
    }

    private static TimeOnly? ReadTimeOnly(Document doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && TimeOnly.TryParseExact(value, "HH:mm", out var time)
            ? time
            : null;
    }

    private static decimal ReadDecimal(Document doc, string key, decimal defaultValue)
    {
        return doc.Contains(key)
            ? Convert.ToDecimal(doc.GetDouble(key))
            : defaultValue;
    }

    private static DateTimeOffset ReadDateTimeOffset(Document doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && DateTimeOffset.TryParse(value, out var dt)
            ? dt
            : DateTimeOffset.UtcNow;
    }
}
