using Microsoft.EntityFrameworkCore;
using OmsorgCore.Application.Abstractions;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Domain.Enums;
using OmsorgCore.Infrastructure.Security;

namespace OmsorgCore.Infrastructure.Persistence;

/// <summary>
/// Legt beim ersten Start einen Standard-Administrator an, damit ein Login überhaupt möglich ist,
/// solange es noch keinen echten Registrierungs-/User-Management-Flow gibt. Idempotent (prüft, ob
/// bereits Benutzer existieren) und bewusst nur für Development gedacht - siehe Program.cs.
/// </summary>
public static class DbSeeder
{
    private const string AdministratorRoleName = "Administrator";

    private static readonly PermissionAction[] AllActions = Enum.GetValues<PermissionAction>();

    public static async Task SeedDefaultAdminAsync(OmsorgCoreDbContext db, IPasswordHasher passwordHasher, SeedOptions options, CancellationToken cancellationToken = default)
    {
        if (await db.Users.AnyAsync(cancellationToken))
        {
            return;
        }

        var role = await db.Roles.FirstOrDefaultAsync(r => r.Name == AdministratorRoleName, cancellationToken);
        if (role is null)
        {
            role = new Role { Name = AdministratorRoleName };
            db.Roles.Add(role);

            foreach (ModuleType module in Enum.GetValues<ModuleType>())
            {
                foreach (PermissionAction action in Enum.GetValues<PermissionAction>())
                {
                    role.RolePermissions.Add(new RolePermission { Role = role, Module = module, Action = action, Scope = PermissionScope.All });
                }
            }
        }

        db.Users.Add(new User
        {
            Username = options.AdminUsername,
            PasswordHash = passwordHasher.Hash(options.AdminPassword),
            IsActive = true,
            Role = role
        });

        await db.SaveChangesAsync(cancellationToken);
    }

