using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services.JobImport;

namespace JobTrackerApi.Services
{
    /// <summary>
    /// Pure, stateless helpers extracted from JobApplicationsController. None of these touch
    /// the database, AI services, or other instance state -- same inputs always produce the
    /// same outputs, so they are safe to share as static methods.
    /// </summary>
    public static class JobApplicationHelpers
    {
        private const string ApplicationAnswerDraftStart = "<<<APPLICATION_ANSWER_DRAFT>>>";
        private const string ApplicationAnswerDraftEnd = "<<<END_APPLICATION_ANSWER_DRAFT>>>";

        public static string GetPreferredDisplayName(ApplicationUser? user)
        {
            if (user is null) return "Your Name";
            if (!string.IsNullOrWhiteSpace(user.DisplayName)) return user.DisplayName.Trim();
            var fullName = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(x => !string.IsNullOrWhiteSpace(x)));
            if (!string.IsNullOrWhiteSpace(fullName)) return fullName;
            if (!string.IsNullOrWhiteSpace(user.UserName)) return user.UserName.Trim();
            if (!string.IsNullOrWhiteSpace(user.Email)) return user.Email.Trim();
            return "Your Name";
        }

        public static string BuildGreeting(JobApplication job)
        {
            if (!string.IsNullOrWhiteSpace(job.Company?.RecruiterName)) return $"Hi {job.Company.RecruiterName.Trim()},";
            if (!string.IsNullOrWhiteSpace(job.Company?.Name)) return $"Hi {job.Company.Name.Trim()} team,";
            return "Hi there,";
        }

        public static string BuildStructuredCvContext(ApplicationUser? user)
        {
            var structured = StructuredCvProfileJson.DeserializePersisted(user?.ProfileCvStructureJson);
            var blocks = new List<string>();

            var contactLines = new List<string>();
            if (!string.IsNullOrWhiteSpace(structured.Contact.FullName)) contactLines.Add($"Name: {structured.Contact.FullName}");
            if (!string.IsNullOrWhiteSpace(structured.Contact.Headline)) contactLines.Add($"Headline: {structured.Contact.Headline}");
            if (!string.IsNullOrWhiteSpace(structured.Contact.Email)) contactLines.Add($"Email: {structured.Contact.Email}");
            if (!string.IsNullOrWhiteSpace(structured.Contact.Location)) contactLines.Add($"Location: {structured.Contact.Location}");
            if (!string.IsNullOrWhiteSpace(structured.Contact.LinkedIn)) contactLines.Add($"LinkedIn: {structured.Contact.LinkedIn}");
            if (contactLines.Count > 0) blocks.Add($"Contact:\n{string.Join("\n", contactLines)}");

            if (structured.Summary.Count > 0)
            {
                blocks.Add($"Summary:\n- {string.Join("\n- ", structured.Summary.Take(4))}");
            }

            if (structured.Skills.Count > 0)
            {
                blocks.Add($"Skills:\n{string.Join(", ", structured.Skills.Take(16))}");
            }

            if (structured.Jobs.Count > 0)
            {
                var jobBlocks = structured.Jobs.Take(3).Select(job =>
                {
                    var header = string.Join(" | ", new[] { job.Title, job.Company, job.Location, FormatStructuredDateRange(job.Start, job.End, job.IsCurrent) }.Where(value => !string.IsNullOrWhiteSpace(value)));
                    var bullets = job.Bullets.Take(3).Select(bullet => $"- {bullet}");
                    return string.Join("\n", new[] { header }.Concat(bullets).Where(value => !string.IsNullOrWhiteSpace(value)));
                }).Where(value => !string.IsNullOrWhiteSpace(value)).ToList();
                if (jobBlocks.Count > 0) blocks.Add($"Work Experience:\n{string.Join("\n\n", jobBlocks)}");
            }

            if (structured.Education.Count > 0)
            {
                var items = structured.Education.Take(3).Select(education => string.Join(" | ", new[] { education.Qualification, education.Institution, education.Location, FormatStructuredDateRange(education.Start, education.End, false) }.Where(value => !string.IsNullOrWhiteSpace(value))));
                blocks.Add($"Education:\n- {string.Join("\n- ", items)}");
            }

            if (structured.Languages.Count > 0)
            {
                var items = structured.Languages.Take(5).Select(language => string.Join(": ", new[] { language.Name, language.Level }.Where(value => !string.IsNullOrWhiteSpace(value))));
                blocks.Add($"Languages:\n- {string.Join("\n- ", items)}");
            }

            if (structured.OtherSections.Count > 0)
            {
                var items = structured.OtherSections.Take(2)
                    .Where(section => !string.IsNullOrWhiteSpace(section.Title) && section.Items.Count > 0)
                    .Select(section => $"{section.Title}: {string.Join("; ", section.Items.Take(4))}")
                    .ToList();
                if (items.Count > 0) blocks.Add($"Other sections:\n- {string.Join("\n- ", items)}");
            }

            if (blocks.Count == 0 && structured.Sections.Count > 0)
            {
                blocks.AddRange(structured.Sections.Take(6).Select(section => $"{section.Name}:\n{section.Content}"));
            }

            return blocks.Count > 0
                ? $"Structured CV:\n{string.Join("\n\n", blocks)}"
                : string.Empty;
        }

