/**
 * cli:scaffold-core-seed — generate.ts
 *
 * Generates 5 deterministic Core foundation seed providers PER application
 * declared in the spec. Each provider implements ONE concern of
 * IClientSeedDataProvider and returns Task.CompletedTask for the others —
 * keeps responsibilities clean and lets the per-module gate
 * (verifyCoreFoundationSeed) grep each concern in isolation.
 *
 * Output structure (one block per application):
 *   src/{AppCode}.Infrastructure/Persistence/Seeding/Applications/{AppPascal}/Core/
 *     {AppPascal}CoreNavigationSeedDataProvider.cs        (Order = 10)
 *     {AppPascal}CoreRolesSeedDataProvider.cs             (Order = 20)
 *     {AppPascal}CorePermissionsSeedDataProvider.cs       (Order = 30)
 *     {AppPascal}CoreRolePermissionsSeedDataProvider.cs   (Order = 40)
 *     {AppPascal}CoreTestUsersSeedDataProvider.cs         (Order = 50, IsDevelopment-gated)
 *
 * Plus a patched DependencyInjection.cs with one block per application
 * registered between `<<< CORE-SEED-DI-{AppPascal} BEGIN/END >>>` markers
 * (idempotent — re-running the CLI overwrites each block contents but never
 * the surrounding code, and each app's block patches independently).
 *
 * Idempotence (SmartStack convention):
 *  - GUIDs declared `static readonly` once per class — re-running scaffold
 *    does NOT regenerate them.
 *  - Each Seed*Async checks existence (composite key) before insert.
 *  - DI block per app is between markers — re-runs replace the block,
 *    leaving the rest of the file untouched.
 */
import {
  CORE_PROVIDER_SUFFIXES,
  coreProviderClassName,
  diBeginMarker,
  diEndMarker,
  pascalCase,
  type ApplicationSpec,
  type CoreSeedSpec,
  type GeneratedFile,
  type NavigationEntry,
  type NavigationLevel,
  type RoleEntry,
  type TestUserEntry,
} from './types.js';

export interface AppDiRegistration {
  /** Application's kebab-case code (e.g. `crm`). */
  appCode: string;
  /** Application's PascalCase code (e.g. `Crm`). Used for DI markers + class names. */
  appPascal: string;
  /** Full block including BEGIN/END marker comments. Idempotent to splice. */
  markerBlock: string;
}

export interface CoreSeedGenerateResult {
  files: GeneratedFile[];
  diRegistration: {
    /** One block per application — caller patches each independently. */
    perApp: AppDiRegistration[];
    /** Suggested target paths (in priority order) for the DI patch. The CLI's index.ts walks these and patches the first one that exists. */
    candidatePaths: string[];
  };
  testUsersManifest: GeneratedFile | null;
}

export function generate(spec: CoreSeedSpec): CoreSeedGenerateResult {
  const files: GeneratedFile[] = [];
  const perApp: AppDiRegistration[] = [];

  for (const app of spec.applications) {
    const appPascal = pascalCase(app.code);
    const ns =
      spec.namespace
        ? `${spec.namespace}.Applications.${appPascal}.Core`
        : `${spec.appCode}.Infrastructure.Persistence.Seeding.Applications.${appPascal}.Core`;
    const seedingDir = `src/${spec.appCode}.Infrastructure/Persistence/Seeding/Applications/${appPascal}/Core`;
    const navByCode = new Map(app.navigation.map((n) => [`${n.level}:${n.code}`, n]));

    files.push(
      {
        path: `${seedingDir}/${coreProviderClassName(appPascal, 'CoreNavigationSeedDataProvider')}.cs`,
        content: renderNavigationProvider(app, appPascal, ns, navByCode),
      },
      {
        path: `${seedingDir}/${coreProviderClassName(appPascal, 'CoreRolesSeedDataProvider')}.cs`,
        content: renderRolesProvider(app, appPascal, ns),
      },
      {
        path: `${seedingDir}/${coreProviderClassName(appPascal, 'CorePermissionsSeedDataProvider')}.cs`,
        content: renderPermissionsProvider(app, appPascal, ns),
      },
      {
        path: `${seedingDir}/${coreProviderClassName(appPascal, 'CoreRolePermissionsSeedDataProvider')}.cs`,
        content: renderRolePermissionsProvider(app, appPascal, ns),
      },
      {
        path: `${seedingDir}/${coreProviderClassName(appPascal, 'CoreTestUsersSeedDataProvider')}.cs`,
        content: renderTestUsersProvider(app, appPascal, ns, spec.testUserPassword, spec.testUserEmailDomain),
      },
    );

    perApp.push({
      appCode: app.code,
      appPascal,
      markerBlock: renderDiRegistrationBlock(appPascal, spec.appCode),
    });
  }

  const testUsersManifest =
    spec.applications.some((a) => a.testUsers.length > 0) && spec.emitDevCredentialsManifest
      ? { path: 'tests/ui-test/test-users.json', content: renderTestUsersManifest(spec) }
      : null;

  return {
    files,
    diRegistration: {
      perApp,
      candidatePaths: [
        `src/${spec.appCode}.Infrastructure/DependencyInjection.cs`,
        `src/${spec.appCode}.Infrastructure/ServiceCollectionExtensions.cs`,
        `src/${spec.appCode}.Infrastructure/InfrastructureModule.cs`,
      ],
    },
    testUsersManifest,
  };
}