    /// <summary>
    /// Legt die vier in REQUIREMENTS.md Abschnitt 3 ("Akteure &amp; Rollen") und Abschnitt 7
    /// ("Rechtematrix") beschriebenen Basis-Rollen an, falls sie noch nicht existieren. Läuft in
    /// JEDER Umgebung (keine Zugangsdaten, nur Rollen-Stammdaten, siehe Program.cs) und ist idempotent
    /// pro Rollenname - existiert eine Rolle bereits (z.B. weil sie über die Rechte-Matrix-UI unter
    /// "Einstellungen" schon angepasst wurde), wird sie NICHT überschrieben.
    ///
    /// WICHTIG: Diese Zuordnung überträgt die Rechtematrix aus REQUIREMENTS.md Abschnitt 7 auf die
    /// aktuell existierenden ModuleType/PermissionAction-Werte - REQUIREMENTS.md selbst kennt diese
    /// Enums nicht, nur die deutschen Modul-/Aktions-Bezeichnungen. Kommt ein neuer ModuleType oder
    /// eine neue PermissionAction dazu, oder ändert sich die Rechtematrix, MUSS diese Methode im
    /// selben Change aktualisiert werden (siehe Pflegehinweis in omsorgCore/CLAUDE.md, Abschnitt
    /// "Rechtesystem"). "Verträge" hat in der Rechtematrix keine eigene Zeile - hier wie
    /// Mitarbeiter/Einrichtungen behandelt (siehe FR-MA-2: Vertragsdaten für Sabina/Malik/Sabrina).
    /// "Außendienst" bekommt Employees/Contracts.View mit PermissionScope.Own (siehe
    /// IPermissionService.GetScopeAsync) - sieht nur die eigene Personalakte/eigene Verträge über
    /// User.EmployeeId. Ebenso Absences.{Create,View,Edit} mit PermissionScope.Own (FR-CON-1/FR-EM-3):
    /// der Außendienst stellt eigene Abwesenheits-/Urlaubs-/Krankmeldungsanträge über
    /// `omsorgWeb/mitarbeiter-app` gegen dieses Backend, Genehmigen/Ablehnen bleibt Büro-Rollen
    /// vorbehalten (kein Approve-Recht hier). Ebenso EmployeeFacilityDistances.{Create,View,Edit} mit
    /// PermissionScope.Own (FR-EIN-4, kilometerbasierte Fahrtkostenabrechnung, siehe
    /// MyFacilityDistancesController) - der Außendienst pflegt seine eigenen Fahrtstrecken je
    /// Einrichtung selbst über `omsorgWeb/mitarbeiter-app`, hat aber KEIN Facilities-Recht und sieht
    /// daher keine Konditionen/CRM-Daten einer Einrichtung, nur deren Namen zur Auswahl. "Geschäftsführung" bekommt AuditLog automatisch mit
    /// (iteriert alle ModuleType-Werte generisch,
    /// siehe unten) - alle anderen Rollen listen ihre Module explizit auf und bekommen AuditLog dadurch
    /// bewusst NICHT (Audit-Log ist per Default nur für Geschäftsführung sichtbar).
    /// </summary>
    public static async Task SeedBaseRolesAsync(OmsorgCoreDbContext db, CancellationToken cancellationToken = default)
    {
        await SeedRoleIfMissingAsync(db, "Geschäftsführung", Enum.GetValues<ModuleType>().Select(m => (m, AllActions, PermissionScope.All)), cancellationToken);

        await SeedRoleIfMissingAsync(db, "Disposition/Buchhaltung", new (ModuleType Module, PermissionAction[] Actions, PermissionScope Scope)[]
        {
            (ModuleType.Employees, AllActions, PermissionScope.All),
            (ModuleType.Facilities, AllActions, PermissionScope.All),
            (ModuleType.Contracts, AllActions, PermissionScope.All),
            (ModuleType.Orders, AllActions, PermissionScope.All),
            (ModuleType.Assignments, AllActions, PermissionScope.All),
            (ModuleType.TimeEntries, AllActions, PermissionScope.All),
            (ModuleType.Invoices, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Approve }, PermissionScope.All),
            (ModuleType.Recruiting, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Edit }, PermissionScope.All),
            (ModuleType.Controlling, new[] { PermissionAction.View }, PermissionScope.All),
            (ModuleType.Documents, AllActions, PermissionScope.All),
            (ModuleType.Absences, AllActions, PermissionScope.All)
        }, cancellationToken);

        await SeedRoleIfMissingAsync(db, "Recruiting", new (ModuleType Module, PermissionAction[] Actions, PermissionScope Scope)[]
        {
            (ModuleType.Employees, new[] { PermissionAction.View }, PermissionScope.All),
            (ModuleType.Facilities, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Edit }, PermissionScope.All),
            (ModuleType.Recruiting, AllActions, PermissionScope.All)
        }, cancellationToken);

        await SeedRoleIfMissingAsync(db, "Außendienst", new (ModuleType Module, PermissionAction[] Actions, PermissionScope Scope)[]
        {
            (ModuleType.Employees, new[] { PermissionAction.View }, PermissionScope.Own),
            (ModuleType.Contracts, new[] { PermissionAction.View }, PermissionScope.Own),
            (ModuleType.Assignments, new[] { PermissionAction.View }, PermissionScope.Own),
            (ModuleType.Absences, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
            (ModuleType.TimeEntries, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
            (ModuleType.EmployeeFacilityDistances, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own)
        }, cancellationToken);
    }

    /// <summary>
    /// Legt alle admin-editierbaren Auswahllisten an (Mitarbeiterstatus, Beschäftigungsart, CRM-Status,
    /// Einrichtungstyp, Vertragstyp/-status, Auftragsstatus — siehe omsorgCore/CLAUDE.md, Abschnitt
    /// "Konfigurierbare Auswahllisten") mit den bisher im Frontend hartcodierten Werten als
    /// Startbefüllung. Läuft in JEDER Umgebung (reine Stammdaten) und ist idempotent PRO LISTE
    /// (`Key`) - analog zu SeedBaseRolesAsync/SeedRoleIfMissingAsync, NICHT "sobald irgendeine
    /// ValueList existiert, alles überspringen": die Migration ReplaceOrderStatusWithValueLists legt
    /// die Liste "OrderStatus" bereits direkt per SQL an (übernimmt die bestehende Auftragsstatus-
    /// Pipeline mit unveränderten Ids), ein pauschaler "existiert schon eine Liste"-Check hätte damit
    /// bei jedem Start sofort abgebrochen und die übrigen sechs Listen nie angelegt.
    ///
    /// Die Auftragsstatus-Pipeline (Anfrage → Prüfung → offen → teilweise besetzt → vollständig
    /// besetzt → aktiv → abgeschlossen, plus Storno aus jedem nicht-terminalen Status, FR-EM-2)
    /// bekommt zusätzlich Übergangsregeln (<see cref="ValueListItemTransition"/>) - die einzige der
    /// sieben Listen, die das braucht.
    /// </summary>
    public static async Task SeedValueListsAsync(OmsorgCoreDbContext db, CancellationToken cancellationToken = default)
    {
        await SeedSimpleListIfMissingAsync(db, "EmployeeStatus", "Mitarbeiterstatus",
            new (string Value, bool IsDefault)[] { ("Aktiv", true), ("Einsatz", false), ("Urlaub", false), ("Krank", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "EmploymentType", "Beschäftigungsart",
            new (string Value, bool IsDefault)[] { ("Vollzeit", false), ("Teilzeit", false), ("Minijob", false), ("Aushilfe", false), ("Praktikant", false), ("Freiberuflich", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "CrmStatus", "CRM-Status",
            new (string Value, bool IsDefault)[] { ("Lead", true), ("Kontaktiert", false), ("Kein Bedarf", false), ("Wiedervorlage", false), ("Interesse", false), ("Angebot", false), ("Kunde", false), ("Bestandskunde", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "FollowUpPeriods", "Wiedervorlage-Fristen",
            new (string Value, bool IsDefault)[] { ("7", false), ("14", true), ("21", false), ("31", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "FacilityType", "Einrichtungstyp",
            new (string Value, bool IsDefault)[] { ("Pflegeheim", false), ("Ambulanter Pflegedienst", false), ("Krankenhaus", false), ("Betreutes Wohnen", false), ("Sonstige", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "ContractType", "Vertragstyp",
            new (string Value, bool IsDefault)[] { ("Arbeitsvertrag", true), ("Dienstleistungsvertrag", false), ("Rahmenvertrag", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "ContractStatus", "Vertragsstatus",
            new (string Value, bool IsDefault)[] { ("Entwurf", true), ("Aktiv", false), ("Beendet", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "DocumentCategory", "Dokumentkategorie",
            new (string Value, bool IsDefault)[] { ("Qualifikation", false), ("Fortbildung", false), ("Führerschein", false), ("Gesundheitsnachweis", false), ("Notiz", false), ("Sonstiges", true) }, cancellationToken);

        // Aufsteigend nach Qualifikationsniveau (SortOrder = Level) - genutzt sowohl für
        // Employee.Qualification als auch für Order.RequiredQualification (FR-EM-1/FR-EM-3), damit
        // beide Seiten auf denselben Werten und derselben Rangfolge basieren.
        await SeedSimpleListIfMissingAsync(db, "Qualification", "Qualifikation",
            new (string Value, bool IsDefault)[]
            {
                ("Ungelernte Kraft", false),
                ("Betreuungskraft (§ 43b/53c SGB XI)", false),
                ("Pflegehelfer/in", false),
                ("Pflegefachassistent/in", false),
                ("Altenpfleger/in", false),
                ("Gesundheits- und Krankenpfleger/in", false),
                ("Pflegefachkraft mit Leitungsfunktion", false)
            }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "ShiftType", "Schichtart",
            new (string Value, bool IsDefault)[]
            {
                ("Frühdienst", false),
                ("Spätdienst", false),
                ("Nachtdienst", false),
                ("Tagdienst", false),
                ("Bereitschaftsdienst", false),
                ("Sonstige", false)
            }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "Priority", "Priorität",
            new (string Value, bool IsDefault)[]
            {
                ("Niedrig", false),
                ("Normal", true),
                ("Hoch", false),
                ("Dringend", false)
            }, cancellationToken);

        // FR-EIN-4: Abrechnungsintervall der Konditionen einer Einrichtung (Facility.BillingInterval).
        await SeedSimpleListIfMissingAsync(db, "BillingInterval", "Abrechnungsintervall",
            new (string Value, bool IsDefault)[] { ("Wöchentlich", false), ("Monatlich", true), ("Quartalsweise", false) }, cancellationToken);

        // FR-CON-1/FR-EM-3: Abwesenheits-/Urlaubs-/Krankmeldungsanträge (Absence.Type/Status).
        // Einfacher String-Status ohne ValueListItemTransition (wie ContractStatus) - "wer darf
        // wohin" ergibt sich vollständig aus den Rechten (Außendienst nur Create, Büro-Rollen
        // Approve), nicht aus einem Übergangsgraphen.
        await SeedSimpleListIfMissingAsync(db, "AbsenceType", "Abwesenheitsart",
            new (string Value, bool IsDefault)[] { ("Urlaub", true), ("Krankmeldung", false), ("Sonstige", false) }, cancellationToken);

        await SeedSimpleListIfMissingAsync(db, "AbsenceStatus", "Antragsstatus",
            new (string Value, bool IsDefault)[] { ("Eingereicht", true), ("Genehmigt", false), ("Abgelehnt", false) }, cancellationToken);
        await SeedAbsenceStatusInitialFlagIfMissingAsync(db, cancellationToken);
        await SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(db, cancellationToken);

        await SeedOrderStatusListIfMissingAsync(db, cancellationToken);
        await SeedCrmStatusTransitionsIfMissingAsync(db, cancellationToken);
        await SeedTimeEntryStatusListIfMissingAsync(db, cancellationToken);

        async Task SeedSimpleListIfMissingAsync(OmsorgCoreDbContext context, string key, string displayName, (string Value, bool IsDefault)[] values, CancellationToken ct)
        {
            if (await context.ValueLists.AnyAsync(l => l.Key == key, ct))
            {
                return;
            }

            var list = new ValueList { Key = key, DisplayName = displayName };
            context.ValueLists.Add(list);

            for (var index = 0; index < values.Length; index++)
            {
                context.ValueListItems.Add(new ValueListItem
                {
                    ValueListId = list.Id,
                    Value = values[index].Value,
                    SortOrder = index,
                    IsDefault = values[index].IsDefault
                });
            }

            await context.SaveChangesAsync(ct);
        }

        async Task SeedOrderStatusListIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
        {
            // Die Migration ReplaceOrderStatusWithValueLists legt die "OrderStatus"-ValueList IMMER
            // an (auch wenn order_status_definitions zuvor leer war, weil SeedOrderStatusesAsync in
            // dieser Umgebung nie gelaufen ist) - ein reiner "existiert die Liste schon"-Check würde
            // hier fälschlich "ja" sagen und nie Items anlegen. Maßgeblich ist daher, ob die Liste
            // bereits Items hat, nicht ob die Liste selbst existiert.
            if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "OrderStatus", ct))
            {
                return;
            }

            var list = await context.ValueLists.FirstOrDefaultAsync(l => l.Key == "OrderStatus", ct);
            if (list is null)
            {
                list = new ValueList { Key = "OrderStatus", DisplayName = "Auftragsstatus" };
                context.ValueLists.Add(list);
            }

            var definitions = new (string Name, bool IsInitial, bool IsTerminal)[]
            {
                ("Anfrage", true, false),
                ("Prüfung", false, false),
                ("offen", false, false),
                ("teilweise besetzt", false, false),
                ("vollständig besetzt", false, false),
                ("aktiv", false, false),
                ("abgeschlossen", false, true),
                ("storniert", false, true)
            };

            var statuses = definitions
                .Select((d, index) => new ValueListItem
                {
                    ValueListId = list.Id,
                    Value = d.Name,
                    SortOrder = index,
                    IsInitial = d.IsInitial,
                    IsTerminal = d.IsTerminal
                })
                .ToList();
            context.ValueListItems.AddRange(statuses);

            Guid IdOf(string name) => statuses.First(s => s.Value == name).Id;

            var forwardPairs = new[]
            {
                ("Anfrage", "Prüfung"),
                ("Prüfung", "offen"),
                ("offen", "teilweise besetzt"),
                ("offen", "vollständig besetzt"),
                ("teilweise besetzt", "vollständig besetzt"),
                ("vollständig besetzt", "aktiv"),
                ("aktiv", "abgeschlossen")
            };

            // Storno ist aus jedem nicht-terminalen Status möglich (auch aus "aktiv").
            var cancelPairs = statuses.Where(s => !s.IsTerminal).Select(s => (s.Value, "storniert"));

            foreach (var (from, to) in forwardPairs.Concat(cancelPairs))
            {
                context.ValueListItemTransitions.Add(new ValueListItemTransition { FromItemId = IdOf(from), ToItemId = IdOf(to) });
            }

            await context.SaveChangesAsync(ct);
        }

        // FR-EIN-3: CRM-Status-Übergänge - locker statt streng linear (anders als OrderStatus), weil
        // ein CRM-Kontakt jederzeit abspringen oder auf Eis gelegt werden kann. Vorwärtskette entlang
        // der bestehenden SortOrder-Reihenfolge, plus von JEDEM Status aus jederzeit nach "Kein Bedarf"
        // und "Wiedervorlage". Läuft nur einmal (Liste selbst wird oben bereits idempotent geseedet,
        // hier zählt nur, ob schon Transitions für "CrmStatus" existieren).
        async Task SeedCrmStatusTransitionsIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
        {
            if (await context.ValueListItemTransitions.AnyAsync(t => t.FromItem.ValueList.Key == "CrmStatus", ct))
            {
                return;
            }

            var items = await context.ValueListItems
                .Where(i => i.ValueList.Key == "CrmStatus")
                .OrderBy(i => i.SortOrder)
                .ToListAsync(ct);

            if (items.Count == 0)
            {
                return;
            }

            Guid IdOf(string value) => items.First(i => i.Value == value).Id;

            var pairs = new HashSet<(Guid From, Guid To)>();

            for (var i = 0; i < items.Count - 1; i++)
            {
                pairs.Add((items[i].Id, items[i + 1].Id));
            }

            var keinBedarfId = IdOf("Kein Bedarf");
            var wiedervorlageId = IdOf("Wiedervorlage");
            foreach (var item in items)
            {
                if (item.Id != keinBedarfId)
                {
                    pairs.Add((item.Id, keinBedarfId));
                }
                if (item.Id != wiedervorlageId)
                {
                    pairs.Add((item.Id, wiedervorlageId));
                }
            }

            foreach (var (from, to) in pairs)
            {
                context.ValueListItemTransitions.Add(new ValueListItemTransition { FromItemId = from, ToItemId = to });
            }

            await context.SaveChangesAsync(ct);
        }

        // FR-ZE-2: Statuspipeline der Zeiterfassung (Entwurf -> eingereicht -> Prüfung -> Rückfrage ->
        // freigegeben -> abgerechnet). Anders als OrderStatus braucht diese Liste zusätzlich
        // RequiresApproval je Transition, damit TimeEntryService.SubmitAsync/DecideAsync unterscheiden
        // können, ob eine Kante vom Ersteller selbst ausgelöst werden darf (Entwurf/Rückfrage ->
        // Eingereicht) oder eine Büro-Entscheidung braucht (alle übrigen).
        async Task SeedTimeEntryStatusListIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
        {
            if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "TimeEntryStatus", ct))
            {
                return;
            }

            var list = new ValueList { Key = "TimeEntryStatus", DisplayName = "Zeiterfassungsstatus" };
            context.ValueLists.Add(list);

            var definitions = new (string Name, bool IsInitial, bool IsTerminal, bool IsEditableByOwner)[]
            {
                ("Entwurf", true, false, true),
                ("Eingereicht", false, false, true),
                ("Prüfung", false, false, false),
                ("Rückfrage", false, false, true),
                ("Freigegeben", false, false, false),
                ("Abgerechnet", false, true, false)
            };

            var statuses = definitions
                .Select((d, index) => new ValueListItem
                {
                    ValueListId = list.Id,
                    Value = d.Name,
                    SortOrder = index,
                    IsInitial = d.IsInitial,
                    IsTerminal = d.IsTerminal,
                    IsEditableByOwner = d.IsEditableByOwner
                })
                .ToList();
            context.ValueListItems.AddRange(statuses);

            Guid IdOf(string name) => statuses.First(s => s.Value == name).Id;

            var transitions = new (string From, string To, bool RequiresApproval)[]
            {
                ("Entwurf", "Eingereicht", false),
                ("Eingereicht", "Prüfung", true),
                ("Prüfung", "Rückfrage", true),
                ("Rückfrage", "Eingereicht", false),
                ("Prüfung", "Freigegeben", true),
                ("Freigegeben", "Abgerechnet", true)
            };

            foreach (var (from, to, requiresApproval) in transitions)
            {
                context.ValueListItemTransitions.Add(new ValueListItemTransition
                {
                    FromItemId = IdOf(from),
                    ToItemId = IdOf(to),
                    RequiresApproval = requiresApproval
                });
            }

            await context.SaveChangesAsync(ct);
        }

        // "Eingereicht" ist der Ausgangszustand jedes Antrags (AbsenceService.CreateAsync/UpdateAsync
        // lesen darüber, statt den Anzeigetext hartzukodieren, siehe dortige Kommentare) - IsInitial
        // markiert das strukturell, genau wie bei "OrderStatus", nicht per Admin-UI wie
        // TriggersFollowUp bei CrmStatus. Läuft idempotent (kein erneutes Setzen, sobald irgendein
        // Item der Liste bereits IsInitial trägt), damit ein späteres manuelles Umflaggen über die
        // Status-Verwaltung nicht bei jedem Start zurückgesetzt wird.
        async Task SeedAbsenceStatusInitialFlagIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
        {
            if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "AbsenceStatus" && i.IsInitial, ct))
            {
                return;
            }

            var item = await context.ValueListItems
                .FirstOrDefaultAsync(i => i.ValueList.Key == "AbsenceStatus" && i.Value == "Eingereicht", ct);
            if (item is null)
            {
                return;
            }

            item.IsInitial = true;
            await context.SaveChangesAsync(ct);
        }

        // "Genehmigt" ist der einzige AbsenceStatus-Wert, der eine Mitarbeiterzuweisung im
        // überlappenden Zeitraum blockiert/warnt (FR-EM-3, AssignmentService.CreateAsync liest
        // darüber statt den Anzeigetext hartzukodieren, siehe BlocksAssignment auf ValueListItem).
        // Idempotent wie SeedAbsenceStatusInitialFlagIfMissingAsync - läuft nicht erneut, sobald
        // irgendein Item der Liste bereits BlocksAssignment trägt.
        async Task SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
        {
            if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "AbsenceStatus" && i.BlocksAssignment, ct))
            {
                return;
            }

            var item = await context.ValueListItems
                .FirstOrDefaultAsync(i => i.ValueList.Key == "AbsenceStatus" && i.Value == "Genehmigt", ct);
            if (item is null)
            {
                return;
            }

            item.BlocksAssignment = true;
            await context.SaveChangesAsync(ct);
        }
    }

    private static async Task SeedRoleIfMissingAsync(
        OmsorgCoreDbContext db,
        string roleName,
        IEnumerable<(ModuleType Module, PermissionAction[] Actions, PermissionScope Scope)> permissions,
        CancellationToken cancellationToken)
    {
        if (await db.Roles.AnyAsync(r => r.Name == roleName, cancellationToken))
        {
            return;
        }

        var role = new Role { Name = roleName };
        foreach (var (module, actions, scope) in permissions)
        {
            foreach (var action in actions)
            {
                role.RolePermissions.Add(new RolePermission { Role = role, Module = module, Action = action, Scope = scope });
            }
        }

        db.Roles.Add(role);
        await db.SaveChangesAsync(cancellationToken);
    }
}