        public static string BuildCvSearchCorpus(ApplicationUser? user)
        {
            var structured = StructuredCvProfileJson.DeserializePersisted(user?.ProfileCvStructureJson);
            var parts = new List<string>();
            if (!string.IsNullOrWhiteSpace(user?.ProfileCvText)) parts.Add(user.ProfileCvText!);
            if (!string.IsNullOrWhiteSpace(structured.Contact.Headline)) parts.Add(structured.Contact.Headline!);
            if (structured.Summary.Count > 0) parts.Add(string.Join("\n", structured.Summary));
            if (structured.Skills.Count > 0) parts.Add(string.Join("\n", structured.Skills));
            if (structured.Jobs.Count > 0)
            {
                parts.Add(string.Join("\n", structured.Jobs.SelectMany(job => new[] { job.Title, job.Company, job.Location }.Where(value => !string.IsNullOrWhiteSpace(value)).Concat(job.Bullets).Concat(job.Skills))));
            }
            if (structured.Education.Count > 0)
            {
                parts.Add(string.Join("\n", structured.Education.SelectMany(education => new[] { education.Qualification, education.Institution, education.Location }.Where(value => !string.IsNullOrWhiteSpace(value)).Concat(education.Details))));
            }
            if (structured.Languages.Count > 0)
            {
                parts.Add(string.Join("\n", structured.Languages.Select(language => string.Join(" ", new[] { language.Name, language.Level, language.Notes }.Where(value => !string.IsNullOrWhiteSpace(value))))));
            }
            return string.Join("\n", parts.Where(part => !string.IsNullOrWhiteSpace(part)));
        }

        public static string? FormatStructuredDateRange(string? start, string? end, bool isCurrent)
        {
            if (string.IsNullOrWhiteSpace(start) && string.IsNullOrWhiteSpace(end)) return null;
            if (string.IsNullOrWhiteSpace(start)) return end;
            return $"{start} - {(isCurrent ? "Present" : end ?? "Present")}";
        }

        public static string ComputeGenerationContextHash(string value)
        {
            var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
            return Convert.ToHexString(bytes).ToLowerInvariant();
        }

        public static int ScoreTailoredExperience(StructuredCvJob job, IEnumerable<string> matchedTags)
        {
            var corpus = string.Join("\n", new[] { job.Title, job.Company, job.Location, string.Join("\n", job.Bullets), string.Join("\n", job.Skills) }
                .Where(value => !string.IsNullOrWhiteSpace(value)))
                .ToLowerInvariant();
            var score = 0;
            foreach (var tag in matchedTags.Where(tag => !string.IsNullOrWhiteSpace(tag)))
            {
                if (corpus.Contains(tag.ToLowerInvariant(), StringComparison.Ordinal)) score += 4;
            }
            score += Math.Min(job.Bullets.Count, 4);
            return score;
        }

        public static List<string> SelectTailoredSkills(StructuredCvProfile structured, string jobText)
        {
            var jobTags = SkillTagger.Detect(jobText).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            var prioritized = structured.Skills
                .Select(skill => new
                {
                    Skill = skill,
                    Score = jobTags.Any(tag => skill.Contains(tag, StringComparison.OrdinalIgnoreCase) || tag.Contains(skill, StringComparison.OrdinalIgnoreCase)) ? 2 : 0
                })
                .OrderByDescending(entry => entry.Score)
                .ThenBy(entry => entry.Skill, StringComparer.OrdinalIgnoreCase)
                .Select(entry => entry.Skill)
                .ToList();

            if (prioritized.Count == 0)
            {
                prioritized = structured.Jobs.SelectMany(job => job.Skills).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            }

            return prioritized.Take(10).ToList();
        }

