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

namespace JobTrackerApi.Services;

public sealed record CvVariantSummary(
    int Id,
    string Name,
    string ThemeId,
    string Language,
    string PublicSlug,
    bool IsPublic,
    int Version,
    int? JobApplicationId,
    DateTimeOffset UpdatedAtUtc,
    string? JobTitle = null,
    string? CompanyName = null);
public sealed record CvVariantVersionInfo(int Version, string Source, DateTimeOffset CreatedAtUtc, bool IsCurrent);

// Person + photo needed to render, resolved from ApplicationUser by the controller so this service
// stays off the Identity store.
public sealed record CvRenderPerson(string FallbackName, string? PhotoDataUrl);

public interface ICvVariantService
{
    Task<IReadOnlyList<CvVariantSummary>> ListAsync(string ownerUserId, CancellationToken ct);
    Task<bool> CanAssociateJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);
    Task<CvVariant?> GetAsync(string ownerUserId, int id, CancellationToken ct);
    Task<CvVariant> CreateAsync(string ownerUserId, string? name, int? jobApplicationId, CvVariantSettings? settings, CancellationToken ct);
    Task<CvVariant?> SaveAsync(string ownerUserId, int id, string? name, CvVariantSettings settings, string source, CancellationToken ct);
    Task<CvVariant?> SetPublicAsync(string ownerUserId, int id, bool isPublic, CancellationToken ct);
    Task<CvVariant?> DuplicateAsync(string ownerUserId, int id, string? newName, CancellationToken ct);
    Task<bool> DeleteAsync(string ownerUserId, int id, CancellationToken ct);
    Task<IReadOnlyList<CvVariantVersionInfo>> ListVersionsAsync(string ownerUserId, int id, CancellationToken ct);
    Task<CvVariant?> RestoreVersionAsync(string ownerUserId, int id, int version, CancellationToken ct);

    // Render a saved variant against the current master profile.
    Task<ThemedCvRenderResult?> RenderAsync(string ownerUserId, int id, CvRenderPerson person, CancellationToken ct);
    // Render arbitrary (unsaved) settings against the current master profile — live preview.
    Task<ThemedCvRenderResult> RenderSettingsAsync(string ownerUserId, CvVariantSettings settings, CvRenderPerson person, CancellationToken ct);
    // The master profile resolved to sections+entries (with ItemKeys) for the Content tab to edit.
    Task<CvRenderModel> OutlineAsync(string ownerUserId, CvRenderPerson person, CancellationToken ct);
    // Render a public variant by slug (anonymous). Returns null if the slug is unknown or not public.
    Task<(ThemedCvRenderResult render, string ownerUserId)?> RenderPublicAsync(string publicSlug, CvRenderPerson person, CancellationToken ct);
    Task<string?> GetPublicOwnerAsync(string publicSlug, CancellationToken ct);
}

public sealed class CvVariantService : ICvVariantService
{
    private readonly JobTrackerContext _db;
    private readonly ICareerProfileService _career;
    private readonly IThemedCvRenderer _renderer;

    public CvVariantService(JobTrackerContext db, ICareerProfileService career, IThemedCvRenderer renderer)
    {
        _db = db;
        _career = career;
        _renderer = renderer;
    }

    public async Task<IReadOnlyList<CvVariantSummary>> ListAsync(string ownerUserId, CancellationToken ct)
    {
        var query = _db.CvVariants
            .Include(x => x.JobApplication)
            .ThenInclude(x => x!.Company)
            .Where(x => x.OwnerUserId == ownerUserId);
        var variants = _db.Database.IsSqlite()
            ? (await query.ToListAsync(ct)).OrderByDescending(x => x.UpdatedAtUtc).ToList()
            : await query.OrderByDescending(x => x.UpdatedAtUtc).ToListAsync(ct);
        return variants.Select(Summarize).ToList();
    }

