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

namespace JobTrackerApi.Services;

// Phase 5.4 — Application Assets.
//
// Connects the career outputs the user already has to one job application. It owns almost nothing:
// CV variants stay in CvVariantService (a lens over the master CareerProfile), documents stay in
// Attachment, AI narrative stays in AiWorkspaceService. The only new state is the append-only cover
// letter history, because JobApplication.CoverLetterText had no way back from a bad rewrite.
//
// The flow is strictly one-directional — CareerProfile -> CvVariant -> application output. Nothing
// here writes upward: no method touches CareerProfile or its children.
// docs/architecture/application-workspace.md.
public sealed record ApplicationCvDto(
    int? AttachedVariantId,
    string? AttachedVariantName,
    string? AttachedThemeId,
    int? AttachedVersion,
    DateTimeOffset? AttachedUpdatedAtUtc,
    bool AttachedIsPublic,
    bool HasTailoredCvText,
    IReadOnlyList<CvVariantSummary> AvailableVariants);

public sealed record TailoringSuggestionDto(string Kind, string Title, string? Detail, IReadOnlyList<string> Items);

public sealed record TailoringPlanDto(
    bool HasJobDescription,
    bool HasCareerProfile,
    bool HasAttachedVariant,
    int MatchScore,
    IReadOnlyList<TailoringSuggestionDto> Suggestions,
    int AiSuggestionCount);

public sealed record CoverLetterVersionDto(int Version, string Source, string? AiAction, int Length, DateTimeOffset CreatedAtUtc, bool IsCurrent);

public sealed record CoverLetterDto(
    string? Text,
    int CurrentVersion,
    IReadOnlyList<CoverLetterVersionDto> Versions,
    int AiSuggestionCount);

public interface IApplicationAssetsService
{
    Task<ApplicationCvDto?> GetCvAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);
    Task<ApplicationCvDto?> AttachVariantAsync(string ownerUserId, int jobApplicationId, int? variantId, CancellationToken ct);
    Task<TailoringPlanDto?> GetTailoringPlanAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);

    Task<CoverLetterDto?> GetCoverLetterAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);
    Task<CoverLetterDto?> SaveCoverLetterAsync(string ownerUserId, int jobApplicationId, string? text, string source, string? aiAction, CancellationToken ct);
    Task<CoverLetterDto?> RestoreCoverLetterAsync(string ownerUserId, int jobApplicationId, int version, CancellationToken ct);
}

public sealed class ApplicationAssetsService : IApplicationAssetsService
{
    private readonly JobTrackerContext _db;
    private readonly ICvVariantService _variants;
    private readonly IApplicationIntelligenceService _intelligence;

    public ApplicationAssetsService(JobTrackerContext db, ICvVariantService variants, IApplicationIntelligenceService intelligence)
    {
        _db = db;
        _variants = variants;
        _intelligence = intelligence;
    }

    // ---------- Part 1: CV variant integration ----------

    public async Task<ApplicationCvDto?> GetCvAsync(string ownerUserId, int jobApplicationId, CancellationToken ct)
    {
        var job = await LoadJobAsync(ownerUserId, jobApplicationId, ct);
        if (job is null) return null;
        return await BuildCvAsync(ownerUserId, job, ct);
    }

    // Attach an EXISTING variant to this application, or detach with null. Creating, duplicating,
    // editing, previewing and exporting all stay in CvVariantService — this only moves the pointer.
    public async Task<ApplicationCvDto?> AttachVariantAsync(string ownerUserId, int jobApplicationId, int? variantId, CancellationToken ct)
    {
        var job = await LoadJobAsync(ownerUserId, jobApplicationId, ct);
        if (job is null) return null;

        // Detaching clears whatever this application currently points at.
        var currentlyAttached = await _db.CvVariants
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == jobApplicationId)
            .ToListAsync(ct);

        if (variantId is null)
        {
            foreach (var v in currentlyAttached) v.JobApplicationId = null;
            await _db.SaveChangesAsync(ct);
            return await BuildCvAsync(ownerUserId, job, ct);
        }

        var target = await _db.CvVariants
            .FirstOrDefaultAsync(v => v.Id == variantId.Value && v.OwnerUserId == ownerUserId, ct);
        if (target is null) return null;