// ─── Helpers ─────────────────────────────────────────────────────────────

function escapeString(s: string): string {
  return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

function emailFor(tu: TestUserEntry, domain: string): string {
  const local = tu.emailLocalPart ?? tu.roleCode;
  return `${local}.test@${domain}`;
}

function guidVar(entry: NavigationEntry, navByCode?: Map<string, NavigationEntry>): string {
  const levelSuffix =
    entry.level === 'application'
      ? 'ApplicationId'
      : entry.level === 'module'
        ? 'ModuleId'
        : entry.level === 'section'
          ? 'SectionId'
          : 'ResourceId';
  // Include the parent chain in the variable name to avoid collisions between
  // entries that share a `code` but live under different parents (e.g. two
  // modules each with a "dashboard" section → `BudgetsDashboardSectionId`
  // vs `CrmDashboardSectionId`). Application-level entries don't need a
  // prefix (the per-app namespace already isolates them).
  if (!navByCode || entry.level === 'application' || !entry.parentCode) {
    return `${pascalCase(entry.code)}${levelSuffix}`;
  }
  const segments = [entry.code];
  let current: NavigationEntry | undefined = entry;
  const guard = new Set<string>();
  while (current && current.parentCode) {
    const key = `${current.level}:${current.code}`;
    if (guard.has(key)) break;
    guard.add(key);
    const parentLevel = parentLevelOf(current.level);
    if (!parentLevel) break;
    const parent = navByCode.get(`${parentLevel}:${current.parentCode}`);
    if (!parent) break;
    if (parent.level !== 'application') {
      segments.unshift(parent.code);
    }
    current = parent;
  }
  return segments.map(pascalCase).join('') + levelSuffix;
}

function parentLevelOf(level: NavigationLevel): NavigationLevel | null {
  switch (level) {
    case 'module':
      return 'application';
    case 'section':
      return 'module';
    case 'resource':
      return 'section';
    default:
      return null;
  }
}

function componentKeyFor(
  entry: NavigationEntry,
  appCode: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  if (entry.componentKey) return entry.componentKey;
  const segments: string[] = [];
  let current: NavigationEntry | undefined = entry;
  const guard = new Set<string>();
  while (current) {
    const key = `${current.level}:${current.code}`;
    if (guard.has(key)) break;
    guard.add(key);
    segments.unshift(current.code);
    if (!current.parentCode) break;
    const parentLevel = parentLevelOf(current.level);
    if (!parentLevel) break;
    current = navByCode.get(`${parentLevel}:${current.parentCode}`);
  }
  if (segments[0] !== appCode) {
    segments.unshift(appCode);
  }
  return segments.join('.');
}

// ─── Provider 1 — Navigation ─────────────────────────────────────────────

function renderNavigationProvider(
  app: ApplicationSpec,
  appPascal: string,
  ns: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  const className = coreProviderClassName(appPascal, 'CoreNavigationSeedDataProvider');
  const apps = app.navigation.filter((n) => n.level === 'application');
  const modules = app.navigation.filter((n) => n.level === 'module');
  const sections = app.navigation.filter((n) => n.level === 'section');
  const resources = app.navigation.filter((n) => n.level === 'resource');

  return `using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces.Persistence;
using SmartStack.Domain.Authorization;
using SmartStack.Domain.Navigation;

namespace ${ns};

/// <summary>
/// Core navigation seed for application "${app.code}" — Application + every Module
/// + every Section + every Resource declared in the PRD for this app. Generated
/// by scaffold-core-seed; do not hand-edit between runs.
///
/// Idempotence pattern: each level (app/module/section/resource) is looked up
/// by code before insert. Re-running this provider does not duplicate rows.
/// Cross-references between levels resolve the parent's actual EF-assigned Id
/// at runtime so the provider does not depend on stable GUIDs across runs.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    public int Order => 10;

    public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${renderApplicationSeed(apps, app.code, navByCode)}

${renderModuleSeed(modules, app.code, navByCode)}

${renderSectionSeed(sections, app.code, navByCode)}

${renderResourceSeed(resources, app.code, navByCode)}

        await context.SaveChangesAsync(ct);
    }

    public Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
}
`;
}

function renderApplicationSeed(
  apps: NavigationEntry[],
  appCode: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  if (apps.length === 0) return `        // No application entries`;
  return apps
    .map((a) => `        if (!await context.NavigationApplications.AnyAsync(x => x.Code == "${a.code}", ct))
        {
            var navApp = NavigationApplication.Create(
                code: "${a.code}",
                label: "${escapeString(a.label)}",
                icon: "${a.icon}",
                iconType: IconType.${a.iconType},
                route: "${a.route}",
                displayOrder: ${a.displayOrder});
            context.NavigationApplications.Add(navApp);
            await context.SaveChangesAsync(ct);
        }`)
    .join('\n\n');
}

function renderModuleSeed(
  modules: NavigationEntry[],
  appCode: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  if (modules.length === 0) return `        // No module entries`;
  return modules
    .map((m) => {
      const parentVar = m.parentCode
        ? guidVar({ ...m, code: m.parentCode, level: 'application', parentCode: undefined } as NavigationEntry, navByCode)
        : 'Guid.Empty';
      return `        {
            var parentApp_${m.code.replace(/[^A-Za-z0-9_]/g, '_')} = await context.NavigationApplications.FirstOrDefaultAsync(a => a.Code == "${m.parentCode ?? ''}", ct);
            if (parentApp_${m.code.replace(/[^A-Za-z0-9_]/g, '_')} is not null && !await context.NavigationModules.AnyAsync(x => x.Code == "${m.code}" && x.ApplicationId == parentApp_${m.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id, ct))
            {
                var navModule = NavigationModule.Create(
                    applicationId: parentApp_${m.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id,
                    code: "${m.code}",
                    label: "${escapeString(m.label)}",
                    icon: "${m.icon}",
                    iconType: IconType.${m.iconType},
                    route: "${m.route}",
                    displayOrder: ${m.displayOrder});
                context.NavigationModules.Add(navModule);
                await context.SaveChangesAsync(ct);
            }
        }`;
    })
    .join('\n\n');
}

function renderSectionSeed(
  sections: NavigationEntry[],
  appCode: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  if (sections.length === 0) return `        // No section entries`;
  return sections
    .map((s) => {
      // Reconstruct the parent module entry from navByCode so the variable
      // name matches what was declared (e.g. `BudgetsModuleId`, prefixed by app).
      const parentEntry = s.parentCode ? navByCode.get(`module:${s.parentCode}`) : undefined;
      const parentVar = parentEntry
        ? guidVar(parentEntry, navByCode)
        : 'Guid.Empty';
      return `        {
            var parentMod_${s.code.replace(/[^A-Za-z0-9_]/g, '_')} = await context.NavigationModules.FirstOrDefaultAsync(m => m.Code == "${s.parentCode ?? ''}", ct);
            if (parentMod_${s.code.replace(/[^A-Za-z0-9_]/g, '_')} is not null && !await context.NavigationSections.AnyAsync(x => x.Code == "${s.code}" && x.ModuleId == parentMod_${s.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id, ct))
            {
                var section = NavigationSection.Create(
                    moduleId: parentMod_${s.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id,
                    code: "${s.code}",
                    label: "${escapeString(s.label)}",
                    icon: "${s.icon}",
                    iconType: IconType.${s.iconType},
                    route: "${s.route}",
                    displayOrder: ${s.displayOrder});
                context.NavigationSections.Add(section);
                await context.SaveChangesAsync(ct);
            }
        }`;
    })
    .join('\n\n');
}

function renderResourceSeed(
  resources: NavigationEntry[],
  appCode: string,
  navByCode: Map<string, NavigationEntry>,
): string {
  if (resources.length === 0) return `        // No resource entries`;
  return resources
    .map((r) => {
      const parentEntry = r.parentCode ? navByCode.get(`section:${r.parentCode}`) : undefined;
      const parentVar = parentEntry
        ? guidVar(parentEntry, navByCode)
        : 'Guid.Empty';
      return `        {
            var parentSec_${r.code.replace(/[^A-Za-z0-9_]/g, '_')} = await context.NavigationSections.FirstOrDefaultAsync(s => s.Code == "${r.parentCode ?? ''}", ct);
            if (parentSec_${r.code.replace(/[^A-Za-z0-9_]/g, '_')} is not null && !await context.NavigationResources.AnyAsync(x => x.Code == "${r.code}" && x.SectionId == parentSec_${r.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id, ct))
            {
                var resource = NavigationResource.Create(
                    sectionId: parentSec_${r.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id,
                    code: "${r.code}",
                    label: "${escapeString(r.label)}",
                    route: "${r.route}",
                    displayOrder: ${r.displayOrder});
                context.NavigationResources.Add(resource);
                await context.SaveChangesAsync(ct);
            }
        }`;
    })
    .join('\n\n');
}

// ─── Provider 2 — Roles ──────────────────────────────────────────────────

function renderRolesProvider(app: ApplicationSpec, appPascal: string, ns: string): string {
  const className = coreProviderClassName(appPascal, 'CoreRolesSeedDataProvider');
  const navClassName = coreProviderClassName(appPascal, 'CoreNavigationSeedDataProvider');

  const roleInserts =
    app.roles.length === 0
      ? '        // No roles defined'
      : app.roles
          .map(
            (r) => `        {
            var navApp_${r.code.replace(/[^A-Za-z0-9_]/g, '_')} = await context.NavigationApplications.FirstOrDefaultAsync(a => a.Code == "${r.applicationCode}", ct);
            if (navApp_${r.code.replace(/[^A-Za-z0-9_]/g, '_')} is not null && !await context.Roles.AnyAsync(x => x.Code == "${r.code}" && x.ApplicationId == navApp_${r.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id, ct))
            {
                var role = Role.Create(
                    name: "${escapeString(r.name)}",
                    shortName: "${escapeString(r.code)}",
                    category: RoleCategory.Custom,
                    applicationId: navApp_${r.code.replace(/[^A-Za-z0-9_]/g, '_')}.Id,
                    code: "${r.code}");
                context.Roles.Add(role);
            }
        }`,
          )
          .join('\n');

  return `using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces.Persistence;
using SmartStack.Domain.Authorization;
using SmartStack.Domain.Navigation;
using SmartStack.Domain.Platform.Administration.Roles;

namespace ${ns};

/// <summary>
/// Core roles seed for application "${app.code}" — one Role per actor declared in
/// the PRD for this app. Resolves the parent NavigationApplication by code at
/// runtime so this provider does not depend on stable GUIDs across runs.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    public int Order => 20;

    public Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${roleInserts}

        await context.SaveChangesAsync(ct);
    }

    public Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
}
`;
}

// ─── Provider 3 — Permissions ────────────────────────────────────────────

function renderPermissionsProvider(app: ApplicationSpec, appPascal: string, ns: string): string {
  const className = coreProviderClassName(appPascal, 'CorePermissionsSeedDataProvider');

  // SmartStack 3.45 Permission API:
  //   Permission.CreateForSection(Guid sectionId, string path, PermissionAction? action,
  //                               bool isWildcard, string description)
  //   PermissionAction { Access, Read, Create, Update, Delete, Export, Import,
  //                      Approve, Reject, Assign, Execute }
  //   ICoreDbContext.Permissions : DbSet<Permission>
  // sectionId is resolved at runtime by looking up NavigationSections by code,
  // matching the same pattern CoreRolePermissionsSeedDataProvider uses for
  // Role/Permission lookups. Idempotence: composite-key duplicate check on Path
  // before insert.
  const permsBlock =
    app.permissions.length === 0
      ? '        // No permissions defined'
      : `        var permissionsToSeed = new (string Path, string Action, string SectionCode)[]
        {
${app.permissions.map((p) => `            ("${escapeString(p.path)}", "${escapeString(p.action)}", "${escapeString(p.sectionCode)}"),`).join('\n')}
        };

        foreach (var perm in permissionsToSeed)
        {
            var section = await context.NavigationSections.FirstOrDefaultAsync(s => s.Code == perm.SectionCode, ct);
            if (section is null) continue;
            if (await context.Permissions.AnyAsync(p => p.Path == perm.Path, ct)) continue;

            var permission = Permission.CreateForSection(
                sectionId: section.Id,
                path: perm.Path,
                action: ParseAction(perm.Action),
                isWildcard: false,
                description: $"Auto-generated for {perm.Path}");
            context.Permissions.Add(permission);
        }

        await context.SaveChangesAsync(ct);`;

  return `using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces.Persistence;
using SmartStack.Domain.Authorization;
using SmartStack.Domain.Navigation;

namespace ${ns};

/// <summary>
/// Core permissions seed for application "${app.code}" — one Permission per
/// (path, action, section) triplet from this app's structured.permissions[]. Each
/// permission is scoped to a NavigationSection resolved by code at runtime, so
/// the provider does not depend on stable GUIDs across runs. The Path duplicate
/// check makes this idempotent across re-runs. Generated by scaffold-core-seed;
/// do not hand-edit.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    public int Order => 30;

    public Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${permsBlock}
    }

    public Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    private static PermissionAction? ParseAction(string action) => action.ToLowerInvariant() switch
    {
        "access" => PermissionAction.Access,
        "read" => PermissionAction.Read,
        "create" => PermissionAction.Create,
        "update" => PermissionAction.Update,
        "delete" => PermissionAction.Delete,
        "export" => PermissionAction.Export,
        "import" => PermissionAction.Import,
        "approve" => PermissionAction.Approve,
        "reject" => PermissionAction.Reject,
        "assign" => PermissionAction.Assign,
        "execute" => PermissionAction.Execute,
        _ => null,
    };
}
`;
}

// ─── Provider 4 — RolePermissions ────────────────────────────────────────

function renderRolePermissionsProvider(app: ApplicationSpec, appPascal: string, ns: string): string {
  const className = coreProviderClassName(appPascal, 'CoreRolePermissionsSeedDataProvider');

  const mappingsBlock =
    app.rolePermissions.length === 0
      ? '        // No role-permission mappings defined'
      : `        var mappings = new (string RoleCode, string PermissionPath)[]
        {
${app.rolePermissions.map((rp) => `            ("${rp.roleCode}", "${rp.permissionPath}"),`).join('\n')}
        };

        foreach (var mapping in mappings)
        {
            var role = await context.Roles.FirstOrDefaultAsync(r => r.Code == mapping.RoleCode, ct);
            var permission = await context.Permissions.FirstOrDefaultAsync(
                p => p.Path == mapping.PermissionPath,
                ct);

            if (role is null || permission is null) continue;

            if (!await context.RolePermissions.AnyAsync(
                rp => rp.RoleId == role.Id && rp.PermissionId == permission.Id,
                ct))
            {
                context.RolePermissions.Add(RolePermission.Create(role.Id, permission.Id));
            }
        }

        await context.SaveChangesAsync(ct);`;

  return `using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces.Persistence;
using SmartStack.Domain.Navigation;
using SmartStack.Domain.Platform.Administration.Roles;
using SmartStack.Domain.Platform.Administration.Users;

namespace ${ns};

/// <summary>
/// Core role-permission mappings for application "${app.code}" — wires each role to
/// its allowed permissions. Lookups by Code at runtime (decoupled from the per-app
/// CoreRolesSeedDataProvider's GUID fields), so this provider is safe to regenerate
/// independently. Generated by scaffold-core-seed; do not hand-edit.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    public int Order => 40;

    public Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${mappingsBlock}
    }
}
`;
}

// ─── Provider 5 — Test users (dev-only) ──────────────────────────────────

function renderTestUsersProvider(
  app: ApplicationSpec,
  appPascal: string,
  ns: string,
  testUserPassword: string,
  testUserEmailDomain: string,
): string {
  const className = coreProviderClassName(appPascal, 'CoreTestUsersSeedDataProvider');
  const rolesByCode = new Map<string, RoleEntry>(app.roles.map((r) => [r.code, r]));

  const userInserts = app.testUsers
    .map((tu) => {
      const role = rolesByCode.get(tu.roleCode);
      if (!role) return `        // Skipped test user: role "${tu.roleCode}" not found in spec`;
      const email = emailFor(tu, testUserEmailDomain);
      const localPart = tu.emailLocalPart ?? tu.roleCode;
      return `        await EnsureTestUserAsync(context, "${email}", "${escapeString(localPart)} (test)", "${role.code}", TestUserPassword, ct);`;
    })
    .join('\n');

  return `using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces.Persistence;
using SmartStack.Domain.Navigation;
using SmartStack.Domain.Platform.Administration.Roles;
using SmartStack.Domain.Platform.Administration.Users;

namespace ${ns};

/// <summary>
/// Core test users for application "${app.code}" — one user per role declared in
/// the app's testUsers[]. Gated by IHostEnvironment.IsDevelopment() so production
/// deployments NEVER seed these.
///
/// PROJECT-SPECIFIC CUSTOMIZATION REQUIRED:
/// The User entity factory and password hashing API differ per project. Adapt
/// EnsureTestUserAsync below to your User entity. Common patterns:
///   - User.CreateForDevelopment(...)  (factory method)
///   - new User { Email = ..., PasswordHash = passwordHasher.Hash(...) }
///   - IUserAdminService.CreateAsync(...)  (application service)
///
/// Order = 50 ensures roles + permissions + role-permissions exist by the time
/// this runs.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    private readonly IHostEnvironment _env;

    public ${className}(IHostEnvironment env)
    {
        _env = env;
    }

    public int Order => 50;

    // Hardcoded dev password — only used when IsDevelopment() is true.
    private const string TestUserPassword = "${escapeString(testUserPassword)}";

    public Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;
    public Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
        // STUB — User creation requires a password hasher (IPasswordHasher / scrypt service)
        // that IClientSeedDataProvider does not expose. The proper integration uses
        // User.Create(email, passwordHash, firstName, lastName) with a pre-computed hash,
        // OR a dedicated IDevTestUserService injected via DI.
        //
        // Test users to seed for application "${app.code}" (when IsDevelopment()):
${app.testUsers.map((tu) => {
  const role = rolesByCode.get(tu.roleCode);
  if (!role) return `        //   - <unknown role: ${tu.roleCode}>`;
  return `        //   - ${emailFor(tu, testUserEmailDomain)} (role=${role.code})`;
}).join('\n')}
        return Task.CompletedTask;
    }

    /// <summary>
    /// Reserved hook — implement when adapting to the project's User entity API.
    /// </summary>
    private static Task EnsureTestUserAsync(
        ICoreDbContext context,
        string email,
        string displayName,
        string roleCode,
        string password,
        CancellationToken ct)
    {
        return Task.CompletedTask;
    }
}
`;
}

function renderTestUsersManifest(spec: CoreSeedSpec): string {
  const users: Array<{
    role: string;
    email: string;
    password: string;
    applicationCode: string;
    roleName: string;
  }> = [];

  for (const app of spec.applications) {
    const rolesByCode = new Map<string, RoleEntry>(app.roles.map((r) => [r.code, r]));
    for (const tu of app.testUsers) {
      const role = rolesByCode.get(tu.roleCode);
      if (!role) continue;
      users.push({
        role: tu.roleCode,
        email: emailFor(tu, spec.testUserEmailDomain),
        password: spec.testUserPassword,
        applicationCode: role.applicationCode,
        roleName: role.name,
      });
    }
  }

  return JSON.stringify(
    {
      generatedAt: new Date().toISOString(),
      appCode: spec.appCode,
      // Gitignored. Used by skills/development/testing/ui-test/cli/run-ui-test.
      // Regenerated on every scaffold-core-seed run — do not edit by hand.
      users,
    },
    null,
    2,
  ) + '\n';
}

// ─── DI block ────────────────────────────────────────────────────────────

function renderDiRegistrationBlock(appPascal: string, appCode: string): string {
  // Fully-qualified type names — the per-app providers live in nested
  // namespaces `{AppCode}.Infrastructure.Persistence.Seeding.Applications.{AppPascal}.Core`,
  // and DependencyInjection.cs (the patch host) doesn't necessarily import
  // them. Fully-qualifying avoids needing a `using` directive at file scope
  // (which we can't safely inject via the marker block).
  const ns = `${appCode}.Infrastructure.Persistence.Seeding.Applications.${appPascal}.Core`;
  const lines = CORE_PROVIDER_SUFFIXES.map(
    (suffix) => `        services.AddScoped<IClientSeedDataProvider, ${ns}.${coreProviderClassName(appPascal, suffix)}>();`,
  );
  return [
    `        ${diBeginMarker(appPascal)}`,
    ...lines,
    `        ${diEndMarker(appPascal)}`,
  ].join('\n');
}

/**
 * Patch a DependencyInjection.cs source for ONE application's DI block.
 * If the BEGIN/END markers for this `appPascal` exist, replace the block
 * content idempotently. Otherwise insert before the last `return services;`
 * (or the file's last `}`).
 *
 * Returns null when the block was already up-to-date (no rewrite needed).
 */
export function patchDependencyInjection(source: string, registration: AppDiRegistration): string | null {
  const begin = diBeginMarker(registration.appPascal);
  const end = diEndMarker(registration.appPascal);
  const block = registration.markerBlock;

  const beginIdx = source.indexOf(begin);
  const endIdx = source.indexOf(end);

  if (beginIdx >= 0 && endIdx >= 0 && endIdx > beginIdx) {
    // Replace existing block. Find the start of the line containing BEGIN and
    // end of the line containing END (so we replace whole lines, not partial).
    const lineStart = source.lastIndexOf('\n', beginIdx) + 1;
    const lineEnd = source.indexOf('\n', endIdx);
    const before = source.slice(0, lineStart);
    const after = lineEnd >= 0 ? source.slice(lineEnd + 1) : '';
    const next = before + block + '\n' + after;
    return next === source ? null : next;
  }

  // No markers — try to insert before the LAST `return services;` statement
  // (the canonical end-of-method marker for SmartStack DI extension methods).
  const returnMatches = [...source.matchAll(/\n\s*return services;\s*\n/g)];
  if (returnMatches.length > 0) {
    const last = returnMatches[returnMatches.length - 1];
    const insertAt = last.index!;
    const before = source.slice(0, insertAt);
    const after = source.slice(insertAt);
    return before + '\n' + block + after;
  }

  // Last resort — append before the file's last `}`. The agent will see this
  // and may need to manually move it inside an extension method body.
  const lastBrace = source.lastIndexOf('}');
  if (lastBrace >= 0) {
    return source.slice(0, lastBrace) + block + '\n' + source.slice(lastBrace);
  }

  return null;
}

// ─── Stable GUIDs ────────────────────────────────────────────────────────

/**
 * Compute a deterministic GUID v5-like string from an arbitrary key. Uses a
 * simple FNV-1a 128-bit-ish digest folded into the GUID format. Same key →
 * same GUID forever, so re-running scaffold-core-seed does NOT churn the
 * database row IDs (re-run idempotence requirement).
 *
 * Not cryptographic — just deterministic. Collisions across distinct keys
 * are statistically negligible at the scale of one app's seed data.
 */
function stableGuid(key: string): string {
  // Simple deterministic 128-bit digest using two FNV-1a passes with
  // different seeds — keeps the implementation dependency-free and fast.
  const fnv = (s: string, seed: number): bigint => {
    let h = BigInt(seed) & 0xffffffffffffffffn;
    const PRIME = 1099511628211n;
    for (let i = 0; i < s.length; i++) {
      h ^= BigInt(s.charCodeAt(i));
      h = (h * PRIME) & 0xffffffffffffffffn;
    }
    return h;
  };
  const high = fnv(key, 0x811c9dc5).toString(16).padStart(16, '0');
  const low = fnv(key + ':low', 0x811c9dc5).toString(16).padStart(16, '0');
  // GUID format: 8-4-4-4-12
  return `${high.slice(0, 8)}-${high.slice(8, 12)}-${high.slice(12, 16)}-${low.slice(0, 4)}-${low.slice(4, 16)}`;
}
