using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

// Phase 5.3 Milestone 1 — timeline intelligence.
//
// A READ-ONLY interpretation layer over JobEvent. JobEvent stays the source of historical truth: this
// writes nothing, stores nothing, and adds no table. It turns rows like
// ("StatusChanged", "Applied", "Interview") into a sentence, tags each event with a category and
// whether it is a milestone, and groups the result by day so the workspace can render a real
// timeline instead of a flat list. docs/architecture/application-workspace.md.
public sealed record TimelineEventDto(
    int Id,
    string Type,
    string Category,
    string Summary,
    string? Detail,
    bool IsMilestone,
    DateTime At);

public sealed record TimelineDayDto(DateTime Date, string Label, IReadOnlyList<TimelineEventDto> Events);

public sealed record TimelineDto(
    IReadOnlyList<TimelineDayDto> Days,
    IReadOnlyList<TimelineEventDto> Milestones,
    IReadOnlyList<string> Categories,
    int TotalEvents);

public interface IApplicationTimelineService
{
    Task<TimelineDto?> GetAsync(string ownerUserId, int jobApplicationId, string? category, bool milestonesOnly, CancellationToken ct);
}

public sealed class ApplicationTimelineService : IApplicationTimelineService
{
    // Event categories, so the UI can filter without knowing every raw Type.
    public const string CategoryLifecycle = "lifecycle";
    public const string CategoryStage = "stage";
    public const string CategoryFollowUp = "follow-up";
    public const string CategoryCommunication = "communication";
    public const string CategoryAi = "ai";

    private readonly JobTrackerContext _db;

    public ApplicationTimelineService(JobTrackerContext db)
    {
        _db = db;
    }

    public async Task<TimelineDto?> GetAsync(string ownerUserId, int jobApplicationId, string? category, bool milestonesOnly, CancellationToken ct)
    {
        var owns = await _db.JobApplications.AsNoTracking()
            .AnyAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
        if (!owns) return null;

        var events = await _db.JobEvents.AsNoTracking()
            .Where(e => e.JobApplicationId == jobApplicationId)
            .OrderByDescending(e => e.At)
            .ThenByDescending(e => e.Id)
            .ToListAsync(ct);

        var projected = events.Select(Describe).ToList();

        var filtered = projected
            .Where(e => string.IsNullOrWhiteSpace(category) || string.Equals(e.Category, category, StringComparison.OrdinalIgnoreCase))
            .Where(e => !milestonesOnly || e.IsMilestone)
            .ToList();

        var days = filtered
            .GroupBy(e => e.At.Date)
            .OrderByDescending(g => g.Key)
            .Select(g => new TimelineDayDto(g.Key, DayLabel(g.Key), g.ToList()))
            .ToList();

        return new TimelineDto(
            days,
            // Milestones ignore the active filter: they are the "what actually happened" spine and stay
            // visible while the user narrows the detail below.
            projected.Where(e => e.IsMilestone).ToList(),
            projected.Select(e => e.Category).Distinct().OrderBy(c => c, StringComparer.Ordinal).ToList(),
            projected.Count);
    }