        // One attached variant per application: the workspace answers "which CV am I sending".
        foreach (var v in currentlyAttached.Where(v => v.Id != target.Id)) v.JobApplicationId = null;
        target.JobApplicationId = jobApplicationId;
        target.UpdatedAtUtc = DateTimeOffset.UtcNow;
        await _db.SaveChangesAsync(ct);

        return await BuildCvAsync(ownerUserId, job, ct);
    }

    private async Task<ApplicationCvDto> BuildCvAsync(string ownerUserId, JobApplication job, CancellationToken ct)
    {
        var attachedQuery = _db.CvVariants.AsNoTracking()
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == job.Id);
        var attached = _db.Database.IsSqlite()
            ? (await attachedQuery.ToListAsync(ct)).MaxBy(v => v.UpdatedAtUtc)
            : await attachedQuery.OrderByDescending(v => v.UpdatedAtUtc).FirstOrDefaultAsync(ct);

        var available = await _variants.ListAsync(ownerUserId, ct);

        return new ApplicationCvDto(
            attached?.Id,
            attached?.Name,
            attached is null ? null : CvVariantSettingsJson.Deserialize(attached.SettingsJson).ThemeId,
            attached?.Version,
            attached?.UpdatedAtUtc,
            attached?.IsPublic ?? false,
            !string.IsNullOrWhiteSpace(job.TailoredCvText),
            available);
    }

    // ---------- Part 2: tailoring workflow ----------

    // Deterministic suggestions built from the Phase 5.3 analysis and match. These are SUGGESTIONS:
    // the service returns what the user could emphasise and the user decides. Nothing here edits a
    // variant, and nothing writes to the CareerProfile.
    public async Task<TailoringPlanDto?> GetTailoringPlanAsync(string ownerUserId, int jobApplicationId, CancellationToken ct)
    {
        var job = await LoadJobAsync(ownerUserId, jobApplicationId, ct);
        if (job is null) return null;

        var analysis = await _intelligence.AnalyzeAsync(ownerUserId, jobApplicationId, ct);
        var match = await _intelligence.MatchAsync(ownerUserId, jobApplicationId, ct);
        if (analysis is null || match is null) return null;

        var attachedVariantId = await _db.CvVariants.AsNoTracking()
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == jobApplicationId)
            .Select(v => (int?)v.Id)
            .FirstOrDefaultAsync(ct);

        var suggestions = new List<TailoringSuggestionDto>();

        if (match.MatchedSkills.Count > 0)
        {
            suggestions.Add(new TailoringSuggestionDto(
                "highlight-skills",
                "Skills to highlight",
                "The advert asks for these and your profile already has them — put them where they are seen first.",
                match.MatchedSkills));
        }

        if (match.RelevantExperience.Count > 0)
        {
            suggestions.Add(new TailoringSuggestionDto(
                "prioritise-experience",
                "Experience to prioritise",
                "Ordered by how much of the advert each role actually covers.",
                match.RelevantExperience.Select(e => e.Subtitle is null ? e.Title : $"{e.Title} — {e.Subtitle}").ToList()));
        }

        if (match.RelevantProjects.Count > 0)
        {
            suggestions.Add(new TailoringSuggestionDto(
                "emphasise-projects",
                "Projects to emphasise",
                "These projects demonstrate what the advert is asking for.",
                match.RelevantProjects.Select(p => p.Subtitle is null ? p.Title : $"{p.Title} — {p.Subtitle}").ToList()));
        }

        if (analysis.Keywords.Count > 0)
        {
            suggestions.Add(new TailoringSuggestionDto(
                "include-keywords",
                "Keywords to include",
                "Vocabulary from the advert. Use the ones that are honestly true of you — never pad.",
                analysis.Keywords));
        }

        if (match.MissingSkills.Count > 0)
        {
            suggestions.Add(new TailoringSuggestionDto(
                "gaps",
                "Gaps to address",
                "Asked for but not found in your profile. Add them if you have them; otherwise be ready to talk about them.",
                match.MissingSkills));
        }

        return new TailoringPlanDto(
            analysis.HasJobDescription,
            match.HasCareerProfile,
            attachedVariantId is not null,
            match.Score,
            suggestions,
            analysis.AiSuggestionCount + match.AiSuggestionCount);
    }

    // ---------- Part 3: cover letter workflow ----------

    public async Task<CoverLetterDto?> GetCoverLetterAsync(string ownerUserId, int jobApplicationId, CancellationToken ct)
    {
        var job = await LoadJobAsync(ownerUserId, jobApplicationId, ct);
        if (job is null) return null;
        return await BuildCoverLetterAsync(ownerUserId, job, ct);
    }

    // Every save snapshots the PREVIOUS text first, so an AI rewrite can always be undone. The user's
    // text is what gets stored — an AI suggestion only becomes a version once the user saves it, which
    // is what "requires approval" means here.
    public async Task<CoverLetterDto?> SaveCoverLetterAsync(string ownerUserId, int jobApplicationId, string? text, string source, string? aiAction, CancellationToken ct)
    {
        var job = await _db.JobApplications
            .FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
        if (job is null) return null;

        var next = text?.Trim() ?? string.Empty;
        var current = job.CoverLetterText?.Trim() ?? string.Empty;

        // Nothing changed: do not spend a version on a no-op save (autosave calls this often).
        if (string.Equals(next, current, StringComparison.Ordinal))
        {
            return await BuildCoverLetterAsync(ownerUserId, job, ct);
        }

        var version = await NextVersionAsync(ownerUserId, jobApplicationId, ct);
        _db.CoverLetterVersions.Add(new CoverLetterVersion
        {
            OwnerUserId = ownerUserId,
            JobApplicationId = jobApplicationId,
            Version = version,
            Text = next,
            Source = CoverLetterSources.IsValid(source) ? source : CoverLetterSources.Manual,
            AiAction = string.IsNullOrWhiteSpace(aiAction) ? null : aiAction.Trim(),
            CreatedAtUtc = DateTimeOffset.UtcNow,
        });

        job.CoverLetterText = next.Length == 0 ? null : next;
        // HasCoverLetter is derived from attachments elsewhere; a written draft counts too.
        if (next.Length > 0) job.HasCoverLetter = true;

        await _db.SaveChangesAsync(ct);
        return await BuildCoverLetterAsync(ownerUserId, job, ct);
    }

    // Restore is non-destructive: the old text comes back as a NEW version, so the thing you restored
    // from is still in the history.
    public async Task<CoverLetterDto?> RestoreCoverLetterAsync(string ownerUserId, int jobApplicationId, int version, CancellationToken ct)
    {
        var job = await _db.JobApplications
            .FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
        if (job is null) return null;

        var snapshot = await _db.CoverLetterVersions.AsNoTracking()
            .FirstOrDefaultAsync(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == jobApplicationId && v.Version == version, ct);
        if (snapshot is null) return null;

        return await SaveCoverLetterAsync(ownerUserId, jobApplicationId, snapshot.Text, CoverLetterSources.Restore, null, ct);
    }

    private async Task<CoverLetterDto> BuildCoverLetterAsync(string ownerUserId, JobApplication job, CancellationToken ct)
    {
        var versions = await _db.CoverLetterVersions.AsNoTracking()
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == job.Id)
            .OrderByDescending(v => v.Version)
            .ToListAsync(ct);

        var current = versions.Count == 0 ? 0 : versions[0].Version;

        var aiCount = await _db.AiInteractions.AsNoTracking()
            .CountAsync(a => a.OwnerUserId == ownerUserId && a.JobApplicationId == job.Id && a.Module == "cover-letter", ct);

        return new CoverLetterDto(
            job.CoverLetterText,
            current,
            versions.Select(v => new CoverLetterVersionDto(
                v.Version, v.Source, v.AiAction, v.Text.Length, v.CreatedAtUtc, v.Version == current)).ToList(),
            aiCount);
    }

    private async Task<int> NextVersionAsync(string ownerUserId, int jobApplicationId, CancellationToken ct)
    {
        var max = await _db.CoverLetterVersions.AsNoTracking()
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == jobApplicationId)
            .Select(v => (int?)v.Version)
            .MaxAsync(ct);
        return (max ?? 0) + 1;
    }

    private Task<JobApplication?> LoadJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct) =>
        _db.JobApplications.AsNoTracking()
            .FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
}