        public static TailoredCvDocument BuildLegacyTailoredCvFallback(JobApplication job)
        {
            var text = (job.TailoredCvText ?? string.Empty).Trim();
            var document = new TailoredCvDocument
            {
                Headline = job.JobTitle,
                CustomSections = string.IsNullOrWhiteSpace(text)
                    ? new List<TailoredCvCustomSection>()
                    : new List<TailoredCvCustomSection>
                    {
                        new TailoredCvCustomSection
                        {
                            Title = "Legacy draft text",
                            Items = text.Split(new[] { "\r\n\r\n", "\n\n" }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(),
                        }
                    }
            };
            return TailoredCvDraftJson.Normalize(document);
        }

        public static TailoredCvDraftDto ToTailoredCvDraftDto(TailoredCvDraft draft)
        {
            var document = TailoredCvDraftJson.FromDraft(draft);
            return new TailoredCvDraftDto(
                draft.Id,
                draft.CanonicalProfileVersion,
                draft.TemplateId,
                document.Headline,
                document.Summary,
                document.SelectedSkills,
                document.Experience,
                document.Education,
                document.CustomSections,
                document.RenderOptions,
                draft.GenerationContextHash,
                draft.LastGeneratedAtUtc,
                draft.LastEditedAtUtc,
                draft.Status,
                TailoredCvDraftJson.RenderPlainText(document),
                false);
        }

        public static TailoredCvDraftDto ToLegacyTailoredCvDraftDto(JobApplication job)
        {
            var document = BuildLegacyTailoredCvFallback(job);
            return new TailoredCvDraftDto(
                null,
                null,
                "legacy-text",
                document.Headline,
                document.Summary,
                document.SelectedSkills,
                document.Experience,
                document.Education,
                document.CustomSections,
                document.RenderOptions,
                null,
                null,
                job.TailoredCvUpdatedAt,
                string.IsNullOrWhiteSpace(job.TailoredCvText) ? "empty" : "legacy-import",
                TailoredCvDraftJson.RenderPlainText(document),
                true);
        }

        public static TailoredCvDocument BuildTailoredCvDocumentForRender(SaveTailoredCvDraftRequest? request, TailoredCvDraft? draft, JobApplication job)
        {
            var baseDocument = draft is not null ? TailoredCvDraftJson.FromDraft(draft) : BuildLegacyTailoredCvFallback(job);
            if (request is null)
            {
                return baseDocument;
            }

            return TailoredCvDraftJson.Normalize(new TailoredCvDocument
            {
                TemplateId = request.TemplateId ?? baseDocument.TemplateId ?? "ats-minimal",
                Headline = request.Headline ?? baseDocument.Headline,
                Summary = request.Summary ?? baseDocument.Summary,
                SelectedSkills = request.SelectedSkills ?? baseDocument.SelectedSkills,
                Experience = request.Experience ?? baseDocument.Experience,
                Education = request.Education ?? baseDocument.Education,
                CustomSections = request.CustomSections ?? baseDocument.CustomSections,
                RenderOptions = request.RenderOptions ?? baseDocument.RenderOptions,
            });
        }

        public static string? ExtractSavedApplicationAnswerDraft(string? notes)
        {
            var value = (notes ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(value)) return null;

            var startIndex = value.IndexOf(ApplicationAnswerDraftStart, StringComparison.Ordinal);
            var endIndex = value.IndexOf(ApplicationAnswerDraftEnd, StringComparison.Ordinal);
            if (startIndex >= 0 && endIndex > startIndex)
            {
                var between = value[(startIndex + ApplicationAnswerDraftStart.Length)..endIndex].Trim();
                return string.IsNullOrWhiteSpace(between) ? null : between;
            }

            const string legacyPrefix = "Application answer draft:";
            var legacyIndex = value.IndexOf(legacyPrefix, StringComparison.OrdinalIgnoreCase);
            if (legacyIndex >= 0)
            {
                var legacy = value[(legacyIndex + legacyPrefix.Length)..].Trim();
                return string.IsNullOrWhiteSpace(legacy) ? null : legacy;
            }

            return null;
        }

        public static string? UpsertSavedApplicationAnswerDraft(string? notes, string? draft)
        {
            var humanNotes = RemoveSavedApplicationAnswerDraft(notes);
            var answer = (draft ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(answer))
            {
                return string.IsNullOrWhiteSpace(humanNotes) ? null : humanNotes;
            }

            var answerBlock = $"{ApplicationAnswerDraftStart}\n{answer}\n{ApplicationAnswerDraftEnd}";
            return string.IsNullOrWhiteSpace(humanNotes)
                ? answerBlock
                : $"{humanNotes}\n\n{answerBlock}";
        }

        public static string BuildFollowUpSubject(JobApplication job, Correspondence? lastMessage)
        {
            var subject = (lastMessage?.Subject ?? string.Empty).Trim();
            if (!string.IsNullOrWhiteSpace(subject))
            {
                return subject.StartsWith("Re:", StringComparison.OrdinalIgnoreCase)
                    ? subject
                    : $"Re: {subject}";
            }

            return $"Following up on {job.JobTitle} application";
        }

        public static List<string> BuildFollowUpContextSignals(JobApplication job, Correspondence? lastMessage, CorrespondenceContextResult? correspondenceContext, SavedPackageMaterial savedPackageMaterial, string? savedApplicationAnswer)
        {
            var signals = new List<string>();

            if (!string.IsNullOrWhiteSpace(job.Company?.RecruiterName)) signals.Add($"Recruiter contact: {job.Company.RecruiterName.Trim()}");
            if (!string.IsNullOrWhiteSpace(job.Company?.RecruiterEmail)) signals.Add($"Recruiter email on file: {job.Company.RecruiterEmail.Trim()}");
            if (lastMessage is not null)
            {
                signals.Add($"Latest correspondence: {lastMessage.Date:yyyy-MM-dd} — {lastMessage.Subject ?? "(no subject)"}");
            }
            if (correspondenceContext?.Participants.Count > 0)
            {
                signals.Add($"Thread participants: {string.Join(", ", correspondenceContext.Participants.Take(3))}");
            }
            if (!string.IsNullOrWhiteSpace(savedPackageMaterial.CoverLetterText)) signals.Add("Saved cover letter available");
            if (!string.IsNullOrWhiteSpace(savedPackageMaterial.RecruiterMessageDraft)) signals.Add("Saved recruiter message available");
            if (!string.IsNullOrWhiteSpace(savedPackageMaterial.TailoredCvText)) signals.Add("Saved tailored CV available");
            if (!string.IsNullOrWhiteSpace(savedApplicationAnswer)) signals.Add("Saved application answer available");

            if (correspondenceContext is not null)
            {
                foreach (var signal in correspondenceContext.Signals)
                {
                    if (!signals.Contains(signal, StringComparer.OrdinalIgnoreCase)) signals.Add(signal);
                }
            }

            return signals.Take(6).ToList();
        }

        public static bool IsExtractableAttachmentExtension(string? extension)
        {
            return extension?.Trim().ToLowerInvariant() switch
            {
                ".pdf" => true,
                ".docx" => true,
                ".txt" => true,
                ".md" => true,
                ".png" => true,
                ".jpg" => true,
                ".jpeg" => true,
                ".webp" => true,
                _ => false,
            };
        }

        public static List<string> BuildFollowUpApproach(string status, List<string> matchedTags, List<string> missingTags)
        {
            var normalized = (status ?? string.Empty).Trim();
            var advice = new List<string>();

            switch (normalized)
            {
                case "Applied":
                    advice.Add("Follow up briefly, reaffirm interest, and reference the date you applied.");
                    advice.Add("Mention one or two of the strongest overlaps from the posting instead of repeating your whole background.");
                    break;
                case "Waiting":
                    advice.Add("Acknowledge that you are following up on next steps and keep the message light but specific.");
                    advice.Add("Use one proof point that shows why you remain a strong fit.");
                    break;
                case "Interview":
                case "Interviewing":
                    advice.Add("Focus on momentum, appreciation, and readiness for the next step.");
                    advice.Add("Reference a memorable point from the process, discussion, or role priorities if possible.");
                    break;
                case "Offer":
                    advice.Add("Keep the tone warm and professional, and focus on clarifying next steps or timing.");
                    advice.Add("Avoid sounding pushy; frame the note around alignment and practical progress.");
                    break;
                case "Rejected":
                    advice.Add("If appropriate, ask for feedback with a respectful and concise tone.");
                    advice.Add("Keep the door open for future opportunities instead of arguing the decision.");
                    break;
                default:
                    advice.Add("Match the tone to the current stage and be specific about why you are following up now.");
                    advice.Add("Keep it concise, credible, and easy to respond to.");
                    break;
            }

            if (matchedTags.Any()) advice.Add($"Lead with relevant overlap such as {string.Join(", ", matchedTags.Take(2))}.");
            if (missingTags.Any()) advice.Add($"Do not overstate areas like {string.Join(", ", missingTags.Take(2))}; frame them honestly.");

            return advice.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList();
        }

        public static IEnumerable<string> SplitTags(string? s)
        {
            if (string.IsNullOrWhiteSpace(s)) yield break;

            var trimmed = s.Trim();

            List<string>? jsonTags = null;
            if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
            {
                try
                {
                    jsonTags = JsonSerializer.Deserialize<List<string>>(trimmed);
                }
                catch
                {
                    jsonTags = null;
                }
            }

            if (jsonTags is not null)
            {
                foreach (var x in jsonTags)
                {
                    var t = (x ?? string.Empty).Trim();
                    if (t.Length == 0) continue;
                    yield return t;
                }
                yield break;
            }

            foreach (var raw in trimmed.Split(new[] { ',', ';', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries))
            {
                var t = raw.Trim();
                if (t.Length == 0) continue;
                yield return t;
            }
        }

        public static string NormalizeForComparison(string value)
        {
            if (string.IsNullOrWhiteSpace(value)) return string.Empty;
            return new string(value.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());
        }

        public static string BuildSummarySource(JobApplication job)
        {
            // Prefer translated text for summaries and skill extraction so non-English
            // postings become easier to understand while keeping the original text intact.
            var parts = new[]
            {
                job.TranslatedDescription,
                job.Description,
                job.Notes
            };

            return string.Join("\n\n", parts.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()));
        }

        public static string? NormalizeTags(string? raw)
        {
            var normalized = SplitTags(raw)
                .Select(tag => tag.Trim())
                .Where(tag => tag.Length > 0)
                .GroupBy(tag => tag, StringComparer.OrdinalIgnoreCase)
                .Select(group =>
                {
                    var first = group.First();
                    return string.Join(" ", first.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(part => char.ToUpperInvariant(part[0]) + part[1..].ToLowerInvariant()));
                })
                .OrderBy(tag => tag, StringComparer.OrdinalIgnoreCase)
                .ToList();

            return normalized.Count == 0 ? null : JsonSerializer.Serialize(normalized);
        }

        public static string? NormalizeUrl(string? url)
        {
            if (string.IsNullOrWhiteSpace(url)) return null;
            var value = url.Trim();
            return Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri.ToString() : value;
        }

        public static string RemoveSavedApplicationAnswerDraft(string? notes)
        {
            var value = notes ?? string.Empty;
            if (string.IsNullOrWhiteSpace(value)) return string.Empty;

            while (true)
            {
                var startIndex = value.IndexOf(ApplicationAnswerDraftStart, StringComparison.Ordinal);
                var endIndex = value.IndexOf(ApplicationAnswerDraftEnd, StringComparison.Ordinal);
                if (startIndex < 0 || endIndex <= startIndex) break;
                var before = value[..startIndex].Trim();
                var after = value[(endIndex + ApplicationAnswerDraftEnd.Length)..].Trim();
                value = string.Join("\n\n", new[] { before, after }.Where(part => !string.IsNullOrWhiteSpace(part))).Trim();
            }

            const string legacyPrefix = "Application answer draft:";
            var legacyIndex = value.IndexOf(legacyPrefix, StringComparison.OrdinalIgnoreCase);
            if (legacyIndex >= 0)
            {
                return value[..legacyIndex].Trim();
            }

            return value.Trim();
        }

        public static bool HasInterviewPrepNotes(string? notes) => !string.IsNullOrWhiteSpace(RemoveSavedApplicationAnswerDraft(notes));

        public static bool IsInterviewStage(string status) =>
            status.Contains("Interview", StringComparison.OrdinalIgnoreCase);

        public static bool IsActiveWorkflowStatus(string status)
        {
            var normalized = (status ?? string.Empty).Trim();
            return normalized switch
            {
                "Applied" => true,
                "Waiting" => true,
                "Interview" => true,
                "Interviewing" => true,
                "Offer" => true,
                _ => false,
            };
        }

        public static WorkflowSignalDto BuildWorkflowSignal(JobApplication job, FollowUpDecision followUpDecision)
        {
            var hasTailoredCv = !string.IsNullOrWhiteSpace(job.TailoredCvText);
            var hasSavedApplicationAnswerDraft = !string.IsNullOrWhiteSpace(ExtractSavedApplicationAnswerDraft(job.Notes));
            var hasInterviewPrepNotes = HasInterviewPrepNotes(job.Notes);
            var needsInterviewPrep = IsInterviewStage(job.Status) && !hasInterviewPrepNotes;
            var hasPackageGap = IsActiveWorkflowStatus(job.Status) && (!hasTailoredCv || !hasSavedApplicationAnswerDraft);
            var needsFollowUpAction = followUpDecision.NeedsFollowUp || (!job.ResponseReceived && job.FollowUpAt is null);

            if (needsInterviewPrep)
            {
                return new WorkflowSignalDto(
                    ActionKey: "interview-prep",
                    Reason: "Interview stage reached but prep notes are still missing.",
                    WorkspaceTab: "interview-prep",
                    FollowMode: null,
                    NeedsAttention: true,
                    HasPackageGap: hasPackageGap,
                    NeedsInterviewPrep: true,
                    NeedsFollowUpAction: needsFollowUpAction,
                    HasTailoredCv: hasTailoredCv,
                    HasSavedApplicationAnswerDraft: hasSavedApplicationAnswerDraft,
                    HasInterviewPrepNotes: hasInterviewPrepNotes);
            }

            if (hasPackageGap)
            {
                var reason = !hasTailoredCv && !hasSavedApplicationAnswerDraft
                    ? "Tailored CV and saved application answers still need work."
                    : !hasTailoredCv
                        ? "Tailored CV missing for this role."
                        : "Saved application answers still need work.";

                return new WorkflowSignalDto(
                    ActionKey: "package-work",
                    Reason: reason,
                    WorkspaceTab: "tailored-cv",
                    FollowMode: null,
                    NeedsAttention: true,
                    HasPackageGap: true,
                    NeedsInterviewPrep: needsInterviewPrep,
                    NeedsFollowUpAction: needsFollowUpAction,
                    HasTailoredCv: hasTailoredCv,
                    HasSavedApplicationAnswerDraft: hasSavedApplicationAnswerDraft,
                    HasInterviewPrepNotes: hasInterviewPrepNotes);
            }

            if (needsFollowUpAction)
            {
                var reason = !string.IsNullOrWhiteSpace(followUpDecision.Reason)
                    ? followUpDecision.Reason!
                    : !job.ResponseReceived && job.FollowUpAt is null
                        ? "No response yet and no follow-up is scheduled."
                        : "Follow-up is due for this role.";

                return new WorkflowSignalDto(
                    ActionKey: "follow-up",
                    Reason: reason,
                    WorkspaceTab: "follow-up",
                    FollowMode: "waiting-update",
                    NeedsAttention: true,
                    HasPackageGap: hasPackageGap,
                    NeedsInterviewPrep: needsInterviewPrep,
                    NeedsFollowUpAction: true,
                    HasTailoredCv: hasTailoredCv,
                    HasSavedApplicationAnswerDraft: hasSavedApplicationAnswerDraft,
                    HasInterviewPrepNotes: hasInterviewPrepNotes);
            }

            return new WorkflowSignalDto(
                ActionKey: "review-readiness",
                Reason: "No urgent workflow gaps are blocking this job right now.",
                WorkspaceTab: "readiness",
                FollowMode: null,
                NeedsAttention: false,
                HasPackageGap: hasPackageGap,
                NeedsInterviewPrep: needsInterviewPrep,
                NeedsFollowUpAction: needsFollowUpAction,
                HasTailoredCv: hasTailoredCv,
                HasSavedApplicationAnswerDraft: hasSavedApplicationAnswerDraft,
                HasInterviewPrepNotes: hasInterviewPrepNotes);
        }

        public static List<string> BuildReadinessReminders(JobApplication job, WorkflowSignalDto workflowSignal)
        {
            var reminders = new List<string>();

            if (workflowSignal.HasPackageGap)
            {
                reminders.Add(workflowSignal.HasTailoredCv
                    ? "Saved application answers are still missing from the package."
                    : workflowSignal.HasSavedApplicationAnswerDraft
                        ? "This role is active but still missing a tailored CV."
                        : "This role is active but still needs a tailored CV and saved application answers.");
            }

            if (workflowSignal.NeedsInterviewPrep)
            {
                reminders.Add("Interview stage reached but prep notes are still missing.");
            }

            if (workflowSignal.NeedsFollowUpAction)
            {
                reminders.Add(job.FollowUpAt is null
                    ? "No response yet and no follow-up is scheduled."
                    : workflowSignal.Reason);
            }

            return reminders
                .Where(reminder => !string.IsNullOrWhiteSpace(reminder))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }
    }
}
