using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Models;

namespace JobTrackerApi.Data
{
    public class JobTrackerContext : IdentityDbContext<ApplicationUser>
{
    private readonly JobTrackerApi.Services.ICurrentUserService _currentUser;

    // Evaluated live on each access, NOT captured in the constructor. The "local" JwtBearer
    // OnTokenValidated resolves this request-scoped DbContext to run LocalSessionValidator BEFORE
    // the authentication middleware assigns HttpContext.User. A constructor snapshot therefore froze
    // CurrentUserId to null for the whole request, and the same scoped instance is reused by the
    // controller — so every tenant-filtered read compiled to `WHERE FALSE` and returned nothing
    // (created rows 404'd on read, lists came back empty) even though writes set OwnerUserId
    // correctly from the controller-resolved user. Reading it live means the query filters see the
    // authenticated user at query-execution time. Deny-on-null is preserved: it is still null for an
    // unauthenticated principal.
    public string? CurrentUserId => _currentUser.UserId;

    public JobTrackerContext(DbContextOptions<JobTrackerContext> options, JobTrackerApi.Services.ICurrentUserService currentUser) : base(options)
    {
        _currentUser = currentUser;
    }

    public DbSet<Company> Companies => Set<Company>();
    public DbSet<Job> Jobs => Set<Job>();
    public DbSet<JobApplication> JobApplications => Set<JobApplication>();
    public DbSet<Correspondence> Correspondences => Set<Correspondence>();
    public DbSet<GmailConnection> GmailConnections => Set<GmailConnection>();
    public DbSet<GmailReviewDecision> GmailReviewDecisions => Set<GmailReviewDecision>();
    public DbSet<MicrosoftGraphConnection> MicrosoftGraphConnections => Set<MicrosoftGraphConnection>();
    public DbSet<ImapConnection> ImapConnections => Set<ImapConnection>();
    public DbSet<Attachment> Attachments => Set<Attachment>();
    public DbSet<RuleSettings> RuleSettings => Set<RuleSettings>();
    public DbSet<UserRuleSettings> UserRuleSettings => Set<UserRuleSettings>();
    public DbSet<SystemEmailSettings> SystemEmailSettings => Set<SystemEmailSettings>();
    public DbSet<JobEvent> JobEvents => Set<JobEvent>();
    public DbSet<CvUploadArtifact> CvUploadArtifacts => Set<CvUploadArtifact>();
    public DbSet<CvExtractionRun> CvExtractionRuns => Set<CvExtractionRun>();
    public DbSet<TailoredCvDraft> TailoredCvDrafts => Set<TailoredCvDraft>();
    public DbSet<TwoFactorRecoveryCode> TwoFactorRecoveryCodes => Set<TwoFactorRecoveryCode>();
    public DbSet<TrustedDevice> TrustedDevices => Set<TrustedDevice>();
    public DbSet<UserSession> UserSessions => Set<UserSession>();
    public DbSet<CareerProfile> CareerProfiles => Set<CareerProfile>();
    public DbSet<CareerProfileVersion> CareerProfileVersions => Set<CareerProfileVersion>();
    public DbSet<CareerExperience> CareerExperiences => Set<CareerExperience>();
    public DbSet<CareerEducation> CareerEducations => Set<CareerEducation>();
    public DbSet<CareerSkill> CareerSkills => Set<CareerSkill>();
    public DbSet<CareerProject> CareerProjects => Set<CareerProject>();
    public DbSet<CareerCertification> CareerCertifications => Set<CareerCertification>();
    public DbSet<CareerLanguage> CareerLanguages => Set<CareerLanguage>();
    public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
    public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
    public DbSet<CvVariant> CvVariants => Set<CvVariant>();
    public DbSet<CvVariantVersion> CvVariantVersions => Set<CvVariantVersion>();
    public DbSet<AiInteraction> AiInteractions => Set<AiInteraction>();
    public DbSet<AiUsageRecord> AiUsageRecords => Set<AiUsageRecord>();
    public DbSet<ApplicationChecklistItem> ApplicationChecklistItems => Set<ApplicationChecklistItem>();
    public DbSet<CoverLetterVersion> CoverLetterVersions => Set<CoverLetterVersion>();
    public DbSet<InterviewPrepItem> InterviewPrepItems => Set<InterviewPrepItem>();
    public DbSet<UserOperation> UserOperations => Set<UserOperation>();
    public DbSet<UserNotification> UserNotifications => Set<UserNotification>();
    public DbSet<EmailSendAttempt> EmailSendAttempts => Set<EmailSendAttempt>();
    public DbSet<EmailDraft> EmailDrafts => Set<EmailDraft>();
    public DbSet<AccountDeletionRequest> AccountDeletionRequests => Set<AccountDeletionRequest>();
    public DbSet<AccountDeletionFile> AccountDeletionFiles => Set<AccountDeletionFile>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.PendingEmail)
            .HasMaxLength(320);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.MicrosoftTenantId)
            .HasMaxLength(36);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.MicrosoftObjectId)
            .HasMaxLength(36);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.DeletionStatus)
            .HasMaxLength(32)
            .HasDefaultValue(AccountDeletionStatuses.Active);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.EmailFollowUpRemindersEnabled)
            .HasDefaultValue(true);

        modelBuilder.Entity<ApplicationUser>()
            .Property(x => x.UiLanguage)
            .HasMaxLength(16);

        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.OwnerKey).HasMaxLength(64);
        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.RequestedByUserId).HasMaxLength(255);
        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.Status).HasMaxLength(32);
        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.Stage).HasMaxLength(32);
        modelBuilder.Entity<AccountDeletionRequest>().Property(x => x.LastErrorCategory).HasMaxLength(64);
        modelBuilder.Entity<AccountDeletionRequest>()
            .HasIndex(x => new { x.OwnerUserId, x.Status });
        modelBuilder.Entity<AccountDeletionRequest>()
            .HasIndex(x => new { x.Status, x.RequestedAtUtc });
        modelBuilder.Entity<AccountDeletionFile>().Property(x => x.Category).HasMaxLength(64);
        modelBuilder.Entity<AccountDeletionFile>().Property(x => x.Status).HasMaxLength(32);
        modelBuilder.Entity<AccountDeletionFile>().Property(x => x.Sha256).HasMaxLength(64);
        modelBuilder.Entity<AccountDeletionFile>()
            .HasIndex(x => new { x.AccountDeletionRequestId, x.Status });
        modelBuilder.Entity<AccountDeletionFile>()
            .HasOne(x => x.Request)
            .WithMany(x => x.Files)
            .HasForeignKey(x => x.AccountDeletionRequestId)
            .OnDelete(DeleteBehavior.Cascade);

        // Both supported databases allow multiple NULL values in a unique index, so legacy
        // rows remain unassigned while each proven tenant/object pair has exactly one owner.
        modelBuilder.Entity<ApplicationUser>()
            .HasIndex(x => new { x.MicrosoftTenantId, x.MicrosoftObjectId })
            .IsUnique();

        modelBuilder.Entity<Company>()
            .HasQueryFilter(c => CurrentUserId != null && c.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<JobApplication>()
            .HasQueryFilter(j => CurrentUserId != null && j.OwnerUserId == CurrentUserId);

        // Job (the opportunity) is tenant-owned like everything else: same deny-on-null filter,
        // so a null CurrentUserId returns nothing rather than every tenant's rows.
        modelBuilder.Entity<Job>()
            .HasQueryFilter(j => CurrentUserId != null && j.OwnerUserId == CurrentUserId);

        // WithMany() with no inverse navigation: Company.Jobs is already the JobApplication
        // collection (a legacy name predating this split), so Job hangs off Company without
        // claiming it. Restrict rather than Cascade — deleting a company should not silently
        // destroy opportunity records that applications may still reference.
        modelBuilder.Entity<Job>()
            .HasOne(j => j.Company)
            .WithMany()
            .HasForeignKey(j => j.CompanyId)
            .OnDelete(DeleteBehavior.Restrict);

        modelBuilder.Entity<Job>()
            .HasIndex(j => j.OwnerUserId);

        // SetNull, not Cascade: an application must survive its Job row being removed, since
        // JobApplication still carries its own copy of the opportunity columns during the
        // Phase 0 -> Phase 1 transition.
        modelBuilder.Entity<JobApplication>()
            .HasOne(j => j.Job)
            .WithMany(o => o.Applications)
            .HasForeignKey(j => j.JobId)
            .OnDelete(DeleteBehavior.SetNull);

        modelBuilder.Entity<UserRuleSettings>()
            .HasKey(x => x.OwnerUserId);

        modelBuilder.Entity<UserRuleSettings>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<RuleSettings>()
            .HasData(new RuleSettings { Id = 1 });

        modelBuilder.Entity<JobApplication>()
            .HasOne(j => j.Company)
            .WithMany(c => c.Jobs)
            .HasForeignKey(j => j.CompanyId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<JobApplication>()
            .HasIndex(j => j.OwnerUserId);

        // Owner-prefixed composite indexes for the tenant-scoped hot paths. Every
        // JobApplication query is scoped by the OwnerUserId global filter first, then
        // filtered by IsDeleted (list/board/stats/analytics) or FollowUpAt (reminders).
        // Status is intentionally excluded from the index because Pomelo maps the
        // unbounded string column to longtext, which MariaDB cannot index without a
        // prefix length. The actual index DDL is applied idempotently in
        // StartupInitializationExtensions (this repo provisions schema via that
        // reconciler, not via the EF ModelSnapshot, which is stale).
        modelBuilder.Entity<JobApplication>()
            .HasIndex(j => new { j.OwnerUserId, j.IsDeleted });

        modelBuilder.Entity<JobApplication>()
            .HasIndex(j => new { j.OwnerUserId, j.FollowUpAt });

        // Board/list endpoints that filter by both IsDeleted and Status. Same MySQL
        // longtext-prefix caveat as above; the reconciler applies `Status(50)` there.
        modelBuilder.Entity<JobApplication>()
            .HasIndex(j => new { j.OwnerUserId, j.IsDeleted, j.Status });

        modelBuilder.Entity<Company>()
            .HasIndex(c => c.OwnerUserId);

        modelBuilder.Entity<Correspondence>()
            .HasQueryFilter(c => CurrentUserId != null && c.JobApplication.OwnerUserId == CurrentUserId)
            .HasOne(c => c.JobApplication)
            .WithMany(j => j.Messages)
            .HasForeignKey(c => c.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<Correspondence>()
            .HasIndex(c => c.JobApplicationId);

        modelBuilder.Entity<GmailConnection>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<GmailReviewDecision>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Ignore<CorrespondenceAttachmentMetadata>();

        modelBuilder.Entity<GmailConnection>()
            .HasIndex(x => new { x.OwnerUserId, x.GmailAddress })
            .IsUnique();

        modelBuilder.Entity<GmailConnection>()
            .HasIndex(x => x.OwnerUserId);

        modelBuilder.Entity<Attachment>()
            .HasOne(a => a.JobApplication)
            .WithMany(j => j.Attachments)
            .HasForeignKey(a => a.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<JobEvent>()
            .HasQueryFilter(x => CurrentUserId != null && x.JobApplication.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<JobEvent>()
            .HasOne(e => e.JobApplication)
            .WithMany(j => j.Events)
            .HasForeignKey(e => e.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<JobEvent>()
            .HasIndex(e => e.JobApplicationId);

        modelBuilder.Entity<CvUploadArtifact>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<CvUploadArtifact>()
            .HasIndex(x => new { x.OwnerUserId, x.UploadedAtUtc });

        modelBuilder.Entity<CvExtractionRun>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<CvExtractionRun>()
            .HasIndex(x => new { x.OwnerUserId, x.StartedAtUtc });

        modelBuilder.Entity<CvExtractionRun>()
            .HasOne(x => x.Artifact)
            .WithMany()
            .HasForeignKey(x => x.ArtifactId)
            .OnDelete(DeleteBehavior.SetNull);

        modelBuilder.Entity<UserOperation>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<UserOperation>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<UserOperation>().Property(x => x.TaskType).HasMaxLength(64);
        modelBuilder.Entity<UserOperation>().Property(x => x.IdempotencyKey).HasMaxLength(128);
        modelBuilder.Entity<UserOperation>().Property(x => x.Status).HasMaxLength(32);
        modelBuilder.Entity<UserOperation>().Property(x => x.EntitlementDecision).HasMaxLength(32);
        modelBuilder.Entity<UserOperation>().Property(x => x.PrivacyPolicy).HasMaxLength(32);
        modelBuilder.Entity<UserOperation>().Property(x => x.SubjectType).HasMaxLength(64);
        modelBuilder.Entity<UserOperation>().Property(x => x.SubjectId).HasMaxLength(128);
        modelBuilder.Entity<UserOperation>().Property(x => x.Provider).HasMaxLength(128);
        modelBuilder.Entity<UserOperation>().Property(x => x.Model).HasMaxLength(128);
        modelBuilder.Entity<UserOperation>().Property(x => x.LeaseToken).HasMaxLength(32);
        modelBuilder.Entity<UserOperation>().Property(x => x.ProgressStage).HasMaxLength(64);
        modelBuilder.Entity<UserOperation>().Property(x => x.FailureCategory).HasMaxLength(64);
        modelBuilder.Entity<UserOperation>().Property(x => x.FailureMessage).HasMaxLength(512);
        modelBuilder.Entity<UserOperation>().Property(x => x.ResultReference).HasMaxLength(256);
        modelBuilder.Entity<UserOperation>()
            .HasIndex(x => new { x.OwnerUserId, x.TaskType, x.IdempotencyKey })
            .IsUnique();
        modelBuilder.Entity<UserOperation>()
            .HasIndex(x => new { x.Status, x.AvailableAtUtc, x.Priority });

        modelBuilder.Entity<UserNotification>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<UserNotification>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<UserNotification>().Property(x => x.Kind).HasMaxLength(64);
        modelBuilder.Entity<UserNotification>().Property(x => x.Title).HasMaxLength(160);
        modelBuilder.Entity<UserNotification>().Property(x => x.Message).HasMaxLength(512);
        modelBuilder.Entity<UserNotification>().Property(x => x.LinkPath).HasMaxLength(256);
        modelBuilder.Entity<UserNotification>()
            .HasIndex(x => x.OperationId)
            .IsUnique();
        modelBuilder.Entity<UserNotification>()
            .HasIndex(x => new { x.OwnerUserId, x.DismissedAtUtc, x.ReadAtUtc, x.CreatedAtUtc });
        modelBuilder.Entity<UserNotification>()
            .HasOne(x => x.Operation)
            .WithOne()
            .HasForeignKey<UserNotification>(x => x.OperationId)
            .OnDelete(DeleteBehavior.SetNull);

        modelBuilder.Entity<EmailSendAttempt>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.Provider).HasMaxLength(32);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.ClientRequestId).HasMaxLength(128);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.PayloadHash).HasMaxLength(64);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.Status).HasMaxLength(32);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.ProviderMessageId).HasMaxLength(256);
        modelBuilder.Entity<EmailSendAttempt>().Property(x => x.FailureCategory).HasMaxLength(64);
        modelBuilder.Entity<EmailSendAttempt>()
            .HasIndex(x => new { x.OwnerUserId, x.ClientRequestId })
            .IsUnique();
        modelBuilder.Entity<EmailSendAttempt>()
            .HasIndex(x => new { x.OwnerUserId, x.CreatedAtUtc });
        modelBuilder.Entity<EmailSendAttempt>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<EmailDraft>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<EmailDraft>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<EmailDraft>().Property(x => x.Provider).HasMaxLength(32);
        modelBuilder.Entity<EmailDraft>().Property(x => x.To).HasMaxLength(320);
        modelBuilder.Entity<EmailDraft>().Property(x => x.Subject).HasMaxLength(998);
        modelBuilder.Entity<EmailDraft>().Property(x => x.ThreadId).HasMaxLength(512);
        modelBuilder.Entity<EmailDraft>().Property(x => x.ClientRequestId).HasMaxLength(128);
        modelBuilder.Entity<EmailDraft>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.UpdatedAtUtc });
        modelBuilder.Entity<EmailDraft>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<TailoredCvDraft>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<TailoredCvDraft>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId })
            .IsUnique();

        modelBuilder.Entity<TailoredCvDraft>()
            .HasOne(x => x.JobApplication)
            .WithOne(j => j.TailoredCvDraft)
            .HasForeignKey<TailoredCvDraft>(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        // No FK to AspNetUsers: the login-time challenge endpoint reads these rows before a
        // session (and thus CurrentUserId) exists, via IgnoreQueryFilters() -- same convention
        // as AdminAuditController's cross-cutting queries.
        modelBuilder.Entity<TwoFactorRecoveryCode>()
            .HasQueryFilter(x => CurrentUserId != null && x.UserId == CurrentUserId);

        modelBuilder.Entity<TwoFactorRecoveryCode>()
            .HasIndex(x => new { x.UserId, x.UsedAtUtc });

        // No FK to AspNetUsers: the login-time trusted-device check reads these rows before a
        // session (and thus CurrentUserId) exists, via IgnoreQueryFilters() -- same convention
        // as TwoFactorRecoveryCode above.
        modelBuilder.Entity<TrustedDevice>()
            .HasQueryFilter(x => CurrentUserId != null && x.UserId == CurrentUserId);

        modelBuilder.Entity<TrustedDevice>()
            .HasIndex(x => x.UserId);

        modelBuilder.Entity<TrustedDevice>()
            .HasIndex(x => x.TokenHash);

        // No FK to AspNetUsers, same convention as TrustedDevice/TwoFactorRecoveryCode above: the
        // OnTokenValidated auth check reads this table before CurrentUserId is meaningfully set
        // for the request being validated, via IgnoreQueryFilters().
        modelBuilder.Entity<UserSession>()
            .HasKey(x => x.Id);

        modelBuilder.Entity<UserSession>()
            .HasQueryFilter(x => CurrentUserId != null && x.UserId == CurrentUserId);

        modelBuilder.Entity<UserSession>()
            .HasIndex(x => x.UserId);

        // Career Workspace foundation (docs/career-workspace-implementation-roadmap.md Phase F1).
        // One CareerProfile per user for now -- see roadmap "Not now: multiple profiles per user".
        modelBuilder.Entity<CareerProfile>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<CareerProfile>().Property(x => x.OwnerUserId).HasMaxLength(255);

        modelBuilder.Entity<CareerProfile>()
            .HasIndex(x => x.OwnerUserId)
            .IsUnique();

        modelBuilder.Entity<CareerProfileVersion>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<CareerProfileVersion>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<CareerProfileVersion>().Property(x => x.Source).HasMaxLength(100);

        modelBuilder.Entity<CareerProfileVersion>()
            .HasIndex(x => new { x.OwnerUserId, x.CareerProfileId, x.Version });

        modelBuilder.Entity<CareerProfileVersion>()
            .HasOne(x => x.CareerProfile)
            .WithMany()
            .HasForeignKey(x => x.CareerProfileId)
            .OnDelete(DeleteBehavior.Cascade);

        // Phase 3: relational children of CareerProfile (the editable master profile).
        // docs/architecture/career-profile-model.md. Same deny-on-null tenant filter as everything
        // else; cascade-delete with the parent; indexed by (OwnerUserId, CareerProfileId, SortOrder)
        // for the ordered per-profile reads the /career editor does.
        ConfigureCareerChild<CareerExperience>(modelBuilder);
        ConfigureCareerChild<CareerEducation>(modelBuilder);
        ConfigureCareerChild<CareerSkill>(modelBuilder);
        ConfigureCareerChild<CareerProject>(modelBuilder);
        ConfigureCareerChild<CareerCertification>(modelBuilder);
        ConfigureCareerChild<CareerLanguage>(modelBuilder);

        // Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5).
        modelBuilder.Entity<InterviewPrepNote>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<InterviewPrepNote>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId })
            .IsUnique();

        modelBuilder.Entity<InterviewPrepNote>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        // Generic AI workspace note persistence (candidate fit, focus plan) -- same rationale as
        // InterviewPrepNote above, generalized because these DTOs are irregular/nested enough that
        // per-field columns would be unreasonable.
        modelBuilder.Entity<AiWorkspaceNote>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        modelBuilder.Entity<AiWorkspaceNote>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.NoteType })
            .IsUnique();

        modelBuilder.Entity<AiWorkspaceNote>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        // Phase 4: CV variants (a lens over the master profile) + their autosave history.
        // docs/architecture/cv-builder.md. Same deny-on-null tenant filter as everything else. A
        // variant references a job application (reference-not-ownership, SetNull on delete) and never
        // owns career data. PublicSlug is globally unique so /cv/{slug} can resolve it anonymously.
        modelBuilder.Entity<CvVariant>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        // Bound the indexed string columns so Pomelo maps them to varchar, not longtext (MariaDB
        // cannot index a TEXT/longtext column without a prefix length). Actual prod DDL is applied by
        // the reconciler (StartupInitializationExtensions), MySQL-safe; see that file + DbContext note.
        modelBuilder.Entity<CvVariant>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<CvVariant>().Property(x => x.PublicSlug).HasMaxLength(64);
        modelBuilder.Entity<CvVariantVersion>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<CvVariant>()
            .HasIndex(x => x.PublicSlug)
            .IsUnique();
        modelBuilder.Entity<CvVariant>()
            .HasIndex(x => new { x.OwnerUserId, x.UpdatedAtUtc });
        modelBuilder.Entity<CvVariant>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.SetNull);

        modelBuilder.Entity<CvVariantVersion>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<CvVariantVersion>()
            .HasIndex(x => new { x.CvVariantId, x.Version });
        modelBuilder.Entity<CvVariantVersion>()
            .HasOne(x => x.CvVariant)
            .WithMany()
            .HasForeignKey(x => x.CvVariantId)
            .OnDelete(DeleteBehavior.Cascade);

        // Phase 5: append-only AI interaction history per job application. Same deny-on-null tenant
        // filter; indexed for the per-job history read; cascades with the application.
        // docs/architecture/ai-career-assistant.md.
        modelBuilder.Entity<AiInteraction>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        // varchar (not longtext) for the indexed columns — see the CvVariant note above.
        modelBuilder.Entity<AiInteraction>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<AiInteraction>().Property(x => x.Module).HasMaxLength(64);
        modelBuilder.Entity<AiInteraction>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.Module, x.CreatedAtUtc });
        modelBuilder.Entity<AiInteraction>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<AiUsageRecord>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        modelBuilder.Entity<AiUsageRecord>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<AiUsageRecord>().Property(x => x.SourceType).HasMaxLength(32);
        modelBuilder.Entity<AiUsageRecord>().Property(x => x.SourceId).HasMaxLength(64);
        modelBuilder.Entity<AiUsageRecord>().Property(x => x.TaskType).HasMaxLength(64);
        modelBuilder.Entity<AiUsageRecord>()
            .HasIndex(x => new { x.OwnerUserId, x.SourceType, x.SourceId })
            .IsUnique();
        modelBuilder.Entity<AiUsageRecord>()
            .HasIndex(x => new { x.OwnerUserId, x.CreatedAtUtc });

        // Phase 5 Milestone 2: the application checklist — a workflow guidance layer over the existing
        // readiness signals, not a second store of truth. Same deny-on-null tenant filter; cascades with
        // the application. docs/architecture/application-workspace.md.
        modelBuilder.Entity<ApplicationChecklistItem>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        // varchar (not longtext) for the indexed columns — see the CvVariant note above.
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.SystemKey).HasMaxLength(64);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.AutoSignal).HasMaxLength(64);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.Category).HasMaxLength(32);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.Status).HasMaxLength(32);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.Section).HasMaxLength(64);
        modelBuilder.Entity<ApplicationChecklistItem>().Property(x => x.Title).HasMaxLength(255);
        // Seeding is idempotent per (application, system key) — the unique index is what enforces it.
        modelBuilder.Entity<ApplicationChecklistItem>()
            .HasIndex(x => new { x.JobApplicationId, x.SystemKey })
            .IsUnique();
        modelBuilder.Entity<ApplicationChecklistItem>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.SortOrder });
        modelBuilder.Entity<ApplicationChecklistItem>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        // Phase 5.4: append-only cover letter history. JobApplication.CoverLetterText stays the
        // current text; this makes every previous state recoverable.
        // docs/architecture/application-workspace.md.
        modelBuilder.Entity<CoverLetterVersion>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        // varchar (not longtext) for the indexed columns — see the CvVariant note above.
        modelBuilder.Entity<CoverLetterVersion>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<CoverLetterVersion>().Property(x => x.Source).HasMaxLength(32);
        modelBuilder.Entity<CoverLetterVersion>().Property(x => x.AiAction).HasMaxLength(32);
        modelBuilder.Entity<CoverLetterVersion>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.Version });
        modelBuilder.Entity<CoverLetterVersion>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);

        // Phase 5.5: user-owned interview preparation. Unlike InterviewPrepNote (an AI cache), nothing
        // regenerates this. docs/architecture/application-workspace.md.
        modelBuilder.Entity<InterviewPrepItem>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
        // varchar (not longtext) for the indexed columns — see the CvVariant note above.
        modelBuilder.Entity<InterviewPrepItem>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<InterviewPrepItem>().Property(x => x.Category).HasMaxLength(32);
        modelBuilder.Entity<InterviewPrepItem>().Property(x => x.Source).HasMaxLength(16);
        modelBuilder.Entity<InterviewPrepItem>().Property(x => x.Title).HasMaxLength(500);
        modelBuilder.Entity<InterviewPrepItem>()
            .HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.SortOrder });
        modelBuilder.Entity<InterviewPrepItem>()
            .HasOne(x => x.JobApplication)
            .WithMany()
            .HasForeignKey(x => x.JobApplicationId)
            .OnDelete(DeleteBehavior.Cascade);
    }

    // Common config for CareerProfile's relational children. The 1:many FK + cascade delete is
    // wired by convention (each child's CareerProfileId + CareerProfile nav match the typed
    // collection on CareerProfile); this adds the tenant query filter and the ordered read index.
    private void ConfigureCareerChild<T>(ModelBuilder modelBuilder) where T : Models.CareerChildEntity
    {
        modelBuilder.Entity<T>()
            .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);

        // Bounded so Pomelo maps them to varchar, not longtext: the composite index below is over
        // OwnerUserId, and MariaDB cannot index a longtext column without a prefix length — an
        // unbounded OwnerUserId is what blew the 3072-byte key limit on a clean MariaDB install.
        modelBuilder.Entity<T>().Property(x => x.OwnerUserId).HasMaxLength(255);
        modelBuilder.Entity<T>().Property(x => x.ItemKey).HasMaxLength(255);

        modelBuilder.Entity<T>()
            .HasIndex(x => new { x.OwnerUserId, x.CareerProfileId, x.SortOrder });
    }
}
}
