using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using JobTrackerApi.Data;
using JobTrackerApi.Services;
using JobTrackerApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Controllers;

[ApiController]
[Route("api/profile-cv")]
[Authorize(AuthenticationSchemes = "local")]
public sealed partial class ProfileCvController : ControllerBase
{
    private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".txt",
        ".md",
        ".pdf",
        ".docx",
        ".png",
        ".jpg",
        ".jpeg",
        ".webp",
    };

    private static readonly Dictionary<string, string> SectionAliases = new(StringComparer.OrdinalIgnoreCase)
    {
        ["professional summary"] = "Professional Summary",
        ["summary"] = "Professional Summary",
        ["profile"] = "Professional Summary",
        ["about me"] = "Professional Summary",
        ["contact"] = "Contact",
        ["contact details"] = "Contact",
        ["core skills"] = "Skills",
        ["skills"] = "Skills",
        ["technical skills"] = "Skills",
        ["technologies"] = "Skills",
        ["tech stack"] = "Skills",
        ["competencies"] = "Skills",
        ["core competencies"] = "Skills",
        ["experience"] = "Work Experience",
        ["professional experience"] = "Work Experience",
        ["career history"] = "Work Experience",
        ["work history"] = "Work Experience",
        ["experience highlights"] = "Work Experience",
        ["work experience"] = "Work Experience",
        ["employment history"] = "Work Experience",
        ["selected achievements"] = "Selected Achievements",
        ["achievements"] = "Selected Achievements",
        ["projects"] = "Projects",
        ["selected projects"] = "Projects",
        ["education"] = "Education",
        ["qualifications"] = "Education",
        ["certifications"] = "Certifications",
        ["certificates"] = "Certifications",
        ["languages"] = "Languages",
        ["interests"] = "Interests",
        ["hobbies"] = "Interests",
        ["awards"] = "Awards",
        ["honours"] = "Awards",
        ["honors"] = "Awards",
        ["publications"] = "Publications",
        ["organisations"] = "Organisations",
        ["organizations"] = "Organisations",
        ["memberships"] = "Organisations",
        ["references"] = "References",
        ["courses"] = "Courses",
        ["training"] = "Courses",
        ["volunteer experience"] = "Volunteer Experience",
        ["volunteering"] = "Volunteer Experience",
        ["additional experience"] = "Additional Experience",
        ["declaration"] = "Declaration",
        ["sammendrag"] = "Professional Summary",
        ["profil"] = "Professional Summary",
        ["kjernekompetanse"] = "Skills",
        ["tekniske ferdigheter"] = "Skills",
        ["teknisk kompetanse"] = "Skills",
        ["arbeidserfaring"] = "Work Experience",
        ["yrkeserfaring"] = "Work Experience",
        ["utvalgte prosjekter"] = "Projects",
        ["prosjekter"] = "Projects",
        ["tilleggserfaring"] = "Additional Experience",
        ["utdanning"] = "Education",
        ["sertifiseringer"] = "Certifications",
        ["språk"] = "Languages",
        ["interesser"] = "Interests",
        ["referanser"] = "References",
        ["lenker"] = "Links",
    };

    private const long MaxFileSizeBytes = 5 * 1024 * 1024;
    private const int ExtractionRunRetentionCount = 20;
    private const string ParserVersion = "m005-s03";
    private const string NormalizerVersion = "m005-s03";
    private const string LlmPromptVersion = "m005-s01";

    private readonly UserManager<ApplicationUser> _users;
    private readonly ISummarizerService _aiService;
    private readonly ICvAiClassifier _cvAiClassifier;
    private readonly ICvAiNormalizer _cvAiNormalizer;
    private readonly JobTrackerContext _db;
    private readonly AppPaths _paths;
    private readonly ILogger<ProfileCvController> _logger;
    private readonly ICvTemplateRenderer _cvTemplateRenderer;
    private readonly ICvPdfExporter _cvPdfExporter;
    private readonly ICvProcessingQueue _cvProcessingQueue;
    private readonly IAppEmailSender _emailSender;
    private readonly ICareerProfileService _careerProfileService;
    private readonly ICvProfileDiffService _cvProfileDiffService;

    public ProfileCvController(UserManager<ApplicationUser> users, ISummarizerService aiService, JobTrackerContext db, AppPaths paths, ILogger<ProfileCvController>? logger = null, ICvAiClassifier? cvAiClassifier = null, ICvAiNormalizer? cvAiNormalizer = null, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, ICvProcessingQueue? cvProcessingQueue = null, IAppEmailSender? emailSender = null, ICareerProfileService? careerProfileService = null, ICvProfileDiffService? cvProfileDiffService = null)
    {
        _users = users;
        _aiService = aiService;
        _cvAiClassifier = cvAiClassifier ?? NoOpCvAiClassifier.Instance;
        _cvAiNormalizer = cvAiNormalizer ?? NoOpCvAiNormalizer.Instance;
        _db = db;
        _paths = paths;
        _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<ProfileCvController>.Instance;
        _cvTemplateRenderer = cvTemplateRenderer ?? new CvTemplateRenderer();
        _cvPdfExporter = cvPdfExporter ?? new ThrowingCvPdfExporter();
        _cvProcessingQueue = cvProcessingQueue ?? NoOpCvProcessingQueue.Instance;
        _emailSender = emailSender ?? NoOpEmailSender.Instance;
        _careerProfileService = careerProfileService ?? new CareerProfileService(db);
        _cvProfileDiffService = cvProfileDiffService ?? new CvProfileDiffService();
    }

    private sealed class NoOpEmailSender : IAppEmailSender
    {
        public static readonly NoOpEmailSender Instance = new();
        public Task SendAsync(string toEmail, string subject, string bodyText, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }

    private sealed class ThrowingCvPdfExporter : ICvPdfExporter
    {
        public Task<CvPdfArtifact> ExportAsync(string ownerUserId, TailoredCvRenderResult renderResult, CancellationToken cancellationToken)
        {
            throw new InvalidOperationException("CV PDF export is not configured for this controller instance.");
        }
    }

    public sealed class RewriteSectionRequest
    {
        public string? SectionName { get; set; }
        public string? Style { get; set; }
        public string? TargetRole { get; set; }
        public JsonElement? JobApplicationId { get; set; }
        public string? TemplateId { get; set; }
        public string? SourceText { get; set; }
        public string? PromptBackground { get; set; }
        public string? Tone { get; set; }
        public string? Language { get; set; }
    }
    // ParseCvRequest / CvTemplateDescriptor / ProfileCvPreviewDto / CvRewriteFailureDto are
    // defined in ProfileCvDtos.cs on main (extracted after this branch forked). The branch's
    // in-file copies are dropped here to avoid duplicate definitions. The LayoutFamily/AtsRating
    // fields the branch added to CvTemplateDescriptor belong to the deferred ATS-badge work
    // (Phase 4), not this foundation integration.
    public sealed record AcceptCvRunRequest(List<string>? AcceptedLowConfidenceIds);
    private sealed record ExtractionPipelineResult(string RawText, string NormalizedText, StructuredCvProfile StructuredCv);
    private sealed record ClassifiedCvBlock(int Index, string OriginalBlock, string SectionName, string Content, CvBlockClassificationResult? Classification);

    [HttpPost("upload")]
    [Authorize(Policy = ProEntitlement.Policy)]
    [RequestSizeLimit(MaxFileSizeBytes)]
    public async Task<IActionResult> Upload([FromForm] IFormFile file)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();
        if (file is null || file.Length == 0) return BadRequest("Select a CV file to upload.");
        if (file.Length > MaxFileSizeBytes) return BadRequest("CV import file is too large. Keep it under 5 MB.");

        var extension = Path.GetExtension(file.FileName ?? string.Empty);
        if (!AllowedExtensions.Contains(extension))
        {
            return BadRequest("Only .txt, .md, .pdf, .docx, .png, .jpg, .jpeg, and .webp CV imports are supported right now.");
        }

        var artifact = await SaveUploadArtifactAsync(user, file, HttpContext.RequestAborted);
        var activeRun = await FindActiveRunAsync(user.Id, "upload", null, artifact.Sha256, HttpContext.RequestAborted);
        if (activeRun is not null)
        {
            TryDeleteCvArtifactFile(artifact.StoragePath);
            return await EnqueueRunAsync(activeRun, HttpContext.RequestAborted);
        }
        _db.CvUploadArtifacts.Add(artifact);
        await _db.SaveChangesAsync(HttpContext.RequestAborted);

        var run = new CvExtractionRun
        {
            OwnerUserId = user.Id,
            ArtifactId = artifact.Id,
            Trigger = "upload",
            ParserVersion = ParserVersion,
            NormalizerVersion = NormalizerVersion,
            LlmPromptVersion = LlmPromptVersion,
            Status = "queued",
            StartedAtUtc = DateTimeOffset.UtcNow,
        };
        _db.CvExtractionRuns.Add(run);
        await _db.SaveChangesAsync(HttpContext.RequestAborted);
        return await EnqueueRunAsync(run, HttpContext.RequestAborted);
    }

    [HttpGet("runs")]
    public async Task<ActionResult<IEnumerable<CvExtractionRunListItem>>> GetRuns()
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var runsQuery = _db.CvExtractionRuns
            .AsNoTracking()
            .Where(x => x.OwnerUserId == user.Id)
            .Select(x => new CvExtractionRunListItem(
                x.Id,
                x.Trigger,
                x.Status,
                x.Artifact != null ? x.Artifact.OriginalFileName : null,
                x.StartedAtUtc,
                x.CompletedAtUtc,
                x.AppliedAtUtc,
                x.ParserVersion,
                x.NormalizerVersion,
                x.LlmPromptVersion,
                x.ErrorMessage,
                null));
        var runs = _db.Database.IsSqlite()
            ? (await runsQuery.ToListAsync(HttpContext.RequestAborted)).OrderByDescending(x => x.StartedAtUtc).Take(10).ToList()
            : await runsQuery.OrderByDescending(x => x.StartedAtUtc).Take(10).ToListAsync(HttpContext.RequestAborted);

        var runIds = runs.Select(run => run.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToList();
        var operations = await _db.UserOperations.AsNoTracking()
            .Where(operation => operation.TaskType == CvProcessingQueue.TaskType && operation.SubjectId != null && runIds.Contains(operation.SubjectId))
            .ToListAsync(HttpContext.RequestAborted);
        var latestOperations = operations
            .GroupBy(operation => operation.SubjectId!, StringComparer.Ordinal)
            .ToDictionary(group => group.Key, group => group.MaxBy(operation => operation.CreatedAtUtc)!);
        runs = runs.Select(run => latestOperations.TryGetValue(run.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), out var operation)
            ? run with { Operation = OperationDto.From(operation) }
            : run).ToList();

        return Ok(runs);
    }

    [HttpGet("runs/{id:int}/diff")]
    public async Task<IActionResult> GetRunDiff([FromRoute] int id)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var run = await _db.CvExtractionRuns.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == user.Id, HttpContext.RequestAborted);
        if (run is null) return NotFound();
        if (string.IsNullOrWhiteSpace(run.StructuredProfileJson)) return Conflict("This extraction run has no reviewable profile.");

        var current = await _careerProfileService.LoadStructuredAsync(user.Id, HttpContext.RequestAborted);
        var extracted = StructuredCvProfileJson.Deserialize(run.StructuredProfileJson);
        return Ok(new { runId = run.Id, run.Status, diff = _cvProfileDiffService.Diff(current, extracted) });
    }

    [HttpPost("runs/{id:int}/accept")]
    public async Task<IActionResult> AcceptRun([FromRoute] int id, [FromBody] AcceptCvRunRequest? request = null)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var run = await _db.CvExtractionRuns.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == user.Id, HttpContext.RequestAborted);
        if (run is null) return NotFound();
        if (run.Status != "pending_review") return Conflict("This extraction run is not awaiting review.");
        if (string.IsNullOrWhiteSpace(run.StructuredProfileJson)) return Conflict("This extraction run has no reviewable profile.");

        var current = await _careerProfileService.LoadStructuredAsync(user.Id, HttpContext.RequestAborted);
        var extracted = StructuredCvProfileJson.Deserialize(run.StructuredProfileJson);
        var diff = _cvProfileDiffService.Diff(current, extracted);
        var acceptedLowConfidenceIds = (request?.AcceptedLowConfidenceIds ?? new List<string>()).ToHashSet(StringComparer.Ordinal);
        var merged = _cvProfileDiffService.Merge(current, extracted, acceptedLowConfidenceIds);
        merged.Metadata.ProfileVersion = (user.CurrentCvProfileVersion ?? 0) + 1;
        merged.Metadata.AppliedExtractionRunId = run.Id;
        merged.Metadata.UpdatedAtUtc = DateTimeOffset.UtcNow;
        await _careerProfileService.SaveVersionAsync(user.Id, merged, $"{run.Trigger}:accepted", HttpContext.RequestAborted);

        user.ProfileCvStructureJson = StructuredCvProfileJson.SerializePersisted(merged);
        if (string.IsNullOrWhiteSpace(user.ProfileCvText)) user.ProfileCvText = run.NormalizedText;
        user.CurrentCvExtractionRunId = run.Id;
        user.CurrentCvProfileVersion = merged.Metadata.ProfileVersion;
        if (run.ArtifactId.HasValue) user.CurrentCvUploadArtifactId = run.ArtifactId.Value;

        run.Status = "applied";
        run.AppliedAtUtc = DateTimeOffset.UtcNow;
        var update = await _users.UpdateAsync(user);
        if (!update.Succeeded) return BadRequest(string.Join("; ", update.Errors.Select(e => e.Description)));
        await _db.SaveChangesAsync(HttpContext.RequestAborted);
        return Ok(new { runId = run.Id, run.Status, diff, structuredCv = merged });
    }

    [HttpPost("runs/{id:int}/discard")]
    public async Task<IActionResult> DiscardRun([FromRoute] int id)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var run = await _db.CvExtractionRuns.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == user.Id, HttpContext.RequestAborted);
        if (run is null) return NotFound();
        if (run.Status != "pending_review") return Conflict("This extraction run is not awaiting review.");

        run.Status = "discarded";
        await _db.SaveChangesAsync(HttpContext.RequestAborted);
        return NoContent();
    }

    [HttpPost("reprocess")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<IActionResult> Reprocess()
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var artifactQuery = _db.CvUploadArtifacts.AsNoTracking().Where(x => x.OwnerUserId == user.Id);
        var artifact = _db.Database.IsSqlite()
            ? (await artifactQuery.ToListAsync(HttpContext.RequestAborted)).MaxBy(x => x.UploadedAtUtc)
            : await artifactQuery.OrderByDescending(x => x.UploadedAtUtc).FirstOrDefaultAsync(HttpContext.RequestAborted);

        if (artifact is null) return BadRequest("Upload a CV before reprocessing it.");
        if (string.IsNullOrWhiteSpace(artifact.StoragePath) || !System.IO.File.Exists(artifact.StoragePath))
        {
            return BadRequest("The stored CV artifact could not be found for reprocessing.");
        }

        var run = await CreateQueuedRunAsync(user.Id, artifact.Id, "reprocess", HttpContext.RequestAborted);
        return await EnqueueRunAsync(run, HttpContext.RequestAborted);
    }

    [HttpPost("rebuild")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<IActionResult> Rebuild()
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();
        if (string.IsNullOrWhiteSpace(user.ProfileCvText)) return BadRequest("Add or import CV text before rebuilding it.");

        var run = await CreateQueuedRunAsync(user.Id, user.CurrentCvUploadArtifactId, "rebuild", HttpContext.RequestAborted);
        return await EnqueueRunAsync(run, HttpContext.RequestAborted);
    }

    [HttpPost("rewrite-section")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<IActionResult> RewriteSection([FromBody] RewriteSectionRequest request)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var structuredCv = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
        var sourceText = string.IsNullOrWhiteSpace(request.SourceText)
            ? (string.IsNullOrWhiteSpace(user.ProfileCvText) ? null : user.ProfileCvText.Trim())
            : request.SourceText.Trim();
        if (string.IsNullOrWhiteSpace(sourceText) && structuredCv.Sections.Count == 0)
        {
            return BadRequest("Add or import CV text before rewriting your CV.");
        }

        var sectionName = NormalizeRewriteSectionName(request.SectionName);
        var style = string.IsNullOrWhiteSpace(request.Style) ? "ats-minimal" : request.Style.Trim();
        var templateId = NormalizeTemplateId(request.TemplateId ?? style);
        var targetRole = string.IsNullOrWhiteSpace(request.TargetRole) ? null : request.TargetRole.Trim();
        var tone = string.IsNullOrWhiteSpace(request.Tone) ? null : request.Tone.Trim();
        var language = string.IsNullOrWhiteSpace(request.Language) ? null : request.Language.Trim();
        var promptBackground = string.IsNullOrWhiteSpace(request.PromptBackground) ? null : request.PromptBackground.Trim();
        var jobApplicationId = ParseFlexibleNullableInt(request.JobApplicationId);
        var jobContext = jobApplicationId.HasValue
            ? await _db.JobApplications
                .AsNoTracking()
                .Include(job => job.Company)
                .Where(job => job.Id == jobApplicationId.Value && job.OwnerUserId == user.Id)
                .Select(job => new
                {
                    job.Id,
                    job.JobTitle,
                    job.Description,
                    job.TranslatedDescription,
                    job.ShortSummary,
                    job.Notes,
                    job.JobUrl,
                    job.Status,
                    CompanyName = job.Company != null ? job.Company.Name : null,
                    RecruiterName = job.Company != null ? job.Company.RecruiterName : null,
                    RecruiterEmail = job.Company != null ? job.Company.RecruiterEmail : null
                })
                .FirstOrDefaultAsync(HttpContext.RequestAborted)
            : null;

        var effectiveTargetRole = targetRole ?? jobContext?.JobTitle;
        var rewriteSource = BuildRewriteSourceText(sectionName, sourceText, structuredCv);
        var templateGuidance = DescribeRewriteTemplate(templateId);
        var roleGuidance = jobContext is not null
            ? $"Target this toward the saved job '{jobContext.JobTitle}' at '{jobContext.CompanyName ?? "Unknown company"}'. Use the full job record below to sharpen wording without inventing facts.\nJob status: {jobContext.Status}\nJob summary: {jobContext.ShortSummary ?? "-"}\nJob description: {jobContext.Description ?? "-"}\nTranslated description: {jobContext.TranslatedDescription ?? "-"}\nNotes: {jobContext.Notes ?? "-"}\nJob URL: {jobContext.JobUrl ?? "-"}\nRecruiter name: {jobContext.RecruiterName ?? "-"}\nRecruiter email: {jobContext.RecruiterEmail ?? "-"}"
            : effectiveTargetRole is not null
                ? $"Target role: {effectiveTargetRole}. Keep it broadly reusable but clearly aligned to that role family."
                : "Keep it broadly reusable for future tailoring.";
        var toneGuidance = tone is not null ? $"Tone guidance: {tone}." : "Tone guidance: confident, professional, concise, and factual.";
        var languageGuidance = language is not null ? $"Write the CV in {language}." : "Write the CV in English unless the source clearly requires another language.";
        var backgroundGuidance = promptBackground is not null ? $"Candidate background and emphasis: {promptBackground}" : string.Empty;

        var subject = sectionName is null ? "this CV" : $"the '{sectionName}' section of this CV";
        var instruction = $"Rewrite only {subject}. Preserve facts, avoid inventing employers, titles, qualifications, dates, locations, salaries, or metrics. Style guidance: {style}. Template direction: {templateGuidance}. {roleGuidance} {toneGuidance} {languageGuidance} {backgroundGuidance} Return only the rewritten CV text with clean headings and strong bullet phrasing when useful.";
        var rewritten = await _aiService.SummarizeSectionAsync(
            instruction,
            rewriteSource,
            sectionName is null ? 1800 : 900,
            sectionName is null ? 400 : 180);

        if (string.IsNullOrWhiteSpace(rewritten))
        {
            var metrics = await _aiService.GetMetricsAsync(HttpContext.RequestAborted);
            var detail = metrics.Healthy
                ? "The rewrite request reached the AI service, but it returned no usable text."
                : "The AI rewrite service is unavailable or not ready.";
            var failureCode = metrics.Healthy ? "rewrite-empty" : "ai-service-unavailable";
            var message = metrics.Healthy
                ? "The AI service returned an empty CV rewrite."
                : "The AI service could not rewrite your CV right now.";

            _logger.LogWarning("CV rewrite returned empty output. Section={SectionName} Template={TemplateId} TargetRole={TargetRole} JobApplicationId={JobApplicationId} HasSourceText={HasSourceText} StructuredSections={StructuredSectionCount} AiHealthy={AiHealthy} AiLastError={AiLastError}",
                sectionName ?? "<whole-cv>", templateId, effectiveTargetRole ?? "<none>", jobApplicationId, !string.IsNullOrWhiteSpace(sourceText), structuredCv.Sections.Count, metrics.Healthy, metrics.LastError ?? "<none>");

            return StatusCode(StatusCodes.Status502BadGateway, new CvRewriteFailureDto(
                failureCode,
                message,
                detail,
                metrics.LastError));
        }

        return Ok(new
        {
            sectionName,
            style,
            templateId,
            targetRole = effectiveTargetRole,
            jobApplicationId = jobContext?.Id,
            text = rewritten.Trim()
        });
    }

    [HttpGet("templates")]
    public ActionResult<IEnumerable<CvTemplateDescriptor>> GetTemplates()
    {
        return Ok(GetCvTemplateDescriptors());
    }

    [HttpPost("rewrite-preview")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<ActionResult<ProfileCvPreviewDto>> BuildRewritePreview([FromBody] RewriteSectionRequest request)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var structuredCv = StructuredCvProfileJson.DeserializePersisted(user.ProfileCvStructureJson);
        var sourceText = string.IsNullOrWhiteSpace(request.SourceText)
            ? (string.IsNullOrWhiteSpace(user.ProfileCvText) ? null : user.ProfileCvText.Trim())
            : request.SourceText.Trim();
        if (string.IsNullOrWhiteSpace(sourceText) && structuredCv.Sections.Count == 0)
        {
            return BadRequest("Add or import CV text before rewriting your CV.");
        }

        var sectionName = NormalizeRewriteSectionName(request.SectionName);
        var style = string.IsNullOrWhiteSpace(request.Style) ? "ats-minimal" : request.Style.Trim();
        var templateId = NormalizeTemplateId(request.TemplateId ?? style);
        var jobApplicationId = ParseFlexibleNullableInt(request.JobApplicationId);
        var job = jobApplicationId.HasValue
            ? await _db.JobApplications.AsNoTracking().Include(job => job.Company)
                .FirstOrDefaultAsync(job => job.Id == jobApplicationId.Value && job.OwnerUserId == user.Id, HttpContext.RequestAborted)
            : null;
        var effectiveTargetRole = string.IsNullOrWhiteSpace(request.TargetRole)
            ? job?.JobTitle
            : request.TargetRole.Trim();

        var rewriteResult = await RewriteSection(request);
        if (rewriteResult is not OkObjectResult ok) return StatusCode((rewriteResult as ObjectResult)?.StatusCode ?? 500, (rewriteResult as ObjectResult)?.Value);

        var rewrittenText = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value)).RootElement.GetProperty("text").GetString()?.Trim() ?? string.Empty;
        var baseText = string.IsNullOrWhiteSpace(sourceText)
            ? string.Join("\n\n", structuredCv.Sections.Select(section => $"## {section.Name}\n{section.Content}"))
            : sourceText!;
        var fullText = sectionName is null ? rewrittenText : ReplaceOrAppendCvSection(baseText, sectionName, rewrittenText);
        var previewStructured = await BuildStructuredCvAsync(fullText, HttpContext.RequestAborted);
        var document = BuildMasterCvDocument(previewStructured, templateId, effectiveTargetRole, job?.JobTitle, job?.Company?.Name);
        var rendered = RenderProfileCv(document, user, effectiveTargetRole ?? user.DisplayName ?? "General CV", job?.Company?.Name);

        return Ok(new ProfileCvPreviewDto(rendered.TemplateId, rendered.Html, rendered.SuggestedFileName, fullText, rewrittenText, sectionName, previewStructured, document, effectiveTargetRole, job?.Id));
    }

    [HttpPost("export-pdf")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<IActionResult> ExportProfileCvPdf([FromBody] RewriteSectionRequest request, CancellationToken cancellationToken)
    {
        var previewResult = await BuildRewritePreview(request);
        if (previewResult.Result is ObjectResult errorResult && errorResult.StatusCode >= 400)
        {
            return StatusCode(errorResult.StatusCode ?? 500, errorResult.Value);
        }

        var ok = previewResult.Result as OkObjectResult;
        if (ok?.Value is not ProfileCvPreviewDto preview)
        {
            return StatusCode(StatusCodes.Status500InternalServerError, "The CV preview could not be prepared for PDF export.");
        }

        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();
        var artifact = await _cvPdfExporter.ExportAsync(user.Id, new TailoredCvRenderResult(preview.TemplateId, preview.SuggestedFileName, preview.Html), cancellationToken);
        return File(artifact.Bytes, "application/pdf", artifact.FileName);
    }

    [HttpPost("parse")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<ActionResult<object>> Parse([FromBody] ParseCvRequest? request)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var source = string.IsNullOrWhiteSpace(request?.Text) ? user.ProfileCvText : request!.Text;
        if (string.IsNullOrWhiteSpace(source)) return BadRequest("Add or import CV text before parsing sections.");

        var normalizedSource = await MaybeReconstructStructuredCvAsync(source, HttpContext.RequestAborted);
        var structuredCv = await BuildStructuredCvAsync(normalizedSource, HttpContext.RequestAborted);
        if (string.IsNullOrWhiteSpace(request?.Text))
        {
            user.ProfileCvText = normalizedSource;
        }
        await ApplyTextExtractionRunAsync(user, "parse", source, normalizedSource, structuredCv, user.CurrentCvUploadArtifactId, HttpContext.RequestAborted);

        return Ok(new { structuredCv, sections = structuredCv.Sections, totalWords = CountWords(normalizedSource), extractionRunId = user.CurrentCvExtractionRunId, profileVersion = user.CurrentCvProfileVersion });
    }

    [HttpPost("improve")]
    [Authorize(Policy = ProEntitlement.Policy)]
    public async Task<IActionResult> Improve()
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();
        if (string.IsNullOrWhiteSpace(user.ProfileCvText)) return BadRequest("Add or import CV text before improving it.");

        var run = await CreateQueuedRunAsync(user.Id, user.CurrentCvUploadArtifactId, "improve", HttpContext.RequestAborted);
        return await EnqueueRunAsync(run, HttpContext.RequestAborted);
    }

    private static string BuildRewriteSourceText(string? sectionName, string? sourceText, StructuredCvProfile structuredCv)
    {
        if (string.IsNullOrWhiteSpace(sectionName))
        {
            return !string.IsNullOrWhiteSpace(sourceText)
                ? sourceText.Trim()
                : string.Join("\n\n", structuredCv.Sections.Select(section => $"## {section.Name}\n{section.Content}"));
        }

        var matchingSection = structuredCv.Sections.FirstOrDefault(section => string.Equals(section.Name, sectionName, StringComparison.OrdinalIgnoreCase));
        if (matchingSection is not null && !string.IsNullOrWhiteSpace(matchingSection.Content))
        {
            return $"## {matchingSection.Name}\n{matchingSection.Content}";
        }

        return !string.IsNullOrWhiteSpace(sourceText)
            ? sourceText.Trim()
            : string.Join("\n\n", structuredCv.Sections.Select(section => $"## {section.Name}\n{section.Content}"));
    }

    private static string DescribeRewriteTemplate(string templateId)
    {
        return TailoredCvTemplateCatalog.Resolve(templateId).RewriteGuidance;
    }

    private static string NormalizeTemplateId(string? value) => TailoredCvTemplateCatalog.NormalizeId(value);

    private static string? NormalizeRewriteSectionName(string? value)
    {
        var trimmed = value?.Trim();
        if (string.IsNullOrWhiteSpace(trimmed)) return null;
        return SectionAliases.TryGetValue(trimmed, out var canonical) ? canonical : trimmed;
    }

    private static int? ParseFlexibleNullableInt(JsonElement? value)
    {
        if (value is null) return null;
        if (value.Value.ValueKind == JsonValueKind.Number && value.Value.TryGetInt32(out var number)) return number;
        if (value.Value.ValueKind == JsonValueKind.String)
        {
            var raw = value.Value.GetString();
            if (int.TryParse(raw, out var parsed)) return parsed;
        }
        return null;
    }

    private static string ReplaceOrAppendCvSection(string source, string sectionName, string sectionDraft)
    {
        var trimmedSource = (source ?? string.Empty).Trim();
        var trimmedDraft = (sectionDraft ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(trimmedDraft)) return trimmedSource;
        if (string.IsNullOrWhiteSpace(trimmedSource)) return $"## {sectionName}\n{trimmedDraft}";

        var normalizedHeading = sectionName.Trim().ToLowerInvariant();
        var headingPattern = new Regex(@"^(##\s+|#\s+)?(?<name>[A-Z][A-Za-z &/]+):?\s*$", RegexOptions.Multiline);
        var matches = headingPattern.Matches(trimmedSource).ToList();
        var targetIndex = matches.FindIndex(match => string.Equals(match.Groups["name"].Value.Trim(), normalizedHeading, StringComparison.OrdinalIgnoreCase));
        if (targetIndex < 0)
        {
            return $"{trimmedSource}\n\n## {sectionName}\n{trimmedDraft}".Trim();
        }

        var start = matches[targetIndex].Index;
        var end = targetIndex + 1 < matches.Count ? matches[targetIndex + 1].Index : trimmedSource.Length;
        var before = trimmedSource[..start].TrimEnd();
        var after = trimmedSource[end..].TrimStart();
        return string.Join("\n\n", new[] { before, $"## {sectionName}\n{trimmedDraft}", after }.Where(part => !string.IsNullOrWhiteSpace(part))).Trim();
    }

    private static IReadOnlyList<CvTemplateDescriptor> GetCvTemplateDescriptors()
    {
        return TailoredCvTemplateCatalog.Templates
            .Select(template => new CvTemplateDescriptor(
                template.Id,
                template.Title,
                template.Tone,
                template.AccentColor,
                template.PreviewTagline,
                template.PreviewSummary,
                template.PreviewBullets.ToList()))
            .ToList();
    }

    private TailoredCvRenderResult RenderProfileCv(TailoredCvDocument document, ApplicationUser user, string targetRole, string? companyName)
    {
        var candidateName = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(value => !string.IsNullOrWhiteSpace(value)));
        if (string.IsNullOrWhiteSpace(candidateName)) candidateName = user.DisplayName?.Trim();
        if (string.IsNullOrWhiteSpace(candidateName)) candidateName = user.UserName?.Trim();
        if (string.IsNullOrWhiteSpace(candidateName)) candidateName = user.Email?.Trim();
        if (string.IsNullOrWhiteSpace(candidateName)) candidateName = "Your Name";
        return _cvTemplateRenderer.Render(document, document.TemplateId, candidateName!, targetRole, companyName, AvatarStorage.Resolve(user.AvatarImageDataUrl));
    }

}