    // One JobEvent row -> a sentence a human can read, plus its category and milestone flag.
    private static TimelineEventDto Describe(JobEvent e)
    {
        var type = (e.Type ?? string.Empty).Trim();
        var (category, summary, isMilestone) = type switch
        {
            "Created" => (CategoryLifecycle, "Application created", true),
            "Deleted" => (CategoryLifecycle, "Application moved to trash", false),
            "Restored" => (CategoryLifecycle, "Application restored from trash", false),
            "Undo" => (CategoryLifecycle, "Change undone", false),
            "StatusChanged" => (CategoryStage, StatusSummary(e), IsMilestoneStatus(e.NewValue)),
            // Phase 5.5 lifecycle events. Interviews and offers are milestones; scheduling and
            // completing a follow-up is routine, so it stays out of the milestone spine.
            "InterviewScheduled" => (CategoryStage, InterviewSummary(e, "Interview scheduled"), true),
            "InterviewCompleted" => (CategoryStage, InterviewSummary(e, "Interview completed"), true),
            "OfferReceived" => (CategoryStage, "Offer received", true),
            "FollowUpCreated" => (CategoryFollowUp, FollowUpSummary(e), false),
            "FollowUpCompleted" => (CategoryFollowUp, "Follow-up completed", false),
            "FollowUpSet" => (CategoryFollowUp, FollowUpSummary(e), false),
            "ResponseUpdated" => (CategoryCommunication, ResponseSummary(e), false),
            "ReplyReceived" => (CategoryCommunication, "Reply received", true),
            "AiRefreshed" => (CategoryAi, "AI suggestions refreshed", false),
            _ => (CategoryLifecycle, string.IsNullOrWhiteSpace(type) ? "Activity recorded" : Humanize(type), false),
        };

        // The note is the user's own words, so it always wins as the detail line.
        var detail = !string.IsNullOrWhiteSpace(e.Note) ? e.Note!.Trim() : null;

        return new TimelineEventDto(e.Id, type, category, summary, detail, isMilestone, e.At);
    }

    private static string StatusSummary(JobEvent e)
    {
        var from = Clean(e.OldValue);
        var to = Clean(e.NewValue);
        if (to is null) return "Status changed";
        return from is null ? $"Moved to {to}" : $"Moved from {from} to {to}";
    }

    private static string InterviewSummary(JobEvent e, string prefix)
    {
        var detail = Clean(e.NewValue);
        if (detail is null) return prefix;
        return DateTime.TryParse(detail, out var parsed)
            ? $"{prefix} for {parsed:d MMMM yyyy}"
            : $"{prefix} — {detail}";
    }

    private static string FollowUpSummary(JobEvent e)
    {
        var to = Clean(e.NewValue);
        if (to is null) return "Follow-up cleared";
        return DateTime.TryParse(to, out var parsed)
            ? $"Follow-up scheduled for {parsed:d MMMM yyyy}"
            : $"Follow-up scheduled for {to}";
    }

    private static string ResponseSummary(JobEvent e)
    {
        var to = Clean(e.NewValue);
        return to is null ? "Response status updated" : $"Response marked {to}";
    }

    // The stages that actually mean something happened, as opposed to routine housekeeping.
    private static bool IsMilestoneStatus(string? status)
    {
        var s = (status ?? string.Empty).Trim();
        if (s.Length == 0) return false;
        return s.Contains("applied", StringComparison.OrdinalIgnoreCase)
            || s.Contains("interview", StringComparison.OrdinalIgnoreCase)
            || s.Contains("offer", StringComparison.OrdinalIgnoreCase)
            || s.Contains("rejected", StringComparison.OrdinalIgnoreCase)
            || s.Contains("accepted", StringComparison.OrdinalIgnoreCase)
            || s.Contains("declined", StringComparison.OrdinalIgnoreCase);
    }

    private static string DayLabel(DateTime date)
    {
        var today = DateTime.Now.Date;
        if (date == today) return "Today";
        if (date == today.AddDays(-1)) return "Yesterday";
        return date.Year == today.Year ? date.ToString("dddd d MMMM") : date.ToString("d MMMM yyyy");
    }

    private static string? Clean(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    // "StatusChanged" -> "Status changed", so an unknown future type still reads as a sentence.
    private static string Humanize(string type)
    {
        var chars = new List<char>(type.Length + 4);
        for (var i = 0; i < type.Length; i++)
        {
            if (i > 0 && char.IsUpper(type[i]))
            {
                chars.Add(' ');
                chars.Add(char.ToLowerInvariant(type[i]));
            }
            else
            {
                chars.Add(type[i]);
            }
        }
        return new string(chars.ToArray());
    }
}