    public Task<bool> CanAssociateJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct) =>
        _db.JobApplications.AnyAsync(job => job.Id == jobApplicationId && job.OwnerUserId == ownerUserId, ct);

    public Task<CvVariant?> GetAsync(string ownerUserId, int id, CancellationToken ct) =>
        _db.CvVariants.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == ownerUserId, ct);

    public async Task<CvVariant> CreateAsync(string ownerUserId, string? name, int? jobApplicationId, CvVariantSettings? settings, CancellationToken ct)
    {
        if (jobApplicationId is not null && !await CanAssociateJobAsync(ownerUserId, jobApplicationId.Value, ct))
            throw new ArgumentException("The job application is unavailable.", nameof(jobApplicationId));
        // The first save is required to obtain the generated variant id used by its initial
        // history row. Keep both saves in one relational transaction so a concurrent autosave
        // cannot slip between them and leave a created CV without version 1.
        await using var transaction = _db.Database.IsRelational()
            ? await _db.Database.BeginTransactionAsync(ct)
            : null;
        var now = DateTimeOffset.UtcNow;
        var normalized = CvVariantSettingsJson.Normalize(settings);
        var variant = new CvVariant
        {
            OwnerUserId = ownerUserId,
            PublicSlug = NewSlug(),
            Name = CleanName(name),
            JobApplicationId = jobApplicationId,
            SettingsJson = CvVariantSettingsJson.Serialize(normalized),
            Version = 1,
            CreatedAtUtc = now,
            UpdatedAtUtc = now,
        };
        _db.CvVariants.Add(variant);
        await _db.SaveChangesAsync(ct);
        AppendVersion(variant, "create");
        await _db.SaveChangesAsync(ct);
        if (transaction is not null) await transaction.CommitAsync(ct);
        return variant;
    }

    public async Task<CvVariant?> SaveAsync(string ownerUserId, int id, string? name, CvVariantSettings settings, string source, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return null;
        if (!string.IsNullOrWhiteSpace(name)) variant.Name = CleanName(name);
        variant.SettingsJson = CvVariantSettingsJson.Serialize(settings);
        variant.Version += 1;
        variant.UpdatedAtUtc = DateTimeOffset.UtcNow;
        AppendVersion(variant, string.IsNullOrWhiteSpace(source) ? "autosave" : source.Trim());
        await _db.SaveChangesAsync(ct);
        return variant;
    }

    public async Task<CvVariant?> SetPublicAsync(string ownerUserId, int id, bool isPublic, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return null;
        if (isPublic && !variant.IsPublic) variant.PublicSlug = NewSlug();
        variant.IsPublic = isPublic;
        variant.UpdatedAtUtc = DateTimeOffset.UtcNow;
        await _db.SaveChangesAsync(ct);
        return variant;
    }

    public async Task<CvVariant?> DuplicateAsync(string ownerUserId, int id, string? newName, CancellationToken ct)
    {
        var source = await GetAsync(ownerUserId, id, ct);
        if (source is null) return null;
        var settings = CvVariantSettingsJson.Deserialize(source.SettingsJson);
        var associatedJobId = source.JobApplicationId is not null
            && await CanAssociateJobAsync(ownerUserId, source.JobApplicationId.Value, ct)
                ? source.JobApplicationId
                : null;
        return await CreateAsync(ownerUserId, string.IsNullOrWhiteSpace(newName) ? $"{source.Name} (copy)" : newName, associatedJobId, settings, ct);
    }

    public async Task<bool> DeleteAsync(string ownerUserId, int id, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return false;
        _db.CvVariantVersions.RemoveRange(_db.CvVariantVersions.Where(x => x.CvVariantId == id));
        _db.CvVariants.Remove(variant);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<IReadOnlyList<CvVariantVersionInfo>> ListVersionsAsync(string ownerUserId, int id, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return Array.Empty<CvVariantVersionInfo>();
        var versions = await _db.CvVariantVersions.Where(x => x.CvVariantId == id)
            .OrderByDescending(x => x.Version)
            .Select(x => new { x.Version, x.Source, x.CreatedAtUtc }).ToListAsync(ct);
        return versions.Select(x => new CvVariantVersionInfo(x.Version, x.Source, x.CreatedAtUtc, x.Version == variant.Version)).ToList();
    }

    public async Task<CvVariant?> RestoreVersionAsync(string ownerUserId, int id, int version, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return null;
        var target = await _db.CvVariantVersions.FirstOrDefaultAsync(x => x.CvVariantId == id && x.Version == version, ct);
        if (target is null) return null;
        var settings = CvVariantSettingsJson.Deserialize(target.SettingsJson);
        return await SaveAsync(ownerUserId, id, null, settings, $"restore:v{version}", ct);
    }

    public async Task<ThemedCvRenderResult?> RenderAsync(string ownerUserId, int id, CvRenderPerson person, CancellationToken ct)
    {
        var variant = await GetAsync(ownerUserId, id, ct);
        if (variant is null) return null;
        var settings = CvVariantSettingsJson.Deserialize(variant.SettingsJson);
        var profile = await _career.LoadStructuredAsync(ownerUserId, ct);
        return RenderInternal(profile, settings, person);
    }

    public async Task<ThemedCvRenderResult> RenderSettingsAsync(string ownerUserId, CvVariantSettings settings, CvRenderPerson person, CancellationToken ct)
    {
        var profile = await _career.LoadStructuredAsync(ownerUserId, ct);
        return RenderInternal(profile, settings, person);
    }

    public async Task<CvRenderModel> OutlineAsync(string ownerUserId, CvRenderPerson person, CancellationToken ct)
    {
        var profile = await _career.LoadStructuredAsync(ownerUserId, ct);
        return CvVariantResolver.Build(profile, new CvVariantSettings(), person.FallbackName, person.PhotoDataUrl);
    }

    public async Task<(ThemedCvRenderResult render, string ownerUserId)?> RenderPublicAsync(string publicSlug, CvRenderPerson person, CancellationToken ct)
    {
        var variant = await _db.CvVariants.IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.PublicSlug == publicSlug && x.IsPublic, ct);
        if (variant is null) return null;
        var settings = CvVariantSettingsJson.Deserialize(variant.SettingsJson);
        var profile = await _career.LoadStructuredForOwnerAsync(variant.OwnerUserId, ct);
        return (RenderInternal(profile, settings, person), variant.OwnerUserId);
    }

    public async Task<string?> GetPublicOwnerAsync(string publicSlug, CancellationToken ct)
    {
        return await _db.CvVariants.IgnoreQueryFilters()
            .Where(x => x.PublicSlug == publicSlug && x.IsPublic)
            .Select(x => x.OwnerUserId).FirstOrDefaultAsync(ct);
    }

    private ThemedCvRenderResult RenderInternal(StructuredCvProfile profile, CvVariantSettings settings, CvRenderPerson person)
    {
        var theme = CvThemeCatalog.Resolve(settings.ThemeId);
        var model = CvVariantResolver.Build(profile, settings, person.FallbackName, person.PhotoDataUrl);
        return _renderer.Render(model, theme, settings);
    }

    private void AppendVersion(CvVariant variant, string source) =>
        _db.CvVariantVersions.Add(new CvVariantVersion
        {
            OwnerUserId = variant.OwnerUserId,
            CvVariantId = variant.Id,
            Version = variant.Version,
            SettingsJson = variant.SettingsJson,
            Source = source,
            CreatedAtUtc = DateTimeOffset.UtcNow,
        });

    private static CvVariantSummary Summarize(CvVariant v)
    {
        var settings = CvVariantSettingsJson.Deserialize(v.SettingsJson);
        return new CvVariantSummary(
            v.Id,
            v.Name,
            settings.ThemeId,
            settings.Language ?? "en",
            v.PublicSlug,
            v.IsPublic,
            v.Version,
            v.JobApplicationId,
            v.UpdatedAtUtc,
            v.JobApplication?.JobTitle,
            v.JobApplication?.Company?.Name);
    }

    private static string CleanName(string? name) => string.IsNullOrWhiteSpace(name) ? "Untitled CV" : name.Trim();
    private static string NewSlug() => Guid.NewGuid().ToString("N");
}
