using System.Text.Json; using System.Text.Json.Serialization; namespace JobTrackerApi.Models; public static class TailoredCvDraftJson { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; public static TailoredCvDocument Empty() => Normalize(new TailoredCvDocument()); public static TailoredCvDocument FromDraft(TailoredCvDraft? draft) { if (draft is null) return Empty(); var document = new TailoredCvDocument { TemplateId = string.IsNullOrWhiteSpace(draft.TemplateId) ? "base" : draft.TemplateId.Trim(), Headline = TrimOrNull(draft.Headline), Summary = DeserializeList(draft.SummaryJson), SelectedSkills = DeserializeList(draft.SelectedSkillsJson), Experience = DeserializeList(draft.ExperienceJson) .Select(NormalizeExperience) .Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0) .ToList(), Education = DeserializeList(draft.EducationJson) .Select(NormalizeEducation) .Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0) .ToList(), CustomSections = DeserializeList(draft.CustomSectionsJson) .Select(NormalizeCustomSection) .Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0) .ToList(), RenderOptions = DeserializeObject(draft.RenderOptionsJson) ?? new TailoredCvRenderOptions(), }; return Normalize(document); } public static void ApplyToDraft(TailoredCvDraft draft, TailoredCvDocument? document) { var normalized = Normalize(document); draft.TemplateId = normalized.TemplateId; draft.Headline = TrimOrNull(normalized.Headline); draft.SummaryJson = JsonSerializer.Serialize(normalized.Summary, SerializerOptions); draft.SelectedSkillsJson = JsonSerializer.Serialize(normalized.SelectedSkills, SerializerOptions); draft.ExperienceJson = JsonSerializer.Serialize(normalized.Experience, SerializerOptions); draft.EducationJson = JsonSerializer.Serialize(normalized.Education, SerializerOptions); draft.CustomSectionsJson = JsonSerializer.Serialize(normalized.CustomSections, SerializerOptions); draft.RenderOptionsJson = JsonSerializer.Serialize(normalized.RenderOptions, SerializerOptions); } public static TailoredCvDocument Normalize(TailoredCvDocument? document) { document ??= new TailoredCvDocument(); document.TemplateId = string.IsNullOrWhiteSpace(document.TemplateId) ? "ats-minimal" : document.TemplateId.Trim(); document.Headline = TrimOrNull(document.Headline); document.Summary = CleanList(document.Summary); document.SelectedSkills = CleanList(document.SelectedSkills); document.Experience = (document.Experience ?? new List()) .Select(NormalizeExperience) .Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0) .ToList(); document.Education = (document.Education ?? new List()) .Select(NormalizeEducation) .Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0) .ToList(); document.CustomSections = (document.CustomSections ?? new List()) .Select(NormalizeCustomSection) .Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0) .ToList(); document.RenderOptions ??= new TailoredCvRenderOptions(); document.RenderOptions.PageMode = string.IsNullOrWhiteSpace(document.RenderOptions.PageMode) ? "one-page" : document.RenderOptions.PageMode.Trim(); document.RenderOptions.AccentColor = string.IsNullOrWhiteSpace(document.RenderOptions.AccentColor) ? "slate" : document.RenderOptions.AccentColor.Trim(); document.RenderOptions.BulletDensity = string.IsNullOrWhiteSpace(document.RenderOptions.BulletDensity) ? "balanced" : document.RenderOptions.BulletDensity.Trim(); document.RenderOptions.SectionOrder = CleanList(document.RenderOptions.SectionOrder); if (document.RenderOptions.SectionOrder.Count == 0) { document.RenderOptions.SectionOrder = new List { "summary", "skills", "experience", "education", "custom" }; } return document; } public static string RenderPlainText(TailoredCvDocument? document) { var normalized = Normalize(document); var lines = new List(); AddLine(lines, normalized.Headline); if (normalized.Summary.Count > 0) { AddBlock(lines, "Professional Summary", normalized.Summary.Select(item => $"- {item}")); } if (normalized.SelectedSkills.Count > 0) { AddBlock(lines, "Core Skills", normalized.SelectedSkills); } if (normalized.Experience.Count > 0) { var block = new List(); foreach (var item in normalized.Experience) { AddLine(block, item.Title); var meta = string.Join(" | ", new[] { item.Company, item.Location, FormatDateRange(item.Start, item.End, item.IsCurrent) }.Where(value => !string.IsNullOrWhiteSpace(value))); AddLine(block, meta); foreach (var bullet in item.Bullets) { AddLine(block, $"- {bullet}"); } AddLine(block, string.Empty); } AddBlock(lines, "Experience", block); } if (normalized.Education.Count > 0) { var block = new List(); foreach (var item in normalized.Education) { AddLine(block, item.Qualification); var meta = string.Join(" | ", new[] { item.Institution, item.Location, FormatDateRange(item.Start, item.End, false) }.Where(value => !string.IsNullOrWhiteSpace(value))); AddLine(block, meta); foreach (var detail in item.Details) { AddLine(block, $"- {detail}"); } AddLine(block, string.Empty); } AddBlock(lines, "Education", block); } foreach (var section in normalized.CustomSections) { AddBlock(lines, section.Title ?? "Additional Information", section.Items); } return string.Join("\n\n", lines.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim(); } private static TailoredCvExperienceItem NormalizeExperience(TailoredCvExperienceItem? item) { item ??= new TailoredCvExperienceItem(); item.Title = TrimOrNull(item.Title); item.Company = TrimOrNull(item.Company); item.Location = TrimOrNull(item.Location); item.Start = TrimOrNull(item.Start); item.End = TrimOrNull(item.End); item.Bullets = CleanList(item.Bullets); item.IsCurrent = item.IsCurrent || string.Equals(item.End, "present", StringComparison.OrdinalIgnoreCase) || string.Equals(item.End, "current", StringComparison.OrdinalIgnoreCase); return item; } private static TailoredCvEducationItem NormalizeEducation(TailoredCvEducationItem? item) { item ??= new TailoredCvEducationItem(); item.Qualification = TrimOrNull(item.Qualification); item.Institution = TrimOrNull(item.Institution); item.Location = TrimOrNull(item.Location); item.Start = TrimOrNull(item.Start); item.End = TrimOrNull(item.End); item.Details = CleanList(item.Details); return item; } private static TailoredCvCustomSection NormalizeCustomSection(TailoredCvCustomSection? item) { item ??= new TailoredCvCustomSection(); item.Title = TrimOrNull(item.Title); item.Items = CleanList(item.Items); return item; } private static List DeserializeList(string? json) { return DeserializeList(json) .Select(value => value?.Trim() ?? string.Empty) .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static List DeserializeList(string? json) { try { return string.IsNullOrWhiteSpace(json) ? new List() : JsonSerializer.Deserialize>(json, SerializerOptions) ?? new List(); } catch { return new List(); } } private static T? DeserializeObject(string? json) where T : class { try { return string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize(json, SerializerOptions); } catch { return null; } } private static List CleanList(IEnumerable? values) { return (values ?? Array.Empty()) .Select(value => value?.Trim() ?? string.Empty) .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static string? TrimOrNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private static string? FormatDateRange(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")}"; } private static void AddBlock(List lines, string title, IEnumerable body) { var content = string.Join("\n", body.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim(); if (string.IsNullOrWhiteSpace(content)) return; lines.Add($"{title}\n{content}"); } private static void AddLine(List lines, string? value) { if (!string.IsNullOrWhiteSpace(value)) lines.Add(value.Trim()); } }