using System.Text.Json; using JobTrackerApi.Data; using JobTrackerApi.Models; namespace JobTrackerApi.Services; // Phase 3: two-way projection between the relational CareerProfile children and the // StructuredCvProfile shape (the derived blob + the frontend/AI edit unit). // See docs/architecture/career-profile-model.md. // // Relational holds: Experience, Education, Skill, Project, Certification, Language. // LongTailJson holds the rest of StructuredCvProfile: Contact, Summary, Interests, OtherSections, // Sections, Metadata (and future achievements/orgs/pubs/courses). public static class CareerProfileMapper { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); /// The non-relational parts of a StructuredCvProfile, stored as CareerProfile.LongTailJson. public sealed class CareerLongTail { public string Version { get; set; } = "1"; public StructuredCvMetadata Metadata { get; set; } = new(); public StructuredCvContact Contact { get; set; } = new(); public List Summary { get; set; } = new(); public List Interests { get; set; } = new(); public List Awards { get; set; } = new(); public List Publications { get; set; } = new(); public List Organisations { get; set; } = new(); public List References { get; set; } = new(); public List OtherSections { get; set; } = new(); public List Sections { get; set; } = new(); } public static string SerializeLongTail(StructuredCvProfile p) => JsonSerializer.Serialize(new CareerLongTail { Version = p.Version, Metadata = p.Metadata, Contact = p.Contact, Summary = p.Summary, Interests = p.Interests, Awards = p.Awards, Publications = p.Publications, Organisations = p.Organisations, References = p.References, OtherSections = p.OtherSections, Sections = p.Sections, }, JsonOptions); private static CareerLongTail DeserializeLongTail(string? json) { if (string.IsNullOrWhiteSpace(json)) return new CareerLongTail(); try { return JsonSerializer.Deserialize(json, JsonOptions) ?? new CareerLongTail(); } catch { return new CareerLongTail(); } } // --- StructuredCvProfile -> relational rows (insert) ------------------------------------------ public static void AddChildren(JobTrackerContext db, int careerProfileId, string ownerUserId, StructuredCvProfile p) { int order = 0; foreach (var j in p.Jobs) db.CareerExperiences.Add(new CareerExperience { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(j.Id), SortOrder = order++, Title = j.Title, Company = j.Company, Location = j.Location, Start = j.Start, End = j.End, StartDate = j.StartDate, EndDate = j.EndDate, IsCurrent = j.IsCurrent, Bullets = j.Bullets, Skills = j.Skills, }); order = 0; foreach (var e in p.Education) db.CareerEducations.Add(new CareerEducation { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(e.Id), SortOrder = order++, Qualification = e.Qualification, QualificationLevel = e.QualificationLevel, Institution = e.Institution, Location = e.Location, Start = e.Start, End = e.End, StartDate = e.StartDate, EndDate = e.EndDate, Details = e.Details, }); order = 0; foreach (var s in p.Skills) db.CareerSkills.Add(new CareerSkill { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(null), SortOrder = order++, Name = s, }); order = 0; foreach (var pr in p.Projects) db.CareerProjects.Add(new CareerProject { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(pr.Id), SortOrder = order++, Name = pr.Name, Role = pr.Role, Location = pr.Location, Start = pr.Start, End = pr.End, StartDate = pr.StartDate, EndDate = pr.EndDate, Bullets = pr.Bullets, Skills = pr.Skills, }); order = 0; foreach (var c in p.Certifications) db.CareerCertifications.Add(new CareerCertification { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(c.Id), SortOrder = order++, Name = c.Name, Issuer = c.Issuer, Location = c.Location, Date = c.Date, DateNormalized = c.DateNormalized, Details = c.Details, }); order = 0; foreach (var l in p.Languages) db.CareerLanguages.Add(new CareerLanguage { CareerProfileId = careerProfileId, OwnerUserId = ownerUserId, ItemKey = KeyOf(null), SortOrder = order++, Name = l.Name, Level = l.Level, Notes = l.Notes, }); } // --- relational rows -> StructuredCvProfile (read) ------------------------------------------- public static StructuredCvProfile ToStructured( string? longTailJson, List experiences, List education, List skills, List projects, List certifications, List languages) { var tail = DeserializeLongTail(longTailJson); return new StructuredCvProfile { Version = tail.Version, Metadata = tail.Metadata, Contact = tail.Contact, Summary = tail.Summary, Interests = tail.Interests, Awards = tail.Awards, Publications = tail.Publications, Organisations = tail.Organisations, References = tail.References, OtherSections = tail.OtherSections, Sections = tail.Sections, Jobs = experiences.Select(x => new StructuredCvJob { Id = NullIfEmpty(x.ItemKey), Title = x.Title, Company = x.Company, Location = x.Location, Start = x.Start, End = x.End, StartDate = x.StartDate, EndDate = x.EndDate, IsCurrent = x.IsCurrent, Bullets = x.Bullets, Skills = x.Skills, }).ToList(), Education = education.Select(x => new StructuredCvEducation { Id = NullIfEmpty(x.ItemKey), Qualification = x.Qualification, QualificationLevel = x.QualificationLevel, Institution = x.Institution, Location = x.Location, Start = x.Start, End = x.End, StartDate = x.StartDate, EndDate = x.EndDate, Details = x.Details, }).ToList(), Skills = skills.Select(x => x.Name ?? string.Empty).Where(x => x.Length > 0).ToList(), Projects = projects.Select(x => new StructuredCvProject { Id = NullIfEmpty(x.ItemKey), Name = x.Name, Role = x.Role, Location = x.Location, Start = x.Start, End = x.End, StartDate = x.StartDate, EndDate = x.EndDate, Bullets = x.Bullets, Skills = x.Skills, }).ToList(), Certifications = certifications.Select(x => new StructuredCvCertification { Id = NullIfEmpty(x.ItemKey), Name = x.Name, Issuer = x.Issuer, Location = x.Location, Date = x.Date, DateNormalized = x.DateNormalized, Details = x.Details, }).ToList(), Languages = languages.Select(x => new StructuredCvLanguage { Name = x.Name, Level = x.Level, Notes = x.Notes, }).ToList(), }; } private static string KeyOf(string? id) => string.IsNullOrWhiteSpace(id) ? Guid.NewGuid().ToString("N")[..12] : id; private static string? NullIfEmpty(string? s) => string.IsNullOrWhiteSpace(s) ? null : s; }